mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
cleanup: remove TUI package and legacy dsh entrypoints
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 apps/cli/README.md
|
||||
README.md: 76d9ed65398322cb9244a31661ee59b60c23f793
|
||||
README.zh.md: 16a7a4ec52b830e45c32a61a103d87be5941ab3b
|
||||
README.md: 360ab26ec3dfecf2841a012fda8947d6a84fdfec
|
||||
README.zh.md: 3ffa5d7726a798b784c67fdb8c4154fddbdea7a4
|
||||
|
||||
@@ -2,82 +2,73 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The `dsh` command has three entry modes: a required raw config overlay, a one-shot headless prompt, and the Web UI. [`src/args.ts`](src/args.ts) owns the Commander grammar, and [`src/bin.ts`](src/bin.ts) dynamically imports only the selected runner. Unknown commands and leaked options fail with a nonzero exit code.
|
||||
|
||||
Argv is parsed once through a [Commander](https://github.com/tj/commander.js) adapter ([`src/args.ts`](src/args.ts)): one program whose default (no subcommand) is the TUI/headless surface (`--config`, `-p`/`--prompt`, `--resume`, `--dump-config`, `--dump-default-config`), whose `meta` subcommand is the same TUI over this checkout, whose `upgrade` subcommand is a guided-session entry, and whose `web` subcommand is the browser UI. `meta` and `upgrade` are experimental: each runs only with its `--experimental` flag or with `DSH_EXPERIMENTAL=1` in the environment, and fails loud (stderr, exit 1) otherwise. `src/bin.ts` switches on the resolved mode and dynamic-imports only that mode's module. `dsh --help` lists every mode and `dsh web --help` renders the web usage, `dsh --version` prints this app's version, and an unknown option or a mistyped `--resume` fails loud (stderr, exit 1) instead of misrouting. Every subcommand that shares no option with the default surface — `upgrade`, `web`, `meta` — rejects a leaked `--config`/`-p`/`--resume`/dump flag rather than running and dropping it. `dsh web`'s `--host`/`--port` are unvalidated pass-through overrides: the `dsh-host-webserver` schema is the single source of both the default (the shipped Web overlay value when a flag is absent) and validity, and rejects a bad value at boot. `--trusted-host` appends named authorities for the /api browser-trust fence; an all-interfaces bind additionally derives the machine's LAN IP literals itself ([`src/app-cli-entry.ts`](src/app-cli-entry.ts)), so the printed LAN URL works without flags.
|
||||
## Raw config
|
||||
|
||||
The TUI surface:
|
||||
|
||||
- boots `base.cordis.yml` plus `tui.cordis.yml` through [`dsh-app-boot`](../../packages/ui/app-boot/README.md); `--config <path>` applies a patch-list overlay instead of the personal overlay, while `--config-replace <path>` boots that file as the complete tree;
|
||||
- resumes a persisted session with `dsh --resume <session-id>` and, when the Node host exposes `process.execve`, supplies the TUI's in-place handoff host: after selector preflight and current-session flush, the host disposes the app and replaces the process with a normalized resume invocation; runtimes without process replacement leave the session running and say so. This CLI owns session identity and the exit line rather than the config: it mints or selects the `main` session id and provides it, plus the exact command that reproduces this invocation, on the boot context ([`MAIN_SESSION_ID_KEY`](../../packages/ui/tui/README.md) and `TUI_GOODBYE_MESSAGE_KEY`). No `cordis.yml` key can drop resume, and a missing or unreadable id fails loud instead of creating a fresh session;
|
||||
- treats the **invoking directory** as the workspace — sessions, relative paths, and workspace instructions resolve from the cwd (`dsh meta` is the sole exception, below);
|
||||
- tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential `cordis` toolset can read and modify it;
|
||||
- applies the personal overlay from `~/.dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `config.yaml` patches the booted tree, while `.env` there is the credential provider's own store (never hoisted into the environment, so keys stay rotatable). Environment precedence is ambient > project `.env`. The shipped tree's Cordis HMR keeps `config.yaml` live; an explicit `--config` tree replaces that overlay, and a tree without HMR reads it at startup only.
|
||||
- presents the [versioned first-run welcome](../../.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.md) through the mounted TUI overlay service when its immutable marker is absent under `DSH_HOME`; only Enter creates that version's marker, while Escape, disposal, or process exit leaves it eligible. The official DeepSeek icon, responsive terminal rasters, all-locale Chinese copy, and notice version are static local owners; the overlay never writes a session event or model context.
|
||||
- registers bare `/compact`: while the agent is idle, it summarizes useful older history even below automatic pressure, rejects arguments, and reports success only after the standalone replacement bracket is durable. A prompt submitted during compaction keeps its queue identity and starts after that checkpoint; injected context remains visible.
|
||||
|
||||
`dsh meta` is that same TUI with this harness checkout as the workspace, so working on dsh itself needs no `cd`. It chdirs to the checkout root — resolved from the launcher's real path, the same root the source-path prompt section names — after the environment is settled, so precedence is unchanged while the session cwd and HMR watch root move together. Meta always starts a fresh session and accepts no default-surface options; use ordinary `dsh --resume <id>` to resume a persisted session.
|
||||
|
||||
`dsh upgrade` is a guided fresh-session entry over the default TUI surface: it mints a fresh session in the invoking directory and seeds its first turn with the bundled `dsh-upgrade` skill, exactly as if the user typed `/skill:<name>`. The launcher passes the skill name on the boot context ([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)), which the TUI auto-invokes once the chat is live. The command takes no options beyond the experimental gate — `--config`, `-p`, and `--resume` fail loud — and seeds only on this first launch, so a later `dsh --resume <id>` of the session is an ordinary TUI session with no re-injection.
|
||||
|
||||
`dsh --dump-config` and `dsh web --dump-config` print the composed config tree — the shipped base, the surface overlay, and the `--config` or personal overlay, exactly the layers that surface would boot — as YAML on stdout and exit without booting; `--dump-default-config` stops at the surface overlay, so diffing the two shows precisely what the user layer changes. Each run of rows is preceded by a `# ==` comment naming the file it comes from and the layers that patched it (e.g. `# == base.cordis.yml, patched by tui.cordis.yml`), so the output shows provenance while staying one loadable document. Composition runs through the include's own patch algorithm and YAML dialect (`applyEntryPatches`/`entryListSchema` from `@cordisjs/plugin-include`), so the dump cannot drift from what boots; `!!js` expressions print verbatim and unevaluated, and a patch whose target row is absent is reported on stderr with its layer, mirroring the Loader's boot-time warning. Launcher-owned boot-context values (session identity, CLI-flag patches) are per-invocation facts outside the config tree and do not appear. The dump flags reject boot-only flags (`-p`, `--resume`, `--config-replace`) rather than silently ignoring them, and `--dump-default-config` takes no `--config`.
|
||||
|
||||
The Web and headless surfaces boot `base.cordis.yml` plus `web.cordis.yml`, then apply `$DSH_HOME/config.yaml`; an explicit `--config <path>` replaces that personal overlay. Both surfaces otherwise share the same composition: both tell the coding agent its resolved model and session working directory, treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root <path>` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, opt into first-message model titles, use the same bounded transient model-request retry policy as the TUI, and mount a disposable in-memory SQLite content-index service. Web additionally names the DeepSeek Harness Web GUI as the interaction surface, this checkout as its own source location, and the process's canonical local URL and mode in both the prompt and managed `$DSH_WEB_URL`/`$DSH_WEB_MODE`; references such as “this page” therefore identify the GUI without claiming access to implicit DOM, route, or screenshot state. In production mode the host reads rebuilt frontend dist and client bundles on the next request, so refreshing the existing URL updates that GUI without replacing its process. `dsh web --dev` mounts the client-plugin HMR receiver, but no-refresh updates additionally require `pnpm run dev:web` in the same checkout to watch and rebuild plugin bundles; shell and ordinary package changes still require a rebuild and page refresh. Bare `apps/web` Vite serving fails before listening because it cannot inject `window.__DSH_BOOT__`. The index service is ACTIVE at boot, while its `node:sqlite` module and database handle open only on the first content search. This keeps Node 22 startup output free of SQLite's experimental warning before search is used; the first actual search may still emit the runtime warning. Each service instance owns its database, so parallel invocations neither share unsupported SQLite state nor leave derived index files behind, and the first search lazily reconciles live and persisted logs. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`).
|
||||
|
||||
The shared composition defaults new TUI, Web, and headless sessions to the `workspace-write` permission preset (`workspace-write` file mode plus `ask` approval policy). Sandbox-enforced bash and filesystem mutations may write only under the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. The browser answers one-shot approval requests and exposes the Access picker; the TUI exposes `/permission`, but has no approval-request answerer, so an automatic wider retry there fails closed until the user deliberately changes the session preset. `DSH_PERMISSION_MODE` changes the process fallback, while a stored General-settings Permission value applies to later sessions without changing an open one.
|
||||
|
||||
All three surfaces consume `$DSH_HOME/config.yaml`; the TUI and Web apply valid edits live, while one-shot headless runs read it at startup. The shipped trees include an empty `repository-plugins` row, so a standalone user can add prepared GitHub Plugins without an SDK project or install command:
|
||||
|
||||
```yaml
|
||||
- id: repository-plugins
|
||||
name: '@deepseek-ai/dsh-repository-plugin'
|
||||
config:
|
||||
repositories:
|
||||
- 'github:PolyArch/humanize#<commit>'
|
||||
```
|
||||
|
||||
The repository must contain a prepared `.dsh-plugin` package; the [repository Plugin contract](../../packages/cordis/repository-plugin/README.md#standalone-app-configuration) documents authoring, nested Plugin paths, the immutable cache, trust boundary, and failure semantics. A failed live edit keeps the last good tree and emits Cordis's `hmr/config-update-failed` event.
|
||||
|
||||
The shipped TUI and Web compositions register the native DeepSeek adapter plus pi-ai OpenAI and Anthropic profiles. Credentials and endpoint overrides come from the provider-standard `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`, `OPENAI_API_KEY` / `OPENAI_BASE_URL`, and `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL` pairs in the boot's layered environment.
|
||||
|
||||
Every surface also registers `web_search` and only `web_search`. Search uses DeepSeek's Anthropic-compatible Messages endpoint, resolves the same `DEEPSEEK_API_KEY` reference for every call, and accepts the separate `DEEPSEEK_SEARCH_BASE_URL` endpoint override; each search is an auxiliary model request with its own latency and token cost. `web_fetch` remains disabled and the composition mounts no default fetch provider, so deployments that need arbitrary page retrieval must opt in through an overlay. The deployment decision and its security boundary live in the [default Web search Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-default-search.md).
|
||||
|
||||
`DSH_TOOLS_MODE` selects the tool presentation mode for the whole Web/headless process: `native` (the schema default when unset), `code` (the `run_code`-only Code Mode wire), or `both`; any other value fails loud at boot through the `dsh-tools` config schema. It is a TEMPORARY seam — process-wide because Loader composition is static — and is removed once the web UI owns per-session tool-mode selection; the TUI surface ignores it and pins `native`.
|
||||
|
||||
[`core-web.cordis.yml`](config/core-web.cordis.yml) is an opt-in `dsh web --config` overlay that keeps the shipped Web host, browser, Workspace, persistence, and permission composition while reducing the default native model surface to owner-scoped persistent `bash` and `str_replace_editor`. The PTY backend and editor consume the existing Web sandbox and filesystem providers. An open persistent shell prevents changing that session's permission mode until the shell closes, so a shell created under wider access cannot survive a downgrade. `DSH_TOOLS_MODE` still controls native/Code Mode presentation for the resulting two-tool registry.
|
||||
|
||||
From a source checkout, start this minimal Web profile with:
|
||||
Raw `dsh` requires an explicit patch-list config:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web --config apps/cli/config/core-web.cordis.yml
|
||||
dsh --config ./app.cordis.yml
|
||||
```
|
||||
|
||||
Every `dsh` surface — TUI, Web, and headless — reports session telemetry by default (the row lives in the shared `base.cordis.yml`): every session-log event streams as OTLP/HTTP log records to `https://harness-telemetry.deepseeksvc.com/v1/logs` on a 10-second batch cadence. `DSH_TELEMETRY_OTLP_URL` points the exporter at a different collector; setting `DSH_TELEMETRY_DISABLED` to ANY non-empty value — including `0` or `false` — disables the row before it loads (a privacy switch prefers off-by-mistake over on-by-mistake). No redaction rule is mounted in this composition yet: exported records are the raw captured copy, including message text, tool arguments and results, and the session's working-directory path. The deployment rulings live in the [web-telemetry-default-mount Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md).
|
||||
The named file is applied directly over [`config/base.cordis.yml`](config/base.cordis.yml) through the Include plugin's patch algorithm. It is not a complete replacement tree, and neither the personal `$DSH_HOME/config.yaml` nor another surface overlay is added. The base deliberately contains no startup agent or interaction front door; the required overlay selects those deployment details. Relative config paths resolve from the invoking directory. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit.
|
||||
|
||||
MCP servers are not a shipped default, because a default would have to name one: `@deepseek-ai/dsh-mcp-client` mounts exactly one server per row and spawns it as a child process, outside `ctx.bash` and so outside the sandbox policy. The package is a runtime dependency of this CLI, so an installed `dsh` can mount your own servers from `$DSH_HOME/config.yaml` or a `--config` overlay without a source checkout:
|
||||
A patch targets a base row by `id` and replaces that row's complete `config` value rather than deep-merging keys. Patch lists may also insert new rows whose plugin modules the shipped Loader can resolve:
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: mcp-github
|
||||
name: '@deepseek-ai/dsh-mcp-client'
|
||||
config:
|
||||
serverName: github
|
||||
transport: stdio
|
||||
command: npx
|
||||
args: ['-y', '@modelcontextprotocol/server-github']
|
||||
env:
|
||||
GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN
|
||||
- id: agent-loop
|
||||
config:
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
The model then sees `mcp__github__*`. See the [mcp-client README](../../packages/mcp/mcp-client/README.md) for the Streamable HTTP transport and the full field table.
|
||||
Inspect the effective tree without booting it:
|
||||
|
||||
## Install (developer machine)
|
||||
```sh
|
||||
dsh --dump-default-config
|
||||
dsh --config ./app.cordis.yml --dump-config
|
||||
```
|
||||
|
||||
Symlink the source-running launcher onto your PATH; it resolves the checkout through its own real path, so code changes apply on the next launch with no build step:
|
||||
`--dump-default-config` prints only the shipped base. `--dump-config` requires `--config` and prints base plus overlay with provenance comments. Composition uses `applyEntryPatches` and `entryListSchema` from `@cordisjs/plugin-include`; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr.
|
||||
|
||||
## Web and headless
|
||||
|
||||
`dsh web` boots `base.cordis.yml` plus [`config/web.cordis.yml`](config/web.cordis.yml), followed by `$DSH_HOME/config.yaml` when present. `dsh web --config <path>` replaces that personal layer with the explicit patch list. `--host`, `--port`, `--workspace-root`, and repeatable `--trusted-host` values become Web host patches; their owning plugin schemas validate them at boot. `--dev` mounts the client-plugin HMR receiver and expects a separate `pnpm run dev:web` watcher for no-refresh client bundle updates.
|
||||
|
||||
```sh
|
||||
dsh web
|
||||
dsh web --config ./web-profile.cordis.yml
|
||||
dsh web --dump-default-config
|
||||
dsh web --dump-config
|
||||
```
|
||||
|
||||
The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default. Binding all interfaces also trusts the machine's discovered LAN IP literals; `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence.
|
||||
|
||||
`dsh -p "task"` uses the same base and Web composition with the startup personal config, starts its Web host on an OS-assigned port, runs one fresh persisted session, prints the final answer, and exits. It accepts neither `--config` nor raw config-dump flags.
|
||||
|
||||
Both modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. Web watches valid personal config edits; headless reads the file once at startup. The [app-boot personal-config contract](../../packages/ui/app-boot/README.md#personal-config) owns layer precedence, credential storage, live-update failure behavior, and `$DSH_HOME` resolution.
|
||||
|
||||
New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one.
|
||||
|
||||
`DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the Web/headless process; another value fails at boot. [`config/core-web.cordis.yml`](config/core-web.cordis.yml) is an optional Web overlay that reduces the native model surface to persistent `bash` and `str_replace_editor` while retaining the shipped host, browser, workspace, persistence, and permission composition.
|
||||
|
||||
## Shared deployment behavior
|
||||
|
||||
The base mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, repository Plugin support, and session telemetry. Provider credentials live in `$DSH_HOME/.env` or the ambient environment and remain rotatable because the launcher never hoists the credential file into `process.env`. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless an overlay inserts a provider and enables it.
|
||||
|
||||
Session events stream as OTLP/HTTP logs by default. `DSH_TELEMETRY_OTLP_URL` selects another collector. Any non-empty `DSH_TELEMETRY_DISABLED` disables the telemetry row before boot. The shipped base has no telemetry redaction rule, so exported records can contain message text, tool arguments and results, and workspace paths; the [telemetry Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md) owns that deployment decision.
|
||||
|
||||
The empty `repository-plugins` row lets Web/headless personal config and raw overlays mount prepared immutable repository Plugin generations. See the [repository Plugin contract](../../packages/cordis/repository-plugin/README.md#standalone-app-configuration). The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for overlays, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
|
||||
|
||||
## Source launcher
|
||||
|
||||
Link the source-running launcher onto PATH:
|
||||
|
||||
```sh
|
||||
ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh
|
||||
```
|
||||
|
||||
Source launches run `apps/cli/src/bin.ts` through tsx's ESM-only hook (`node --import tsx/esm`), which transforms TypeScript and projects the root tsconfig `paths` map into module resolution. Node's native TypeScript modes are not used: Node 26 removed `--experimental-transform-types`, and strip-only mode rejects syntax the source graph relies on (vendored parameter properties, decorators, runtime enums/namespaces). The CJS hook stays off because the source graph is ESM-only and the CJS resolver adds ~0.4s of startup. `bin/dsh` pins `TSX_TSCONFIG_PATH` to the checkout's root tsconfig so resolution is cwd-independent, and the `dsh-source-launch-smoke` node-compat gate runs this exact launch vector on every supported Node line. tsx applies the `paths` map without checking dependency declarations, so declaration completeness rests on the static gates: the TUI configs resolve bare plugins through `examples/package.json`, the Web/headless `cordis.yml` through this package's `dependencies`, and `verify-cordis-config` requires every configured bare plugin to be declared, while allowing unrelated dependencies.
|
||||
|
||||
`pnpm run dsh` runs the same entry from the repo root and forwards arguments directly, for example `pnpm run dsh -p "task"`. The built form (`lib/bin.js`, via `pnpm run build`) boots the same config under plain Node.
|
||||
It resolves the checkout through its real path and launches `apps/cli/src/bin.ts` with `node --import tsx/esm`. `TSX_TSCONFIG_PATH` is pinned to the checkout root, so workspace package resolution is independent of the invoking directory. `pnpm run dsh` uses the same entry and forwards arguments. The built form is `apps/cli/lib/bin.js` after `pnpm run build`.
|
||||
|
||||
@@ -2,82 +2,73 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`dsh` 命令有三种入口模式:必需的原始配置 overlay、一次性 headless 提示词,以及 Web UI。[`src/args.ts`](src/args.ts) 拥有 Commander 命令行语法,[`src/bin.ts`](src/bin.ts) 只会动态导入选中模式的运行器。未知命令和误传入其他模式的选项都会以非零代码退出。
|
||||
|
||||
Argv 只会通过 [Commander](https://github.com/tj/commander.js) 适配器([`src/args.ts`](src/args.ts))解析一次:同一个程序的默认形式(无子命令)是 TUI/无头界面(`--config`、`-p`/`--prompt`、`--resume`、`--dump-config`、`--dump-default-config`),`meta` 子命令是以本 checkout 为 workspace 的同一个 TUI,`upgrade` 子命令是引导会话入口,`web` 子命令则是浏览器 UI。`meta` 与 `upgrade` 是实验性命令:只有带上各自的 `--experimental` 标志或在环境中设置 `DSH_EXPERIMENTAL=1` 才会运行,否则明确报错(stderr,退出码 1)。`src/bin.ts` 按解析后的 mode 分支,仅动态导入该 mode 的模块。`dsh --help` 列出所有 mode,`dsh web --help` 渲染 Web 用法,`dsh --version` 打印此应用的版本;未知选项或拼错的 `--resume` 会明确报错(stderr,退出码 1),而不会被错路由。凡与默认界面不共享任何选项的子命令(`upgrade`、`web`、`meta`)都会拒绝泄漏进来的 `--config`/`-p`/`--resume`/dump 标志,而不会照常运行并丢弃它。`dsh web` 的 `--host`/`--port` 是未验证的直通覆盖:`dsh-host-webserver` schema 是默认值(标志缺失时使用已交付的 Web 覆盖层值)和有效性的唯一真源,并在启动时拒绝错误值。`--trusted-host` 为 /api 浏览器信任栅栏追加具名权威;全接口绑定还会自行推导本机的 LAN IP 字面量([`src/app-cli-entry.ts`](src/app-cli-entry.ts)),因此打印出的 LAN URL 无需任何标志即可使用。
|
||||
## 原始配置
|
||||
|
||||
TUI 界面:
|
||||
|
||||
- 通过 [`dsh-app-boot`](../../packages/ui/app-boot/README.md) 启动 `base.cordis.yml` 与 `tui.cordis.yml`;`--config <path>` 应用一个补丁列表覆盖并替代个人覆盖,而 `--config-replace <path>` 将指定文件作为完整配置树启动;
|
||||
- 使用 `dsh --resume <session-id>` 恢复已持久化会话。当 Node 宿主公开 `process.execve` 时,还会提供 TUI 的原地移交宿主:选择器预检并刷新当前会话后,宿主会释放应用,并以规范化的恢复调用替换进程;不支持进程替换的运行时会让会话继续运行并给出提示。会话身份与退出行由本 CLI 拥有,而非由配置指定:它创建或选定 `main` 会话 id,并把该 id 以及可复现本次调用的确切命令一起提供到启动上下文([`MAIN_SESSION_ID_KEY`](../../packages/ui/tui/README.md) 与 `TUI_GOODBYE_MESSAGE_KEY`)。任何 `cordis.yml` 键都无法移除恢复能力;缺失或无法读取的 id 会明确报错,而不会创建新会话;
|
||||
- 将 **调用目录** 视为 workspace:会话、相对路径和 workspace 指令都从 cwd 解析(`dsh meta` 是唯一例外,见下文);
|
||||
- 告知 agent 自身源码所在位置:启动后添加一个命名此 harness checkout 的提示词段。该路径从启动器的真实路径解析,因此在 PATH 符号链接和任意 cwd 下仍然有效,使自指的 `cordis` 工具集可以读取并修改它;
|
||||
- 应用 `~/.dsh` 中的个人覆盖(参见 [app-boot 的个人配置](../../packages/ui/app-boot/README.md#personal-config)):`config.yaml` 修补已启动的树,而那里的 `.env` 是凭据 provider 自己的存储(绝不会被提升进环境,因此密钥始终可轮换)。环境优先级为环境中已有的值 > 项目 `.env`。已交付配置树中的 Cordis HMR 会持续应用 `config.yaml` 的变更;显式 `--config` 配置树会替代该个人覆盖,未包含 HMR 的配置树只在启动时读取该文件。
|
||||
- 当 `DSH_HOME` 下不存在不可变确认标记时,通过已挂载的 TUI overlay 服务呈现[版本化首次运行欢迎页](../../.agents/notes/implemented/feature/2026-07-30-versioned-tui-first-run-welcome.md);只有 Enter 会创建该版本的标记,Escape、资源释放或进程退出仍保留展示资格。官方 DeepSeek 图标、响应式终端栅格图、所有 locale 共用的中文文案和通知版本均由静态本地文件持有;overlay 不会写入会话事件或模型上下文。
|
||||
- 注册裸 `/compact`:agent 空闲时,即使未达到自动压力,也会摘要有效的较早历史;该命令拒绝参数,并只在独立替换标记对持久化后报告成功。压缩(compaction)期间提交的提示词保留其队列身份,并在该检查点之后启动;注入的上下文仍保持可见。
|
||||
|
||||
`dsh meta` 是以本 harness checkout 为 workspace 的同一个 TUI,因此开发 dsh 自身无需 `cd`。它在环境确定之后才 chdir 到 checkout 根目录(从启动器的真实路径解析,与源码路径提示词段所指的根目录相同),因此环境优先级不变,而会话 cwd 与 HMR 监视根目录会一并移动。Meta 始终创建新会话,不接受默认界面的任何选项;恢复已持久化会话应使用普通的 `dsh --resume <id>`。
|
||||
|
||||
`dsh upgrade` 是默认 TUI 界面之上的引导式全新会话入口:它在调用目录中创建一个全新会话,并以内置 `dsh-upgrade` skill 播种其首轮,效果等同于用户手动键入 `/skill:<name>`。启动器将 skill 名称提供到启动上下文([`INITIAL_SKILL_KEY`](../../packages/ui/tui/README.md)),TUI 在聊天就绪后自动调用它。该命令除实验性门槛外不接受任何选项——`--config`、`-p`、`--resume` 都会明确报错——且仅在首次启动时播种,因此之后 `dsh --resume <id>` 恢复该会话时是普通 TUI 会话,不会重复注入。
|
||||
|
||||
`dsh --dump-config` 和 `dsh web --dump-config` 把合成后的配置树——已交付的基础配置、界面覆盖层,以及 `--config` 或个人覆盖层,恰好是该界面启动时组装的那些层——以 YAML 打印到 stdout 后退出,不启动任何东西;`--dump-default-config` 止步于界面覆盖层,因此对两份输出做 diff 就能精确看出用户层改了什么。每段连续的行之前都有一条 `# ==` 注释,标明该段来自哪个文件以及被哪些层修补过(例如 `# == base.cordis.yml, patched by tui.cordis.yml`),因此输出既展示来源,又仍是一份可加载的文档。合成通过 include 自己的补丁算法和 YAML 方言(`@cordisjs/plugin-include` 的 `applyEntryPatches`/`entryListSchema`)完成,因此 dump 不可能与实际启动漂移;`!!js` 表达式原样打印、不求值,目标行不存在的补丁会连同其所在层报到 stderr,与 Loader 启动时的警告一致。由启动器持有的启动上下文值(会话身份、CLI 标志补丁)是每次调用的事实,位于配置树之外,不会出现。dump 标志会拒绝仅用于启动的标志(`-p`、`--resume`、`--config-replace`)而不是静默忽略它们,`--dump-default-config` 不接受 `--config`。
|
||||
|
||||
Web 和无头界面启动 `base.cordis.yml` 与 `web.cordis.yml`,随后应用 `$DSH_HOME/config.yaml`;显式的 `--config <path>` 会替代该个人覆盖。除此之外,两者共享同一套组合:两者都会告知编码 agent 所用模型和会话工作目录,将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root <path>` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,选用首条消息模型标题,采用与 TUI 相同的有界暂时性模型请求重试策略,并挂载一个可丢弃的内存 SQLite 内容索引服务。Web 还会明确说明交互界面是 DeepSeek Harness Web GUI、当前 checkout 是自身源码位置,并在提示词及受管的 `$DSH_WEB_URL`/`$DSH_WEB_MODE` 中提供该进程的规范本地 URL 和模式;因此,「这个页面」等表述会指向该 GUI,但 agent 不会声称可以访问未显式提供的 DOM、路由或截图状态。在生产模式下,宿主会在下次请求时读取重新构建的前端 dist 和客户端 bundle,因此刷新现有 URL 即可更新该 GUI,无须替换其进程。`dsh web --dev` 会挂载客户端插件的 HMR(热模块替换)接收端,但要实现无刷新更新,还需在同一 checkout 中运行 `pnpm run dev:web`,以监视并重新构建插件 bundle;shell 和普通包(package)的更改仍需重新构建并刷新页面。直接使用裸 `apps/web` Vite 服务会在开始监听前失败,因为它无法注入 `window.__DSH_BOOT__`。索引服务在启动时处于 ACTIVE 状态,但其 `node:sqlite` 模块与数据库句柄分别要到首次内容搜索才会导入和打开。这样可使 Node 22 在尚未使用搜索时的启动输出不出现 SQLite 实验性警告;首次实际搜索仍可能发出运行时警告。每个服务实例独占自己的数据库,因此并行调用既不会共享不受支持的 SQLite 状态,也不会留下派生索引文件,首次搜索还会惰性对账实时日志与持久化日志。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。
|
||||
|
||||
共享组合把新建 TUI、Web 和无头会话的权限默认设为 `workspace-write` preset(`workspace-write` 文件模式加 `ask` 审批策略)。由沙箱强制约束的 bash 与文件系统修改只能写入会话工作区和平台临时根目录;读取、网络访问和进程可见性不受该策略约束。浏览器可以应答一次性审批请求,并提供 Access 选择器;TUI 提供 `/permission`,但没有审批请求应答者,因此自动请求更宽权限的重试会以拒绝方式关闭,直到用户主动更改会话 preset。`DSH_PERMISSION_MODE` 会更改进程回退值,而「通用」设置中已存储的「权限」值只适用于之后的会话,不会更改已打开的会话。
|
||||
|
||||
三个界面都会使用 `$DSH_HOME/config.yaml`;TUI 和 Web 实时应用有效编辑,而一次性无头运行只在启动时读取。已交付的配置树包含一个空的 `repository-plugins` 配置项,因此独立用户无需 SDK 项目或安装命令,只需配置即可添加已准备的 GitHub 插件:
|
||||
|
||||
```yaml
|
||||
- id: repository-plugins
|
||||
name: '@deepseek-ai/dsh-repository-plugin'
|
||||
config:
|
||||
repositories:
|
||||
- 'github:PolyArch/humanize#<commit>'
|
||||
```
|
||||
|
||||
仓库必须包含已准备的 `.dsh-plugin` 包;[仓库插件契约](../../packages/cordis/repository-plugin/README.md#standalone-app-configuration)说明创作方式、嵌套插件路径、不可变缓存、信任边界和失败语义。实时编辑失败时,最后一个可用树保持运行,并发出 Cordis 的 HMR(热模块替换)事件 `hmr/config-update-failed`。
|
||||
|
||||
已交付的 TUI 和 Web 组合会注册原生 DeepSeek 适配器,以及 pi-ai 的 OpenAI 和 Anthropic 提供方配置。凭据和端点覆盖来自启动分层环境中的提供方标准变量对:`DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`、`OPENAI_API_KEY` / `OPENAI_BASE_URL` 和 `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`。
|
||||
|
||||
每个界面也都只注册 `web_search` 这一个 Web 工具。搜索使用 DeepSeek 的 Anthropic 兼容 Messages 端点,每次调用都会解析同一个 `DEEPSEEK_API_KEY` 凭据引用,并接受独立的 `DEEPSEEK_SEARCH_BASE_URL` 端点覆盖;每次搜索都是一次辅助模型请求,会产生独立的延迟与 token 成本。`web_fetch` 仍处于禁用状态,组合也未挂载默认抓取提供方;需要任意页面抓取能力的部署必须通过覆盖层选择启用。部署决策及其安全边界见[默认 Web 搜索 Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-default-search.md)。
|
||||
|
||||
`DSH_TOOLS_MODE` 为整个 Web/无头进程选择工具呈现模式:`native`(未设置时的 schema 默认值)、`code`(仅含 `run_code` 的 Code Mode 线路)或 `both`;任何其他值都会经由 `dsh-tools` 配置 schema 在启动时明确报错。它是一个临时 seam——Loader 组合是静态的,因此该设置作用于整个进程——待 Web UI 负责逐会话工具模式选择后便会移除;TUI 界面会忽略该变量并固定为 `native`。
|
||||
|
||||
[`core-web.cordis.yml`](config/core-web.cordis.yml) 是一个可选启用的 `dsh web --config` 覆盖层:它保留已交付的 Web 宿主、浏览器、Workspace、持久化与权限组合,同时将默认的原生模型界面精简为以所有者为作用域的持久 `bash` 以及 `str_replace_editor`。PTY 后端和编辑器分别消费现有的 Web 沙箱与文件系统提供方。持久 shell 处于打开状态时,会阻止所属会话更改权限模式;因此,在较宽权限下创建的 shell 无法在降权后继续存活。`DSH_TOOLS_MODE` 仍控制由此得到的双工具注册表采用原生/Code Mode 呈现。
|
||||
|
||||
在源码 checkout 中,用以下命令启动这个精简 Web profile:
|
||||
原始 `dsh` 要求显式传入一份 patch 列表配置:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web --config apps/cli/config/core-web.cordis.yml
|
||||
dsh --config ./app.cordis.yml
|
||||
```
|
||||
|
||||
每个 `dsh` 界面——TUI、Web 与无头——都默认上报会话遥测(该行位于共享的 `base.cordis.yml`):每条会话日志事件以 OTLP/HTTP 日志记录的形式、按 10 秒批处理节奏流向 `https://harness-telemetry.deepseeksvc.com/v1/logs`。`DSH_TELEMETRY_OTLP_URL` 可将 exporter 指向其他 collector;将 `DSH_TELEMETRY_DISABLED` 设为**任意非空值**——包括 `0` 或 `false`——都会在该行加载前将其关停(隐私开关取「宁可误关、不可误开」)。该组合当前未挂载任何脱敏规则:导出记录即原始捕获副本,包含消息正文、工具参数与结果、以及会话工作目录路径。部署口径见 [web-telemetry-default-mount Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md)。
|
||||
指定文件会通过 Include 插件的 patch 算法,直接应用在 [`config/base.cordis.yml`](config/base.cordis.yml) 之上。它不是完整替换树,系统也不会添加个人 `$DSH_HOME/config.yaml` 或其他 surface overlay。base 有意不包含启动 agent(智能体)或交互入口;必需的 overlay 负责选择这些部署细节。相对配置路径以调用目录为基准解析。配置解析、schema 校验、模块解析或插件启动失败都会被报告,并以非零代码退出。SIGINT 和 SIGTERM 会在退出前 dispose(资源释放)已挂载的根上下文。
|
||||
|
||||
MCP 服务器不是交付默认值,因为默认值必须点名一台:`@deepseek-ai/dsh-mcp-client` 每一行只挂载一台服务器,并把它作为子进程 spawn,该进程不经 `ctx.bash`,因此也不受沙箱策略约束。该包是本 CLI 的运行时依赖,所以已安装的 `dsh` 无需源码检出即可从 `$DSH_HOME/config.yaml` 或 `--config` 覆盖层挂载你自己的服务器:
|
||||
patch 通过 `id` 定位 base 配置项,并替换该配置项的完整 `config` 值,而不是深度合并各个键。它也可以插入新配置项:
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: mcp-github
|
||||
name: '@deepseek-ai/dsh-mcp-client'
|
||||
config:
|
||||
serverName: github
|
||||
transport: stdio
|
||||
command: npx
|
||||
args: ['-y', '@modelcontextprotocol/server-github']
|
||||
env:
|
||||
GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN
|
||||
- id: agent-loop
|
||||
config:
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
模型随后会看到 `mcp__github__*`。Streamable HTTP 传输与完整字段表见 [mcp-client README](../../packages/mcp/mcp-client/README.md)。
|
||||
可以在不启动应用的情况下检查有效配置树:
|
||||
|
||||
## 安装(开发机)
|
||||
```sh
|
||||
dsh --dump-default-config
|
||||
dsh --config ./app.cordis.yml --dump-config
|
||||
```
|
||||
|
||||
将从源码运行的启动器符号链接到 PATH 上;它通过自身真实路径解析 checkout,因此代码更改会在下次启动时生效,无需构建:
|
||||
`--dump-default-config` 只打印随附 base。`--dump-config` 要求提供 `--config`,并打印带来源注释的 base 与 overlay。组合过程使用 `@cordisjs/plugin-include` 的 `applyEntryPatches` 和 `entryListSchema`;`!!js` 表达式保持未求值状态,未匹配的 patch 目标会报告到 stderr。
|
||||
|
||||
## Web 与 headless
|
||||
|
||||
`dsh web` 会启动 `base.cordis.yml` 加 [`config/web.cordis.yml`](config/web.cordis.yml),并在 `$DSH_HOME/config.yaml` 存在时继续应用该文件。`dsh web --config <path>` 会以显式 patch 列表替换个人层。`--host`、`--port`、`--workspace-root` 和可重复的 `--trusted-host` 值会转为 Web 宿主 patch;各自所属插件的 schema 会在启动时校验它们。`--dev` 会挂载客户端插件 HMR(热模块替换)接收器,要实现无需刷新的客户端 bundle 更新,还需单独运行 `pnpm run dev:web` watcher。
|
||||
|
||||
```sh
|
||||
dsh web
|
||||
dsh web --config ./web-profile.cordis.yml
|
||||
dsh web --dump-default-config
|
||||
dsh web --dump-config
|
||||
```
|
||||
|
||||
生产 Web 运行器需要已构建的包(package)与前端产物(`pnpm run build`)。它默认通过 `http://127.0.0.1:3080` 提供服务。绑定所有网络接口时,系统也会信任本机探测到的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任边界所接受的具名权威。
|
||||
|
||||
`dsh -p "task"` 使用相同的 base 与 Web 组合及启动时个人配置,在由操作系统分配的端口上启动 Web 宿主,运行一个全新的持久会话,打印最终答案后退出。它不接受 `--config` 或原始配置输出标志。
|
||||
|
||||
两种模式都以调用目录作为默认 workspace 根目录,加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,渲染预算为 65,536 字节,并使用内存 SQLite 会话内容索引。Web 会持续应用有效的个人配置编辑;headless 只在启动时读取该文件一次。层次优先级、凭据存储、实时更新失败行为与 `$DSH_HOME` 解析均由 [app-boot 个人配置契约](../../packages/ui/app-boot/README.md#personal-config) 统一定义。
|
||||
|
||||
新会话默认使用 `workspace-write` 权限 preset。Bash 和文件系统写操作受限于会话 workspace 与平台临时根目录;读取、网络访问与进程可见性不受限制。`DSH_PERMISSION_MODE` 会改变进程回退值。已存储的常规设置权限会影响之后的 Web 会话,不会更改已打开的会话。
|
||||
|
||||
`DSH_TOOLS_MODE` 为 Web/headless 进程选择 `native`、`code` 或 `both`;其他值会在启动时失败。[`config/core-web.cordis.yml`](config/core-web.cordis.yml) 是可选的 Web overlay,它在保留随附宿主、浏览器、workspace、持久化与权限组合的同时,将面向原生模型的工具缩减为持久 `bash` 和 `str_replace_editor`。
|
||||
|
||||
## 共享部署行为
|
||||
|
||||
base 会挂载原生 DeepSeek 适配器、设置与凭据提供方、稳定的 `web_search`、仓库插件支持与会话遥测。提供方凭据位于 `$DSH_HOME/.env` 或环境中,且仍可轮换,因为启动器绝不会把凭据文件提升进 `process.env`。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;除非 overlay 插入提供方并启用 `web_fetch`,否则后者处于禁用状态。
|
||||
|
||||
会话事件默认以 OTLP/HTTP 日志的形式流式发送。`DSH_TELEMETRY_OTLP_URL` 用于选择其他 collector。`DSH_TELEMETRY_DISABLED` 的任何非空值都会在启动前禁用遥测配置项。随附 base 没有遥测脱敏规则,因此导出记录可能包含消息文本、工具参数与结果,以及 workspace 路径;该部署决策由[遥测 Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md) 统一定义。
|
||||
|
||||
空的 `repository-plugins` 配置项允许 Web/headless 个人配置与原始 overlay 挂载已准备的不可变仓库插件 generation。详见[仓库插件契约](../../packages/cordis/repository-plugin/README.md#standalone-app-configuration)。CLI(命令行界面)还将 `@deepseek-ai/dsh-mcp-client` 作为 overlay 依赖发布,但默认不启用任何 MCP 服务器,因为每条服务器命令都是 agent 沙箱之外的受信任可执行代码。
|
||||
|
||||
## 源码启动器
|
||||
|
||||
将以源码运行的启动器链接到 PATH:
|
||||
|
||||
```sh
|
||||
ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh
|
||||
```
|
||||
|
||||
源码启动会通过 tsx 的 ESM-only hook(`node --import tsx/esm`)运行 `apps/cli/src/bin.ts`,由它转换 TypeScript 并将根 tsconfig 的 `paths` 映射投射到模块解析中。不使用 Node 原生 TypeScript 模式:Node 26 移除了 `--experimental-transform-types`,而 strip-only 模式无法接受源码图依赖的语法(vendor 中的参数属性、装饰器、运行时 enum/namespace)。CJS hook 保持关闭,因为源码图是纯 ESM,而 CJS 解析器会增加约 0.4s 启动耗时。`bin/dsh` 将 `TSX_TSCONFIG_PATH` 固定到 checkout 的根 tsconfig,使解析与 cwd 无关;node-compat 门禁 `dsh-source-launch-smoke` 会在每条受支持的 Node 版本线上运行这一精确启动向量。tsx 应用 `paths` 映射时不检查依赖声明,声明完整性由静态门禁保障:TUI 配置通过 `examples/package.json` 解析裸插件,Web/无头 `cordis.yml` 通过本包的 `dependencies` 解析;`verify-cordis-config` 要求每个已配置的裸插件均已声明,同时允许存在无关依赖。
|
||||
|
||||
`pnpm run dsh` 从仓库根目录运行同一入口并直接转发参数,例如 `pnpm run dsh -p "task"`。构建形式(`lib/bin.js`,通过 `pnpm run build`)会在普通 Node 下启动同一配置。
|
||||
它会通过自身实际路径解析该检出,并使用 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`。`TSX_TSCONFIG_PATH` 固定指向检出根目录,因此 workspace 包解析不受调用目录影响。`pnpm run dsh` 使用同一入口并转发参数。构建后的形式是执行 `pnpm run build` 后的 `apps/cli/lib/bin.js`。
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="#4D6BFE"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,149 +1,149 @@
|
||||
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
|
||||
Run `pnpm run gen-doc-graphs` to regenerate. -->
|
||||
|
||||
# TUI Agent App Composition
|
||||
# DSH Base Composition
|
||||
|
||||
The TUI surface combines the shared CLI base with its surface overlay and full-screen terminal package.
|
||||
The raw CLI applies one required caller-selected patch list over this shared base; Web and headless apply their own shipped overlays.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
cfg["apps/cli/config<br/>cordis.yml"]
|
||||
plugin_tui_timer["timer<br/>@cordisjs/plugin-timer"]
|
||||
cfg --> plugin_tui_timer
|
||||
plugin_tui_hmr["hmr<br/>@cordisjs/plugin-hmr"]
|
||||
cfg --> plugin_tui_hmr
|
||||
plugin_tui_repository_plugins["repository-plugins<br/>@deepseek-ai/dsh-repository-plugin"]
|
||||
cfg --> plugin_tui_repository_plugins
|
||||
plugin_tui_llm["llm<br/>@deepseek-ai/dsh-llm"]
|
||||
cfg --> plugin_tui_llm
|
||||
plugin_tui_session["session<br/>@deepseek-ai/dsh-session"]
|
||||
cfg --> plugin_tui_session
|
||||
plugin_tui_session_title["session-title<br/>@deepseek-ai/dsh-session-title"]
|
||||
cfg --> plugin_tui_session_title
|
||||
plugin_tui_session_title_llm["session-title-llm<br/>@deepseek-ai/dsh-session-title-first-message-llm"]
|
||||
cfg --> plugin_tui_session_title_llm
|
||||
plugin_tui_user_interaction["user-interaction<br/>@deepseek-ai/dsh-user-interaction"]
|
||||
cfg --> plugin_tui_user_interaction
|
||||
plugin_tui_agent["agent<br/>@deepseek-ai/dsh-agent"]
|
||||
cfg --> plugin_tui_agent
|
||||
plugin_tui_tasks["tasks<br/>@deepseek-ai/dsh-tasks-local"]
|
||||
cfg --> plugin_tui_tasks
|
||||
plugin_tui_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
|
||||
cfg --> plugin_tui_llm_retry
|
||||
plugin_tui_settings["settings<br/>@deepseek-ai/dsh-settings-local"]
|
||||
cfg --> plugin_tui_settings
|
||||
plugin_tui_credentials["credentials<br/>@deepseek-ai/dsh-credentials-local"]
|
||||
cfg --> plugin_tui_credentials
|
||||
plugin_tui_llm_pi_ai["llm-pi-ai<br/>@deepseek-ai/dsh-llm-pi-ai"]
|
||||
cfg --> plugin_tui_llm_pi_ai
|
||||
plugin_tui_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
|
||||
cfg --> plugin_tui_session_persistence_jsonl
|
||||
plugin_tui_session_query_sqlite["session-query-sqlite<br/>@deepseek-ai/dsh-session-query-sqlite"]
|
||||
cfg --> plugin_tui_session_query_sqlite
|
||||
plugin_tui_telemetry_otel["telemetry-otel<br/>@deepseek-ai/dsh-session-telemetry-otel"]
|
||||
cfg --> plugin_tui_telemetry_otel
|
||||
plugin_tui_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
|
||||
cfg --> plugin_tui_subprocess
|
||||
plugin_tui_sandbox["sandbox<br/>@deepseek-ai/dsh-sandbox-local"]
|
||||
cfg --> plugin_tui_sandbox
|
||||
plugin_tui_sandbox_policy["sandbox-policy<br/>@deepseek-ai/dsh-sandbox-policy"]
|
||||
cfg --> plugin_tui_sandbox_policy
|
||||
plugin_tui_bash_sandbox["bash-sandbox<br/>@deepseek-ai/dsh-bash-sandbox"]
|
||||
cfg --> plugin_tui_bash_sandbox
|
||||
plugin_tui_approval["approval<br/>@deepseek-ai/dsh-user-approval"]
|
||||
cfg --> plugin_tui_approval
|
||||
plugin_tui_permission["permission<br/>@deepseek-ai/dsh-permission"]
|
||||
cfg --> plugin_tui_permission
|
||||
plugin_tui_tool_bash["tool-bash<br/>@deepseek-ai/dsh-tool-bash"]
|
||||
cfg --> plugin_tui_tool_bash
|
||||
plugin_tui_tool_tasks["tool-tasks<br/>@deepseek-ai/dsh-tool-tasks"]
|
||||
cfg --> plugin_tui_tool_tasks
|
||||
plugin_tui_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"]
|
||||
cfg --> plugin_tui_fs_policy
|
||||
plugin_tui_tool_fs["tool-fs<br/>@deepseek-ai/dsh-tool-fs"]
|
||||
cfg --> plugin_tui_tool_fs
|
||||
plugin_tui_tool_fs_search["tool-fs-search<br/>@deepseek-ai/dsh-tool-fs-search"]
|
||||
cfg --> plugin_tui_tool_fs_search
|
||||
plugin_tui_workspace_context["workspace-context<br/>@deepseek-ai/dsh-workspace-context"]
|
||||
cfg --> plugin_tui_workspace_context
|
||||
plugin_tui_skill["skill<br/>@deepseek-ai/dsh-skill"]
|
||||
cfg --> plugin_tui_skill
|
||||
plugin_tui_skill_local["skill-local<br/>@deepseek-ai/dsh-skill-local"]
|
||||
cfg --> plugin_tui_skill_local
|
||||
plugin_tui_tool_skill["tool-skill<br/>@deepseek-ai/dsh-tool-skill"]
|
||||
cfg --> plugin_tui_tool_skill
|
||||
plugin_tui_commands["commands<br/>@deepseek-ai/dsh-commands"]
|
||||
cfg --> plugin_tui_commands
|
||||
plugin_tui_goal["goal<br/>@deepseek-ai/dsh-goal"]
|
||||
cfg --> plugin_tui_goal
|
||||
plugin_tui_goal_session["goal-session<br/>@deepseek-ai/dsh-goal-session"]
|
||||
cfg --> plugin_tui_goal_session
|
||||
plugin_tui_command_goal["command-goal<br/>@deepseek-ai/dsh-command-goal"]
|
||||
cfg --> plugin_tui_command_goal
|
||||
plugin_tui_plan_mode["plan-mode<br/>@deepseek-ai/dsh-plan-mode"]
|
||||
cfg --> plugin_tui_plan_mode
|
||||
plugin_tui_token_meter["token-meter<br/>@deepseek-ai/dsh-token-meter"]
|
||||
cfg --> plugin_tui_token_meter
|
||||
plugin_tui_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"]
|
||||
cfg --> plugin_tui_compact_basic
|
||||
plugin_tui_command_compact["command-compact<br/>@deepseek-ai/dsh-command-compact"]
|
||||
cfg --> plugin_tui_command_compact
|
||||
plugin_tui_subagent["subagent<br/>@deepseek-ai/dsh-subagent"]
|
||||
cfg --> plugin_tui_subagent
|
||||
plugin_tui_subagent_spawn["subagent-spawn<br/>@deepseek-ai/dsh-subagent-spawn"]
|
||||
cfg --> plugin_tui_subagent_spawn
|
||||
plugin_tui_subagent_fork["subagent-fork<br/>@deepseek-ai/dsh-subagent-fork"]
|
||||
cfg --> plugin_tui_subagent_fork
|
||||
plugin_tui_tool_subagent_control["tool-subagent-control<br/>@deepseek-ai/dsh-tool-subagent-control"]
|
||||
cfg --> plugin_tui_tool_subagent_control
|
||||
plugin_tui_tool_subagent_list_agents["tool-subagent-list-agents<br/>@deepseek-ai/dsh-tool-subagent-control/list-agents"]
|
||||
cfg --> plugin_tui_tool_subagent_list_agents
|
||||
plugin_tui_tool_subagent["tool-subagent<br/>@deepseek-ai/dsh-tool-subagent"]
|
||||
cfg --> plugin_tui_tool_subagent
|
||||
plugin_tui_tool_subagent_fork["tool-subagent-fork<br/>@deepseek-ai/dsh-tool-subagent"]
|
||||
cfg --> plugin_tui_tool_subagent_fork
|
||||
plugin_tui_tool_subagent_report["tool-subagent-report<br/>@deepseek-ai/dsh-tool-subagent-report"]
|
||||
cfg --> plugin_tui_tool_subagent_report
|
||||
plugin_tui_workflow_workerthread["workflow-workerthread<br/>@deepseek-ai/dsh-workflow-workerthread"]
|
||||
cfg --> plugin_tui_workflow_workerthread
|
||||
plugin_tui_tool_workflow["tool-workflow<br/>@deepseek-ai/dsh-tool-workflow"]
|
||||
cfg --> plugin_tui_tool_workflow
|
||||
plugin_tui_timeout_policy["timeout-policy<br/>@deepseek-ai/dsh-timeout-policy"]
|
||||
cfg --> plugin_tui_timeout_policy
|
||||
plugin_tui_spill_local["spill-local<br/>@deepseek-ai/dsh-spill-local"]
|
||||
cfg --> plugin_tui_spill_local
|
||||
plugin_tui_spill_policy["spill-policy<br/>@deepseek-ai/dsh-spill-policy"]
|
||||
cfg --> plugin_tui_spill_policy
|
||||
plugin_tui_session_checkpoint_policy["session-checkpoint-policy<br/>@deepseek-ai/dsh-session-checkpoint-policy"]
|
||||
cfg --> plugin_tui_session_checkpoint_policy
|
||||
plugin_tui_tool_result_prune["tool-result-prune<br/>@deepseek-ai/dsh-compact-tool-result-prune"]
|
||||
cfg --> plugin_tui_tool_result_prune
|
||||
plugin_tui_tool_todo["tool-todo<br/>@deepseek-ai/dsh-tool-todo"]
|
||||
cfg --> plugin_tui_tool_todo
|
||||
plugin_tui_tool_goal["tool-goal<br/>@deepseek-ai/dsh-tool-goal"]
|
||||
cfg --> plugin_tui_tool_goal
|
||||
plugin_tui_tool_ralph["tool-ralph<br/>@deepseek-ai/dsh-tool-ralph"]
|
||||
cfg --> plugin_tui_tool_ralph
|
||||
plugin_tui_tool_str_replace_editor["tool-str-replace-editor<br/>@deepseek-ai/dsh-tool-str-replace-editor"]
|
||||
cfg --> plugin_tui_tool_str_replace_editor
|
||||
plugin_tui_repeat_tool_guard["repeat-tool-guard<br/>@deepseek-ai/dsh-repeat-tool-guard"]
|
||||
cfg --> plugin_tui_repeat_tool_guard
|
||||
plugin_tui_web["web<br/>@deepseek-ai/dsh-web"]
|
||||
cfg --> plugin_tui_web
|
||||
plugin_tui_web_search_deepseek["web-search-deepseek<br/>@deepseek-ai/dsh-web-search-deepseek"]
|
||||
cfg --> plugin_tui_web_search_deepseek
|
||||
plugin_tui_tool_web["tool-web<br/>@deepseek-ai/dsh-tool-web"]
|
||||
cfg --> plugin_tui_tool_web
|
||||
plugin_tui_tools["tools<br/>@deepseek-ai/dsh-tools"]
|
||||
cfg --> plugin_tui_tools
|
||||
plugin_tui_system_prompt["system-prompt<br/>@deepseek-ai/dsh-system-prompt"]
|
||||
cfg --> plugin_tui_system_prompt
|
||||
plugin_tui_agent_loop["agent-loop<br/>@deepseek-ai/dsh-agent-loop"]
|
||||
cfg --> plugin_tui_agent_loop
|
||||
plugin_tui_fs_sandbox["fs-sandbox<br/>@deepseek-ai/dsh-fs-sandbox"]
|
||||
cfg --> plugin_tui_fs_sandbox
|
||||
plugin_tui_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
|
||||
cfg --> plugin_tui_llm_deepseek
|
||||
cfg["apps/cli/config/base.cordis.yml<br/>cordis.yml"]
|
||||
plugin_dsh_base_timer["timer<br/>@cordisjs/plugin-timer"]
|
||||
cfg --> plugin_dsh_base_timer
|
||||
plugin_dsh_base_hmr["hmr<br/>@cordisjs/plugin-hmr"]
|
||||
cfg --> plugin_dsh_base_hmr
|
||||
plugin_dsh_base_repository_plugins["repository-plugins<br/>@deepseek-ai/dsh-repository-plugin"]
|
||||
cfg --> plugin_dsh_base_repository_plugins
|
||||
plugin_dsh_base_llm["llm<br/>@deepseek-ai/dsh-llm"]
|
||||
cfg --> plugin_dsh_base_llm
|
||||
plugin_dsh_base_session["session<br/>@deepseek-ai/dsh-session"]
|
||||
cfg --> plugin_dsh_base_session
|
||||
plugin_dsh_base_session_title["session-title<br/>@deepseek-ai/dsh-session-title"]
|
||||
cfg --> plugin_dsh_base_session_title
|
||||
plugin_dsh_base_session_title_llm["session-title-llm<br/>@deepseek-ai/dsh-session-title-first-message-llm"]
|
||||
cfg --> plugin_dsh_base_session_title_llm
|
||||
plugin_dsh_base_user_interaction["user-interaction<br/>@deepseek-ai/dsh-user-interaction"]
|
||||
cfg --> plugin_dsh_base_user_interaction
|
||||
plugin_dsh_base_agent["agent<br/>@deepseek-ai/dsh-agent"]
|
||||
cfg --> plugin_dsh_base_agent
|
||||
plugin_dsh_base_tasks["tasks<br/>@deepseek-ai/dsh-tasks-local"]
|
||||
cfg --> plugin_dsh_base_tasks
|
||||
plugin_dsh_base_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
|
||||
cfg --> plugin_dsh_base_llm_retry
|
||||
plugin_dsh_base_settings["settings<br/>@deepseek-ai/dsh-settings-local"]
|
||||
cfg --> plugin_dsh_base_settings
|
||||
plugin_dsh_base_credentials["credentials<br/>@deepseek-ai/dsh-credentials-local"]
|
||||
cfg --> plugin_dsh_base_credentials
|
||||
plugin_dsh_base_llm_pi_ai["llm-pi-ai<br/>@deepseek-ai/dsh-llm-pi-ai"]
|
||||
cfg --> plugin_dsh_base_llm_pi_ai
|
||||
plugin_dsh_base_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
|
||||
cfg --> plugin_dsh_base_session_persistence_jsonl
|
||||
plugin_dsh_base_session_query_sqlite["session-query-sqlite<br/>@deepseek-ai/dsh-session-query-sqlite"]
|
||||
cfg --> plugin_dsh_base_session_query_sqlite
|
||||
plugin_dsh_base_telemetry_otel["telemetry-otel<br/>@deepseek-ai/dsh-session-telemetry-otel"]
|
||||
cfg --> plugin_dsh_base_telemetry_otel
|
||||
plugin_dsh_base_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
|
||||
cfg --> plugin_dsh_base_subprocess
|
||||
plugin_dsh_base_sandbox["sandbox<br/>@deepseek-ai/dsh-sandbox-local"]
|
||||
cfg --> plugin_dsh_base_sandbox
|
||||
plugin_dsh_base_sandbox_policy["sandbox-policy<br/>@deepseek-ai/dsh-sandbox-policy"]
|
||||
cfg --> plugin_dsh_base_sandbox_policy
|
||||
plugin_dsh_base_bash_sandbox["bash-sandbox<br/>@deepseek-ai/dsh-bash-sandbox"]
|
||||
cfg --> plugin_dsh_base_bash_sandbox
|
||||
plugin_dsh_base_approval["approval<br/>@deepseek-ai/dsh-user-approval"]
|
||||
cfg --> plugin_dsh_base_approval
|
||||
plugin_dsh_base_permission["permission<br/>@deepseek-ai/dsh-permission"]
|
||||
cfg --> plugin_dsh_base_permission
|
||||
plugin_dsh_base_tool_bash["tool-bash<br/>@deepseek-ai/dsh-tool-bash"]
|
||||
cfg --> plugin_dsh_base_tool_bash
|
||||
plugin_dsh_base_tool_tasks["tool-tasks<br/>@deepseek-ai/dsh-tool-tasks"]
|
||||
cfg --> plugin_dsh_base_tool_tasks
|
||||
plugin_dsh_base_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"]
|
||||
cfg --> plugin_dsh_base_fs_policy
|
||||
plugin_dsh_base_tool_fs["tool-fs<br/>@deepseek-ai/dsh-tool-fs"]
|
||||
cfg --> plugin_dsh_base_tool_fs
|
||||
plugin_dsh_base_tool_fs_search["tool-fs-search<br/>@deepseek-ai/dsh-tool-fs-search"]
|
||||
cfg --> plugin_dsh_base_tool_fs_search
|
||||
plugin_dsh_base_workspace_context["workspace-context<br/>@deepseek-ai/dsh-workspace-context"]
|
||||
cfg --> plugin_dsh_base_workspace_context
|
||||
plugin_dsh_base_skill["skill<br/>@deepseek-ai/dsh-skill"]
|
||||
cfg --> plugin_dsh_base_skill
|
||||
plugin_dsh_base_skill_local["skill-local<br/>@deepseek-ai/dsh-skill-local"]
|
||||
cfg --> plugin_dsh_base_skill_local
|
||||
plugin_dsh_base_tool_skill["tool-skill<br/>@deepseek-ai/dsh-tool-skill"]
|
||||
cfg --> plugin_dsh_base_tool_skill
|
||||
plugin_dsh_base_commands["commands<br/>@deepseek-ai/dsh-commands"]
|
||||
cfg --> plugin_dsh_base_commands
|
||||
plugin_dsh_base_goal["goal<br/>@deepseek-ai/dsh-goal"]
|
||||
cfg --> plugin_dsh_base_goal
|
||||
plugin_dsh_base_goal_session["goal-session<br/>@deepseek-ai/dsh-goal-session"]
|
||||
cfg --> plugin_dsh_base_goal_session
|
||||
plugin_dsh_base_command_goal["command-goal<br/>@deepseek-ai/dsh-command-goal"]
|
||||
cfg --> plugin_dsh_base_command_goal
|
||||
plugin_dsh_base_plan_mode["plan-mode<br/>@deepseek-ai/dsh-plan-mode"]
|
||||
cfg --> plugin_dsh_base_plan_mode
|
||||
plugin_dsh_base_token_meter["token-meter<br/>@deepseek-ai/dsh-token-meter"]
|
||||
cfg --> plugin_dsh_base_token_meter
|
||||
plugin_dsh_base_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"]
|
||||
cfg --> plugin_dsh_base_compact_basic
|
||||
plugin_dsh_base_command_compact["command-compact<br/>@deepseek-ai/dsh-command-compact"]
|
||||
cfg --> plugin_dsh_base_command_compact
|
||||
plugin_dsh_base_subagent["subagent<br/>@deepseek-ai/dsh-subagent"]
|
||||
cfg --> plugin_dsh_base_subagent
|
||||
plugin_dsh_base_subagent_spawn["subagent-spawn<br/>@deepseek-ai/dsh-subagent-spawn"]
|
||||
cfg --> plugin_dsh_base_subagent_spawn
|
||||
plugin_dsh_base_subagent_fork["subagent-fork<br/>@deepseek-ai/dsh-subagent-fork"]
|
||||
cfg --> plugin_dsh_base_subagent_fork
|
||||
plugin_dsh_base_tool_subagent_control["tool-subagent-control<br/>@deepseek-ai/dsh-tool-subagent-control"]
|
||||
cfg --> plugin_dsh_base_tool_subagent_control
|
||||
plugin_dsh_base_tool_subagent_list_agents["tool-subagent-list-agents<br/>@deepseek-ai/dsh-tool-subagent-control/list-agents"]
|
||||
cfg --> plugin_dsh_base_tool_subagent_list_agents
|
||||
plugin_dsh_base_tool_subagent["tool-subagent<br/>@deepseek-ai/dsh-tool-subagent"]
|
||||
cfg --> plugin_dsh_base_tool_subagent
|
||||
plugin_dsh_base_tool_subagent_fork["tool-subagent-fork<br/>@deepseek-ai/dsh-tool-subagent"]
|
||||
cfg --> plugin_dsh_base_tool_subagent_fork
|
||||
plugin_dsh_base_tool_subagent_report["tool-subagent-report<br/>@deepseek-ai/dsh-tool-subagent-report"]
|
||||
cfg --> plugin_dsh_base_tool_subagent_report
|
||||
plugin_dsh_base_workflow_workerthread["workflow-workerthread<br/>@deepseek-ai/dsh-workflow-workerthread"]
|
||||
cfg --> plugin_dsh_base_workflow_workerthread
|
||||
plugin_dsh_base_tool_workflow["tool-workflow<br/>@deepseek-ai/dsh-tool-workflow"]
|
||||
cfg --> plugin_dsh_base_tool_workflow
|
||||
plugin_dsh_base_timeout_policy["timeout-policy<br/>@deepseek-ai/dsh-timeout-policy"]
|
||||
cfg --> plugin_dsh_base_timeout_policy
|
||||
plugin_dsh_base_spill_local["spill-local<br/>@deepseek-ai/dsh-spill-local"]
|
||||
cfg --> plugin_dsh_base_spill_local
|
||||
plugin_dsh_base_spill_policy["spill-policy<br/>@deepseek-ai/dsh-spill-policy"]
|
||||
cfg --> plugin_dsh_base_spill_policy
|
||||
plugin_dsh_base_session_checkpoint_policy["session-checkpoint-policy<br/>@deepseek-ai/dsh-session-checkpoint-policy"]
|
||||
cfg --> plugin_dsh_base_session_checkpoint_policy
|
||||
plugin_dsh_base_tool_result_prune["tool-result-prune<br/>@deepseek-ai/dsh-compact-tool-result-prune"]
|
||||
cfg --> plugin_dsh_base_tool_result_prune
|
||||
plugin_dsh_base_tool_todo["tool-todo<br/>@deepseek-ai/dsh-tool-todo"]
|
||||
cfg --> plugin_dsh_base_tool_todo
|
||||
plugin_dsh_base_tool_goal["tool-goal<br/>@deepseek-ai/dsh-tool-goal"]
|
||||
cfg --> plugin_dsh_base_tool_goal
|
||||
plugin_dsh_base_tool_ralph["tool-ralph<br/>@deepseek-ai/dsh-tool-ralph"]
|
||||
cfg --> plugin_dsh_base_tool_ralph
|
||||
plugin_dsh_base_tool_str_replace_editor["tool-str-replace-editor<br/>@deepseek-ai/dsh-tool-str-replace-editor"]
|
||||
cfg --> plugin_dsh_base_tool_str_replace_editor
|
||||
plugin_dsh_base_repeat_tool_guard["repeat-tool-guard<br/>@deepseek-ai/dsh-repeat-tool-guard"]
|
||||
cfg --> plugin_dsh_base_repeat_tool_guard
|
||||
plugin_dsh_base_web["web<br/>@deepseek-ai/dsh-web"]
|
||||
cfg --> plugin_dsh_base_web
|
||||
plugin_dsh_base_web_search_deepseek["web-search-deepseek<br/>@deepseek-ai/dsh-web-search-deepseek"]
|
||||
cfg --> plugin_dsh_base_web_search_deepseek
|
||||
plugin_dsh_base_tool_web["tool-web<br/>@deepseek-ai/dsh-tool-web"]
|
||||
cfg --> plugin_dsh_base_tool_web
|
||||
plugin_dsh_base_tools["tools<br/>@deepseek-ai/dsh-tools"]
|
||||
cfg --> plugin_dsh_base_tools
|
||||
plugin_dsh_base_system_prompt["system-prompt<br/>@deepseek-ai/dsh-system-prompt"]
|
||||
cfg --> plugin_dsh_base_system_prompt
|
||||
plugin_dsh_base_agent_loop["agent-loop<br/>@deepseek-ai/dsh-agent-loop"]
|
||||
cfg --> plugin_dsh_base_agent_loop
|
||||
plugin_dsh_base_fs_sandbox["fs-sandbox<br/>@deepseek-ai/dsh-fs-sandbox"]
|
||||
cfg --> plugin_dsh_base_fs_sandbox
|
||||
plugin_dsh_base_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
|
||||
cfg --> plugin_dsh_base_llm_deepseek
|
||||
```
|
||||
|
||||
| Plugin id | Package / module |
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# The shared `dsh` core: every row both the TUI (`tui.cordis.yml`) and the web
|
||||
# surface (`web.cordis.yml`) mount identically. Neither surface includes the
|
||||
# other — each is a patch list applied over THIS file at one include level, so a
|
||||
# surface overlay, a `--config` overlay, and the personal `~/.dsh/config.yaml`
|
||||
# all address these rows by id. Patch lists stack in that order, last write
|
||||
# winning per row.
|
||||
# The shared `dsh` core. Raw `dsh --config <path>` applies its required patch
|
||||
# list directly over this file. Web and headless apply their shipped overlay,
|
||||
# followed by an explicit or personal user layer. Every layer addresses these
|
||||
# rows by id at one include level, with the last write winning per row.
|
||||
#
|
||||
# A patch replaces the targeted row's whole `config` rather than merging into
|
||||
# it, so a row whose value differs per surface does NOT live here: it belongs to
|
||||
# it, so a row whose value differs by mode does NOT live here: it belongs to
|
||||
# each overlay, keeping any single row down to one overlay layer plus the user's.
|
||||
# Rows with surface-specific values appear below only with shared plugin identity
|
||||
# and neutral defaults; each overlay restates the complete surface configuration.
|
||||
# Mode-specific rows appear below only with shared plugin identity and neutral
|
||||
# defaults; each overlay restates its complete configuration.
|
||||
#
|
||||
# Row order carries no load semantics (activation is service-availability
|
||||
# driven); the grouping is for readers.
|
||||
@@ -93,15 +91,15 @@
|
||||
config:
|
||||
root: !!js dshHomePath('sessions')
|
||||
|
||||
# TUI consumes this shared session capability. Its launcher supplies a unique
|
||||
# process-local path; other surfaces repoint or disable the row in their
|
||||
# overlay (web patches it to an ephemeral in-memory index).
|
||||
# Raw configs can supply a process-local path or disable this shared session
|
||||
# capability. The neutral default is process-local and opens only when used.
|
||||
- id: session-query-sqlite
|
||||
name: '@deepseek-ai/dsh-session-query-sqlite'
|
||||
config:
|
||||
path: !!js launcherSessionQueryPath ?? './.sessions/session-query.db'
|
||||
path: ':memory:'
|
||||
openAt: first-search
|
||||
|
||||
# Session telemetry, on for every dsh surface: mirrors every session-log
|
||||
# Session telemetry, on for every dsh mode: mirrors every session-log
|
||||
# event (assistant/chunk projected to first-of-step) plus ops markers onto
|
||||
# OTLP/HTTP log records, streaming on the batch processor's cadence
|
||||
# (10s/batch here) — not at exit; a crash loses at most the last unexported
|
||||
@@ -119,9 +117,8 @@
|
||||
# disables the SDK's 5-try backoff), maxExportBatchSize == maxQueueSize
|
||||
# (both explicit) makes the drain a single batch, and exportTimeoutMillis
|
||||
# is the processor's own cap on that one export cycle — the second bound
|
||||
# when the exporter's clock alone does not fire. Every surface's exit path
|
||||
# drains it: web/headless dispose on SIGINT/SIGTERM, and the TUI's normal
|
||||
# exit and /resume handoff both dispose the root.
|
||||
# when the exporter's clock alone does not fire. Every CLI exit path drains it
|
||||
# by disposing the root on SIGINT/SIGTERM.
|
||||
- id: telemetry-otel
|
||||
name: '@deepseek-ai/dsh-session-telemetry-otel'
|
||||
config:
|
||||
@@ -138,7 +135,7 @@
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
# Every shipped product surface starts with the same file-effect boundary.
|
||||
# Every shipped CLI mode starts with the same file-effect boundary.
|
||||
# The environment remains an explicit deployment override; otherwise fresh
|
||||
# sessions pin workspace-write + ask through the permission service below.
|
||||
- id: sandbox
|
||||
@@ -342,7 +339,7 @@
|
||||
thresholds: [3, 5, 8]
|
||||
argumentsPreviewChars: 500
|
||||
|
||||
# Every surface enables the stable web_search model surface. DeepSeek search
|
||||
# Every mode enables the stable web_search model surface. DeepSeek search
|
||||
# resolves the same DEEPSEEK_API_KEY credential the Models page manages for
|
||||
# chat, at each search; its Messages endpoint is separate from the
|
||||
# chat-completions endpoint, so it takes its own base-URL override. Fetch stays
|
||||
@@ -367,35 +364,35 @@
|
||||
fetch: false
|
||||
searchTimeoutMs: 60000
|
||||
|
||||
# ── rows every surface mounts, whose values each overlay states ──────────────
|
||||
# ── rows every mode mounts, whose values each overlay may state ──────────────
|
||||
|
||||
# The tool registry. Presentation mode is a surface choice, so each overlay
|
||||
# states it; omitting it here keeps the schema default (native).
|
||||
# The tool registry. Presentation mode is a deployment choice; omitting it here
|
||||
# keeps the schema default (native).
|
||||
- id: tools
|
||||
name: '@deepseek-ai/dsh-tools'
|
||||
|
||||
# The deployment persona is a surface choice; plan-mode and tool plugins own
|
||||
# The deployment persona is a deployment choice; plan-mode and tool plugins own
|
||||
# their own prompt sections.
|
||||
- id: system-prompt
|
||||
name: '@deepseek-ai/dsh-system-prompt'
|
||||
config:
|
||||
persona: ''
|
||||
|
||||
# Agents created at startup. The TUI pre-creates `main`; the web surface creates
|
||||
# sessions on client request, so its overlay keeps this empty.
|
||||
# Agents created at startup. The base stays empty; raw overlays may create
|
||||
# agents, while Web creates sessions on client request.
|
||||
- id: agent-loop
|
||||
name: '@deepseek-ai/dsh-agent-loop'
|
||||
config:
|
||||
agents: []
|
||||
|
||||
# The sandboxed filesystem provider. `cwd` defaults to `process.cwd()`; the TUI
|
||||
# states it explicitly because that value is also the session workspace.
|
||||
# The sandboxed filesystem provider. `cwd` defaults to `process.cwd()`; an
|
||||
# overlay can pin another workspace.
|
||||
- id: fs-sandbox
|
||||
name: '@deepseek-ai/dsh-fs-sandbox'
|
||||
|
||||
# The native DeepSeek adapter. No key or endpoint is inlined: both resolve per
|
||||
# request from the `llm-deepseek:` settings section over this entry, with the
|
||||
# key coming from the credential store below. Thinking defaults are a surface
|
||||
# key coming from the credential store below. Thinking defaults are a deployment
|
||||
# choice.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
# `dsh` (the default surface) — the full-screen TUI, as a patch list over
|
||||
# `base.cordis.yml`. The launcher includes the base and applies this file, then
|
||||
# any `--config` overlay, then the personal `~/.dsh/config.yaml`, as sibling
|
||||
# patch lists at ONE include level: patches never cross an include boundary, so
|
||||
# stacking overlays as nested includes would silently stop reaching base rows.
|
||||
#
|
||||
# A patch replaces the targeted row's whole `config`, so each row below restates
|
||||
# every key it owns. A patch whose `id` matches no row is skipped with a Loader
|
||||
# warning, which is deliberate: one personal overlay is shared across surfaces,
|
||||
# so a row that exists only under `web` must not fail the TUI's boot.
|
||||
#
|
||||
# The launcher owns session identity and the exit line, and provides both on the
|
||||
# boot context rather than through config, so no key here — and no overlay
|
||||
# replacing one — can drop `--resume`.
|
||||
|
||||
# ── surface-specific values the base deliberately omits ─────────────────────
|
||||
|
||||
# `main` is the agent the TUI drives. `provider`/`model` are the route `dsh
|
||||
# login` rewrites and a personal overlay repoints; `cwd` anchors the session to
|
||||
# the invoking directory, which is also what scopes `/resume` to this workspace.
|
||||
- id: agent-loop
|
||||
config:
|
||||
agents:
|
||||
- id: main
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-pro
|
||||
cwd: !!js process.cwd()
|
||||
|
||||
# Keep the persona to identity and behavior; tool plugins own tool guidance.
|
||||
# The loop resolves {{model}} from this agent's configuration.
|
||||
- id: system-prompt
|
||||
config:
|
||||
persona: |
|
||||
You are a coding agent powered by the {{model}} model.
|
||||
|
||||
Verify your work by running the code or tests. Keep answers brief and
|
||||
factual.
|
||||
|
||||
# Shipped default: full thinking at max effort on every request. Exact-model
|
||||
# resolution materializes request defaults before the request header is logged.
|
||||
- id: llm-deepseek
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||
thinking: enabled
|
||||
reasoningEffort: max
|
||||
|
||||
# This single-session app resolves relative paths from the process cwd.
|
||||
- id: fs-sandbox
|
||||
config:
|
||||
cwd: !!js process.cwd()
|
||||
|
||||
# The shipped TUI presents the native tool registry.
|
||||
- id: tools
|
||||
config:
|
||||
mode: native
|
||||
|
||||
# ── TUI-only rows ───────────────────────────────────────────────────────────
|
||||
|
||||
- insert:
|
||||
# The derived query index behind `/resume`. The launcher provides a unique
|
||||
# process-local path because this SQLite backend has one writer owner; the
|
||||
# project-local fallback applies when no launcher sets the typed slot.
|
||||
- id: session-reference
|
||||
name: '@deepseek-ai/dsh-session-reference'
|
||||
|
||||
# The projection registry plus its durable checkpoint cache (over the same
|
||||
# storage root the web surface uses): `/resume` reads titles from the
|
||||
# zero-I/O checkpoint row or a tail-only cold read instead of scanning
|
||||
# whole logs, and checkpoints written by either surface serve both.
|
||||
- id: session-projection
|
||||
name: '@deepseek-ai/dsh-session-projection'
|
||||
- id: storage
|
||||
name: '@deepseek-ai/dsh-storage'
|
||||
- id: storage-json
|
||||
name: '@deepseek-ai/dsh-storage-json'
|
||||
config:
|
||||
root: !!js dshHomePath('storages')
|
||||
- id: storage-domain
|
||||
name: '@deepseek-ai/dsh-storage-domain'
|
||||
config:
|
||||
backend: json
|
||||
- id: session-projection-cache
|
||||
name: '@deepseek-ai/dsh-session-projection-cache'
|
||||
config:
|
||||
writeEveryEvents: 200
|
||||
writeIntervalMs: 5000
|
||||
|
||||
# Terminal-multiplexer context, mounted only where a terminal exists.
|
||||
- id: tmux-context
|
||||
name: '@deepseek-ai/dsh-tmux-context'
|
||||
config:
|
||||
refreshIntervalMs: 900000
|
||||
|
||||
# The keyboard-backed provider behind ask_user_question and the plan-mode
|
||||
# review, and the front door it renders inside.
|
||||
- id: tui-prompt
|
||||
name: '@deepseek-ai/dsh-tui/prompt'
|
||||
|
||||
# The TUI renders exactly the agent the agent-loop row bound, so it reads the
|
||||
# same launcher-owned identity rather than restating one.
|
||||
- id: tui
|
||||
name: '@deepseek-ai/dsh-tui'
|
||||
config:
|
||||
sessionId: !!js configuredAgentIdentities?.main?.id ?? 'main'
|
||||
showReasoning: true
|
||||
maxToolOutputLines: 6
|
||||
|
||||
- id: tool-ask-user
|
||||
name: '@deepseek-ai/dsh-tool-ask-user'
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: interactive TUI, headless task, and browser UI surfaces",
|
||||
"description": "dsh CLI: explicit config overlays, headless tasks, and the browser UI",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -9,7 +9,6 @@
|
||||
},
|
||||
"files": [
|
||||
"lib/bin.js",
|
||||
"assets",
|
||||
"config",
|
||||
"src"
|
||||
],
|
||||
@@ -87,7 +86,6 @@
|
||||
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-query-sqlite": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-telemetry-otel": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-title": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-title-first-message-llm": "workspace:^",
|
||||
@@ -106,9 +104,7 @@
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tasks-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-tmux-context": "workspace:^",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-bash-persistent": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
|
||||
@@ -126,7 +122,6 @@
|
||||
"@deepseek-ai/dsh-tool-web": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-tui": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
"@deepseek-ai/dsh-web": "workspace:^",
|
||||
@@ -134,15 +129,12 @@
|
||||
"@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace-context": "workspace:^",
|
||||
"@earendil-works/pi-tui": "0.80.7",
|
||||
"commander": "^15.0.0",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"js-yaml": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-llm-mock-server": "workspace:^",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"execa": "^10.0.0",
|
||||
"node-pty": "1.1.0"
|
||||
"execa": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* AppCLIEntry — the pre-cordis boot glue the config-tree dsh surfaces share
|
||||
* (`dsh web` and `dsh -p`; the TUI composes dsh-app-boot directly).
|
||||
* (`dsh web` and `dsh -p`).
|
||||
* Everything here is what must exist before the Loader runs: the patch
|
||||
* composition over the shipped base and surface overlay (profile json + CLI
|
||||
* composition over the shipped base and Web overlay (profile json + CLI
|
||||
* flags + the resolved frontend dist), and the fail-loud activation audit after the tree
|
||||
* settles. The environment is what the bin already loaded (ambient plus the
|
||||
* invoking directory's `.env`); `$DSH_HOME/.env` belongs to the credential
|
||||
@@ -89,7 +89,7 @@ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: b
|
||||
/**
|
||||
* Whether a config file carries the telemetry row, parsed under the same
|
||||
* `!!js`-tolerant dialect the boot uses — the `hasRow` input for launchers
|
||||
* that compose their patch lists outside {@link AppCLIEntry} (the TUI).
|
||||
* that compose their patch lists outside {@link AppCLIEntry} (raw `dsh`).
|
||||
* @param file - absolute path of the config or overlay file.
|
||||
* @returns true when a top-level (or inserted) row has the telemetry id.
|
||||
*/
|
||||
|
||||
@@ -1,44 +1,26 @@
|
||||
/**
|
||||
* 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`, `upgrade`, and `web` are real subcommands; the experimental ones
|
||||
* (`meta`, `upgrade`) run only under the `--experimental` flag or
|
||||
* `DSH_EXPERIMENTAL=1`. 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.
|
||||
* Commander adapter for the `dsh` command-line entry. The default command
|
||||
* boots one required `--config` overlay over the shipped base; `-p` selects
|
||||
* the one-shot headless path and `web` selects the browser application.
|
||||
* Commander owns help, version, and parse errors.
|
||||
* @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.
|
||||
*/
|
||||
interface TuiInvocation {
|
||||
mode: 'tui'
|
||||
config?: string
|
||||
configReplace?: string
|
||||
resume?: string
|
||||
/** Boot a caller-selected overlay over the shipped base config. */
|
||||
interface ConfigInvocation {
|
||||
mode: 'config'
|
||||
config: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the composed config tree and exit, without booting: `--dump-config`
|
||||
* composes the shipped base, the surface overlay, and the `--config` or
|
||||
* personal overlay — exactly the layers that surface would boot;
|
||||
* `--dump-default-config` stops at the surface overlay (the shipped tree, no
|
||||
* user layer).
|
||||
*/
|
||||
/** Print a composed config tree and exit without booting. */
|
||||
interface DumpConfigInvocation {
|
||||
mode: 'dump-config'
|
||||
surface: 'tui' | 'web'
|
||||
/** Omit the `--config`/personal layer and print only the shipped composition. */
|
||||
surface: 'config' | 'web'
|
||||
/** Omit every caller or personal layer and print the shipped tree. */
|
||||
defaultOnly: boolean
|
||||
/** The `--config` overlay to compose instead of the personal one. */
|
||||
/** Explicit overlay to compose over the base or Web surface. */
|
||||
config?: string
|
||||
}
|
||||
|
||||
@@ -48,52 +30,24 @@ interface HeadlessInvocation {
|
||||
prompt: string
|
||||
}
|
||||
|
||||
/** Interactive fresh TUI over this harness checkout; accepts no default-surface options, only the experimental gate. */
|
||||
interface MetaInvocation {
|
||||
mode: 'meta'
|
||||
}
|
||||
|
||||
/**
|
||||
* Guided fresh-session entry: `dsh upgrade` seeds the first turn
|
||||
* with the `dsh-upgrade` skill. It always mints a
|
||||
* fresh session in the invoking directory and takes no options beyond the
|
||||
* experimental gate — `--resume`, `--config`, and `-p` are rejected as
|
||||
* mistyped, so there is nothing to carry.
|
||||
*/
|
||||
interface SkillSessionInvocation {
|
||||
mode: 'upgrade'
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:
|
||||
* the `dsh-host-webserver` schema (`host` a loopback/all-interfaces literal,
|
||||
* `port` a natural ≤ 65535) is the single source of both the default (the
|
||||
* shipped Web overlay value stands when a flag is absent) and validity (a bad
|
||||
* value fails loud at boot). `port` is `Number`-coerced only because the schema
|
||||
* wants a number, not a string. `dev` mounts the client HMR driver;
|
||||
* `workspaceRoot` is the parent directory for name-created workspaces.
|
||||
* Browser UI: `dsh web`. Host and port remain unvalidated pass-throughs to
|
||||
* the webserver schema; absent values leave the shipped Web overlay intact.
|
||||
*/
|
||||
interface WebInvocation {
|
||||
mode: 'web'
|
||||
/** Overlay of loader patches applied over the shipped web composition. */
|
||||
/** Overlay applied over the shipped Web composition instead of the personal one. */
|
||||
config?: string
|
||||
host?: string
|
||||
port?: number
|
||||
dev: boolean
|
||||
workspaceRoot?: string
|
||||
/** Extra authorities for the /api browser-trust fence (`host` or `host:port`); LAN IP literals are derived, not listed here. */
|
||||
/** Extra authorities for the /api browser-trust fence. */
|
||||
trustedHosts?: string[]
|
||||
}
|
||||
|
||||
/** The resolved `dsh` invocation: exactly one mode. `--help`/`--version`/errors exit inside {@link parseDshArgs}. */
|
||||
export type DshInvocation =
|
||||
| TuiInvocation
|
||||
| DumpConfigInvocation
|
||||
| HeadlessInvocation
|
||||
| MetaInvocation
|
||||
| SkillSessionInvocation
|
||||
| WebInvocation
|
||||
/** The resolved `dsh` invocation. Help, version, and errors exit inside {@link parseDshArgs}. */
|
||||
export type DshInvocation = ConfigInvocation | DumpConfigInvocation | HeadlessInvocation | WebInvocation
|
||||
|
||||
/** Raw web-subcommand options straight from Commander. */
|
||||
interface WebOptions {
|
||||
@@ -107,13 +61,9 @@ interface WebOptions {
|
||||
dumpDefaultConfig?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the two dump flags for one surface, or return `undefined` when
|
||||
* neither was passed. Both flags together are contradictory (one includes the
|
||||
* user layer, the other excludes it) and fail loud through `error`.
|
||||
*/
|
||||
/** Resolve config-dump flags for one command shape. */
|
||||
function resolveDump(
|
||||
surface: 'tui' | 'web',
|
||||
surface: 'config' | 'web',
|
||||
options: { config?: string; dumpConfig?: boolean; dumpDefaultConfig?: boolean },
|
||||
error: (message: string) => never,
|
||||
): DumpConfigInvocation | undefined {
|
||||
@@ -125,6 +75,9 @@ function resolveDump(
|
||||
if (defaultOnly && options.config !== undefined) {
|
||||
error('error: --dump-default-config prints the shipped tree and takes no --config')
|
||||
}
|
||||
if (surface === 'config' && !defaultOnly && options.config === undefined) {
|
||||
error('error: --dump-config requires --config <path>')
|
||||
}
|
||||
return {
|
||||
mode: 'dump-config',
|
||||
surface,
|
||||
@@ -133,12 +86,7 @@ function resolveDump(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow the raw `web` options into a {@link WebInvocation}. No host/port
|
||||
* validation: both flow to the webserver schema, which is the sole gate. `port`
|
||||
* is coerced to a number (the schema rejects a string) but not range-checked
|
||||
* here — `NaN`/out-of-range fail loud at the schema on boot.
|
||||
*/
|
||||
/** Narrow raw `web` options into a {@link WebInvocation}. */
|
||||
function resolveWeb(options: WebOptions): WebInvocation {
|
||||
return {
|
||||
mode: 'web',
|
||||
@@ -152,137 +100,72 @@ function resolveWeb(options: WebOptions): WebInvocation {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the raw argv into a {@link DshInvocation}, or print and exit for
|
||||
* `--help`/`--version`/a parse error. The default (no subcommand) is the
|
||||
* TUI/headless surface; `web` is a subcommand.
|
||||
* @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.
|
||||
* @param experimentalEnv - whether the environment opts into experimental
|
||||
* subcommands (`DSH_EXPERIMENTAL=1`); the caller reads the process boundary.
|
||||
* @returns the resolved invocation (only reached on a valid, non-help invocation).
|
||||
* Resolve argv into one invocation, or print and exit for help, version, or an
|
||||
* error.
|
||||
* @param argv - arguments after the Node binary and script.
|
||||
* @param version - version string printed by `--version`.
|
||||
* @returns the resolved invocation.
|
||||
*/
|
||||
export function parseDshArgs(argv: readonly string[], version: string, experimentalEnv: boolean): DshInvocation {
|
||||
export function parseDshArgs(argv: readonly string[], version: string): DshInvocation {
|
||||
let resolved: DshInvocation | undefined
|
||||
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`.
|
||||
.description('dsh: boot a DeepSeek Harness config overlay over the shipped base configuration.')
|
||||
.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
|
||||
dsh --config ./app.cordis.yml boot an overlay over the shipped base
|
||||
dsh -p "run the tests" answer one task, print the result, and exit
|
||||
dsh web serve the browser UI
|
||||
`)
|
||||
.exitOverride()
|
||||
// Stop parent options at a subcommand boundary so `web --config` belongs to
|
||||
// Web while `--config ... web` remains a leaked default-surface option.
|
||||
.enablePositionalOptions()
|
||||
// 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')
|
||||
.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')
|
||||
.option('--dump-config', 'print the composed config tree (base + surface + --config/personal overlay) and exit')
|
||||
.option('--dump-default-config', 'print the shipped config tree (base + surface overlay, no user layer) and exit')
|
||||
.option('-p, --prompt <task>', 'answer this task without an interactive UI, then exit')
|
||||
.option('--config <path>', 'overlay of loader patches to apply over the shipped base')
|
||||
.option('--dump-config', 'print the base plus --config overlay and exit')
|
||||
.option('--dump-default-config', 'print the shipped base config and exit')
|
||||
.action((options: {
|
||||
config?: string
|
||||
configReplace?: string
|
||||
prompt?: string
|
||||
resume?: string
|
||||
dumpConfig?: boolean
|
||||
dumpDefaultConfig?: boolean
|
||||
}) => {
|
||||
const dump = resolveDump('tui', options, message => program.error(message))
|
||||
if (options.config === '') program.error('error: --config needs a path')
|
||||
const dump = resolveDump('config', options, message => program.error(message))
|
||||
if (dump !== undefined) {
|
||||
// The dump prints composition; a boot-only flag alongside it would be
|
||||
// silently ignored, so reject the mix loud.
|
||||
if (options.prompt !== undefined || options.resume !== undefined || options.configReplace !== undefined) {
|
||||
program.error('error: --dump-config/--dump-default-config take none of -p/--prompt, --resume, or --config-replace')
|
||||
if (options.prompt !== undefined) {
|
||||
program.error('error: --dump-config/--dump-default-config take no -p/--prompt')
|
||||
}
|
||||
resolved = dump
|
||||
return
|
||||
}
|
||||
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) program.error('error: --prompt takes no --config')
|
||||
resolved = { mode: 'headless', prompt: options.prompt }
|
||||
return
|
||||
}
|
||||
// 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 },
|
||||
}
|
||||
const config = options.config ?? program.error('error: --config <path> is required')
|
||||
resolved = { mode: 'config', config }
|
||||
})
|
||||
|
||||
// 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/prompt/resume option is a mistyped invocation that must fail
|
||||
// loud rather than silently run and drop the input.
|
||||
/** Reject parent options that crossed a subcommand boundary. */
|
||||
const rejectParentOptions = (command: string): void => {
|
||||
const parent = program.opts<{
|
||||
config?: string
|
||||
configReplace?: string
|
||||
prompt?: string
|
||||
resume?: string
|
||||
dumpConfig?: boolean
|
||||
dumpDefaultConfig?: boolean
|
||||
}>()
|
||||
if (parent.config !== undefined || parent.configReplace !== undefined
|
||||
|| parent.prompt !== undefined || parent.resume !== undefined
|
||||
if (parent.config !== undefined || parent.prompt !== undefined
|
||||
|| parent.dumpConfig !== undefined || parent.dumpDefaultConfig !== undefined) {
|
||||
program.error(`error: ${command} takes none of --config, --config-replace, -p/--prompt, --resume, --dump-config, or --dump-default-config`)
|
||||
program.error(`error: ${command} takes none of parent --config, -p/--prompt, --dump-config, or --dump-default-config`)
|
||||
}
|
||||
}
|
||||
|
||||
// `meta` and `upgrade` are experimental: each runs only under its own
|
||||
// `--experimental` flag or an environment-wide `DSH_EXPERIMENTAL=1` opt-in,
|
||||
// and fails loud otherwise so the gate is never silently skipped.
|
||||
const requireExperimental = (command: string, flag: boolean | undefined): void => {
|
||||
if (flag !== true && !experimentalEnv) {
|
||||
program.error(`error: ${command} is experimental; pass --experimental or set DSH_EXPERIMENTAL=1`)
|
||||
}
|
||||
}
|
||||
|
||||
// Registration order is the rendered help order, so daily use comes first
|
||||
// and the harness-development surfaces (`web --dev`, `meta`)
|
||||
// come last. `upgrade` is a guided fresh-session entry: beyond the
|
||||
// experimental gate it takes no options and always mints a fresh session,
|
||||
// so nothing is left to carry.
|
||||
program
|
||||
.command('upgrade')
|
||||
.description('update this dsh installation to the latest version (experimental)')
|
||||
.option('--experimental', 'acknowledge this subcommand is experimental')
|
||||
.action((options: { experimental?: boolean }) => {
|
||||
rejectParentOptions('upgrade')
|
||||
requireExperimental('upgrade', options.experimental)
|
||||
resolved = { mode: 'upgrade' }
|
||||
})
|
||||
|
||||
// Host and port name no default: the CLI passes neither through when the flag
|
||||
// is absent, so the shipped Web overlay 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')
|
||||
web
|
||||
.option('--config <path>', 'apply this overlay of loader patches over the shipped configuration')
|
||||
.option('--config <path>', 'apply this overlay of loader patches over the shipped Web 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', 'mount the client-plugin HMR receiver (run pnpm run dev:web separately to rebuild bundles)')
|
||||
@@ -292,6 +175,7 @@ Examples:
|
||||
.option('--dump-default-config', 'print the shipped config tree (base + web overlay, no user layer) and exit')
|
||||
.action((options: WebOptions) => {
|
||||
rejectParentOptions('web')
|
||||
if (options.config === '') program.error('error: --config needs a path')
|
||||
const dump = resolveDump('web', options, message => program.error(message))
|
||||
if (dump !== undefined) {
|
||||
resolved = dump
|
||||
@@ -300,25 +184,12 @@ Examples:
|
||||
resolved = resolveWeb(options)
|
||||
})
|
||||
|
||||
program
|
||||
.command('meta')
|
||||
.description('work on the dsh source that runs this command, from any directory (experimental)')
|
||||
.option('--experimental', 'acknowledge this subcommand is experimental')
|
||||
.action((options: { experimental?: boolean }) => {
|
||||
rejectParentOptions('meta')
|
||||
requireExperimental('meta', options.experimental)
|
||||
resolved = { mode: 'meta' }
|
||||
})
|
||||
|
||||
try {
|
||||
program.parse(argv, { from: 'user' })
|
||||
} 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)
|
||||
}
|
||||
/* v8 ignore next -- the default action or a subcommand action always resolves, or parse throws above */
|
||||
/* v8 ignore next -- an action resolves or Commander throws */
|
||||
if (resolved === undefined) throw new Error('dsh: no invocation resolved')
|
||||
return resolved
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @module @deepseek-ai/dsh/bin
|
||||
*/
|
||||
|
||||
/* v8 ignore file -- built-bin and PTY tests exercise this self-executing dispatch. */
|
||||
/* v8 ignore file -- built-bin acceptance exercises this self-executing dispatch. */
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -25,10 +25,14 @@ function readVersion(): string {
|
||||
}
|
||||
|
||||
loadEnv('dsh')
|
||||
// The env opt-in is read at the process boundary; `1` is the documented value.
|
||||
const invocation = parseDshArgs(process.argv.slice(2), readVersion(), process.env.DSH_EXPERIMENTAL === '1')
|
||||
const invocation = parseDshArgs(process.argv.slice(2), readVersion())
|
||||
|
||||
switch (invocation.mode) {
|
||||
case 'config': {
|
||||
const { runConfig } = await import('./config.ts')
|
||||
await runConfig(invocation.config)
|
||||
break
|
||||
}
|
||||
case 'web': {
|
||||
const { runWeb } = await import('./web.ts')
|
||||
await runWeb(invocation.host, invocation.port, invocation.dev, invocation.workspaceRoot, invocation.trustedHosts, invocation.config)
|
||||
@@ -39,26 +43,11 @@ switch (invocation.mode) {
|
||||
await runHeadless(invocation.prompt)
|
||||
break
|
||||
}
|
||||
case 'tui': {
|
||||
const { runTui } = await import('./tui.ts')
|
||||
await runTui(invocation.config, invocation.resume, undefined, undefined, invocation.configReplace)
|
||||
break
|
||||
}
|
||||
case 'dump-config': {
|
||||
const { runDumpConfig } = await import('./dump-config.ts')
|
||||
runDumpConfig(invocation.surface, invocation.defaultOnly, invocation.config)
|
||||
break
|
||||
}
|
||||
case 'meta': {
|
||||
const { runTui, SOURCE_ROOT } = await import('./tui.ts')
|
||||
await runTui(undefined, undefined, SOURCE_ROOT)
|
||||
break
|
||||
}
|
||||
case 'upgrade': {
|
||||
const { runTui } = await import('./tui.ts')
|
||||
await runTui(undefined, undefined, undefined, `dsh-${invocation.mode}`)
|
||||
break
|
||||
}
|
||||
default:
|
||||
invocation satisfies never
|
||||
throw new Error(`dsh: unhandled invocation mode ${JSON.stringify(invocation)}`)
|
||||
|
||||
54
apps/cli/src/config.ts
Normal file
54
apps/cli/src/config.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Raw `dsh --config <path>` boot: apply one required patch-list overlay over
|
||||
* the shipped base config, then leave process lifetime to the mounted plugins.
|
||||
* @module @deepseek-ai/dsh/config
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from 'cordis'
|
||||
import {
|
||||
boot,
|
||||
installFailLoud,
|
||||
loadOverlayPatches,
|
||||
resolveConfigPath,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { configHasTelemetryRow, resolveTelemetryPatch } from './app-cli-entry.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
const BASE_CONFIG = fileURLToPath(new URL('../config/base.cordis.yml', import.meta.url))
|
||||
|
||||
/* v8 ignore start -- the source-launch and built-bin acceptance paths own executable dispatch */
|
||||
/**
|
||||
* Boot the shipped base with one explicit overlay.
|
||||
* @param config - required patch-list path parsed from `--config`.
|
||||
*/
|
||||
export async function runConfig(config: string): Promise<void> {
|
||||
const app: { current?: Context } = {}
|
||||
let exiting = false
|
||||
const shutdown = (code: number): void => {
|
||||
if (exiting) return
|
||||
exiting = true
|
||||
void Promise.resolve(app.current?.fiber.dispose()).finally(() => { process.exit(code) })
|
||||
}
|
||||
// An inserted front door can publish readiness before sibling rows finish
|
||||
// mounting. Signals must own teardown throughout that startup window, not
|
||||
// only after boot() settles.
|
||||
process.on('SIGTERM', () => { shutdown(0) })
|
||||
process.on('SIGINT', () => { shutdown(130) })
|
||||
installFailLoud(NAME, process, async () => {
|
||||
await app.current?.fiber.dispose()
|
||||
})
|
||||
const overlay = resolveConfigPath(config, undefined)
|
||||
const telemetryPatch = resolveTelemetryPatch(
|
||||
process.env.DSH_TELEMETRY_DISABLED,
|
||||
configHasTelemetryRow(BASE_CONFIG),
|
||||
)
|
||||
const ctx = await boot(NAME, BASE_CONFIG, [
|
||||
...loadOverlayPatches(NAME, overlay),
|
||||
...telemetryPatch === undefined ? [] : [telemetryPatch],
|
||||
], (hostCtx) => {
|
||||
app.current = hostCtx
|
||||
})
|
||||
app.current = ctx
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
@@ -1,12 +1,6 @@
|
||||
/**
|
||||
* `dsh --dump-config` / `dsh web --dump-config` — print the composed config
|
||||
* tree without booting: the shipped base, the surface overlay, and (unless
|
||||
* `--dump-default-config`) the `--config` or personal overlay, composed
|
||||
* through the include's own patch algorithm so the printed tree is exactly
|
||||
* what that surface would mount. `!!js` expressions print verbatim,
|
||||
* unevaluated — the dump shows composition, not one process's environment.
|
||||
* Launcher-provided boot-context values (session identity, CLI-flag patches)
|
||||
* are per-invocation facts outside the config tree and do not appear.
|
||||
* Config-dump entry for raw `dsh --config` and `dsh web`: compose through the
|
||||
* include plugin's patch algorithm without booting or evaluating `!!js`.
|
||||
* @module @deepseek-ai/dsh/dump-config
|
||||
*/
|
||||
|
||||
@@ -22,39 +16,36 @@ import {
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
const BASE_CONFIG = fileURLToPath(new URL('../config/base.cordis.yml', import.meta.url))
|
||||
const SURFACE_OVERLAYS = {
|
||||
tui: fileURLToPath(new URL('../config/tui.cordis.yml', import.meta.url)),
|
||||
web: fileURLToPath(new URL('../config/web.cordis.yml', import.meta.url)),
|
||||
} as const
|
||||
const WEB_OVERLAY = fileURLToPath(new URL('../config/web.cordis.yml', import.meta.url))
|
||||
|
||||
/* v8 ignore start -- composition over the unit-tested renderConfigDump; the
|
||||
built-bin e2e drives this path end to end */
|
||||
/* v8 ignore start -- built-bin acceptance drives this boot-free dispatch */
|
||||
/**
|
||||
* Print one surface's composed config tree to stdout, with a comment
|
||||
* separator naming the file each section of rows comes from (and the layers
|
||||
* that patched it).
|
||||
* @param surface - which surface overlay to compose over the shared base.
|
||||
* @param defaultOnly - stop at the surface overlay (no `--config`/personal layer).
|
||||
* @param config - the `--config` overlay path composed instead of the personal
|
||||
* one, or `undefined` to use `$DSH_HOME/config.yaml`.
|
||||
* Print a raw or Web composition with provenance comments.
|
||||
* @param surface - raw base-plus-config composition, or the Web composition.
|
||||
* @param defaultOnly - omit the explicit or personal user layer.
|
||||
* @param config - explicit overlay path; required for a non-default raw dump.
|
||||
*/
|
||||
export function runDumpConfig(surface: 'tui' | 'web', defaultOnly: boolean, config?: string): void {
|
||||
const overlay = SURFACE_OVERLAYS[surface]
|
||||
const layers: ConfigDumpLayer[] = [
|
||||
{ label: basename(overlay), patches: loadOverlayPatches(NAME, overlay) },
|
||||
]
|
||||
if (!defaultOnly) {
|
||||
if (config === undefined) {
|
||||
const personal = loadPersonalPatches(NAME)
|
||||
// The personal file may be absent; the shipped layers still print.
|
||||
if (personal !== undefined) {
|
||||
layers.push({ label: join(resolveDshHome(), PERSONAL_CONFIG_FILENAME), patches: personal })
|
||||
}
|
||||
} else {
|
||||
export function runDumpConfig(surface: 'config' | 'web', defaultOnly: boolean, config?: string): void {
|
||||
const layers: ConfigDumpLayer[] = []
|
||||
if (surface === 'config') {
|
||||
if (!defaultOnly) {
|
||||
/* v8 ignore next -- parseDshArgs requires this combination */
|
||||
if (config === undefined) throw new Error('dsh: raw config dump requires an overlay')
|
||||
layers.push({ label: config, patches: loadOverlayPatches(NAME, config) })
|
||||
}
|
||||
} else {
|
||||
layers.push({ label: basename(WEB_OVERLAY), patches: loadOverlayPatches(NAME, WEB_OVERLAY) })
|
||||
if (!defaultOnly) {
|
||||
if (config === undefined) {
|
||||
const personal = loadPersonalPatches(NAME)
|
||||
if (personal !== undefined) {
|
||||
layers.push({ label: join(resolveDshHome(), PERSONAL_CONFIG_FILENAME), patches: personal })
|
||||
}
|
||||
} else {
|
||||
layers.push({ label: config, patches: loadOverlayPatches(NAME, config) })
|
||||
}
|
||||
}
|
||||
}
|
||||
process.stdout.write(renderConfigDump(NAME, BASE_CONFIG, layers))
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* Static terminal rasters derived from the official 24x24 DeepSeek icon.
|
||||
*
|
||||
* Source: `../../assets/deepseek-color.svg`, whose path data is copied exactly
|
||||
* from the supplied official icon (viewBox `0 0 24 24`, fill `#4D6BFE`). Each
|
||||
* tier rasterizes that path into a square binary
|
||||
* mask without redrawing its contour. The Unicode form packs two source rows
|
||||
* into `▀`/`▄`/`█`; the ASCII fallback packs the same two bits into
|
||||
* `'`/`_`/`#`. Assets contain no ANSI and are never generated at runtime.
|
||||
* @module @deepseek-ai/dsh/tui-onboarding/tui-first-run-welcome-art
|
||||
*/
|
||||
|
||||
/** Responsive official-icon raster tier. */
|
||||
export type TuiFirstRunWelcomeArtTier = 'full' | 'compact' | 'minimal'
|
||||
|
||||
/** One raster with a block-cell primary and bit-equivalent ASCII fallback. */
|
||||
export interface TuiFirstRunWelcomeArt {
|
||||
/** Two vertical source pixels per terminal cell. */
|
||||
readonly unicode: readonly string[]
|
||||
/** Same two-bit cells encoded as top `'`, bottom `_`, and both `#`. */
|
||||
readonly ascii: readonly string[]
|
||||
}
|
||||
|
||||
const fullUnicode = Object.freeze([
|
||||
' ▄',
|
||||
' ▄▄▄▄▄▄▄▄▄▄███▀ ██▄',
|
||||
' ▄███████████████▄ ████▄ ▄▄▄▄██',
|
||||
' ▄███████████████████▄ ████████████▀',
|
||||
' ▄██████████████████████▄ ▀█████████▀',
|
||||
'▄███▀█████████████████████▄ ████▀▀',
|
||||
'███ ▀▀█████████▀▀▀█████████▀',
|
||||
'███ ▀███████▀█ ▀███████',
|
||||
'███▄ ▀███████▄ ▀█████▀',
|
||||
'▀███ ▀██████████████',
|
||||
' ▀███▄ ▀███████████▀',
|
||||
' ▀███▄ ▄▄▄ ▀████████▀',
|
||||
' █████▄ ███▄▄ ▀█████▄▄',
|
||||
' ▀█████████████▄▄▄▄█▀█████▀',
|
||||
' ▀▀███████████▀▀',
|
||||
])
|
||||
|
||||
const fullAscii = Object.freeze([
|
||||
' _',
|
||||
" __________###' ##_",
|
||||
' _###############_ ####_ ____##',
|
||||
" _###################_ ############'",
|
||||
" _######################_ '#########'",
|
||||
"_###'#####################_ ####''",
|
||||
"### ''#########'''#########'",
|
||||
"### '#######'# '#######",
|
||||
"###_ '#######_ '#####'",
|
||||
"'### '##############",
|
||||
" '###_ '###########'",
|
||||
" '###_ ___ '########'",
|
||||
" #####_ ###__ '#####__",
|
||||
" '#############____#'#####'",
|
||||
" ''###########''",
|
||||
])
|
||||
|
||||
const compactUnicode = Object.freeze([
|
||||
' ▄▄▄▄▄▄▄██▀ █▄ ▄',
|
||||
' ▄███████████▄▄ ███▄▄████',
|
||||
' ████████████████▄ ▀██████▀',
|
||||
'██▀▀▀▀▀████████████▄▄██▀',
|
||||
'██ ▀█████▄ ▀█████',
|
||||
'██▄ ▀████▄ ▄████',
|
||||
' ██▄ ████████▀',
|
||||
' ██▄ ▄▄ ▀█████▀',
|
||||
' ▀███▄▄▄███▄ ████▄▄',
|
||||
' ▀▀▀███████▀▀',
|
||||
])
|
||||
|
||||
const compactAscii = Object.freeze([
|
||||
" _______##' #_ _",
|
||||
' _###########__ ###__####',
|
||||
" ################_ '######'",
|
||||
"##'''''############__##'",
|
||||
"## '#####_ '#####",
|
||||
"##_ '####_ _####",
|
||||
" ##_ ########'",
|
||||
" ##_ __ '#####'",
|
||||
" '###___###_ ####__",
|
||||
" '''#######''",
|
||||
])
|
||||
|
||||
const minimalUnicode = Object.freeze([
|
||||
' ▄▄▄▄▄▄ ▄▄',
|
||||
' ▄████████▄ ▀████▀',
|
||||
'█▀▀▀▀███████▄██▀',
|
||||
'█▄ ▀███ ▀███',
|
||||
'▀█▄ ▀█████',
|
||||
' ▀█▄▄ █▄▄▀███▄',
|
||||
' ▀▀▀▀▀▀',
|
||||
])
|
||||
|
||||
const minimalAscii = Object.freeze([
|
||||
' ______ __',
|
||||
" _########_ '####'",
|
||||
"#''''#######_##'",
|
||||
"#_ '### '###",
|
||||
"'#_ '#####",
|
||||
" '#__ #__'###_",
|
||||
" ''''''",
|
||||
])
|
||||
|
||||
/** Exact-path terminal rasters by responsive tier. */
|
||||
export const TUI_FIRST_RUN_WELCOME_WHALE = Object.freeze({
|
||||
full: Object.freeze({ unicode: fullUnicode, ascii: fullAscii }),
|
||||
compact: Object.freeze({ unicode: compactUnicode, ascii: compactAscii }),
|
||||
minimal: Object.freeze({ unicode: minimalUnicode, ascii: minimalAscii }),
|
||||
}) satisfies Readonly<Record<TuiFirstRunWelcomeArtTier, TuiFirstRunWelcomeArt>>
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Centrally owned version and all-locale Chinese copy for the shipped TUI first-run notice.
|
||||
*
|
||||
* A material wording change increments {@link TUI_FIRST_RUN_WELCOME_NOTICE_VERSION}
|
||||
* so every Harness home presents the revised notice once.
|
||||
* @module @deepseek-ai/dsh/tui-onboarding/tui-first-run-welcome-copy
|
||||
*/
|
||||
|
||||
/** Copy version persisted after the user explicitly continues. */
|
||||
export const TUI_FIRST_RUN_WELCOME_NOTICE_VERSION = 4
|
||||
|
||||
/** Locale-shaped text rendered by the first-run welcome overlay. */
|
||||
export interface TuiFirstRunWelcomeNoticeCopy {
|
||||
/** Overlay heading. */
|
||||
readonly title: string
|
||||
/** Ordered prose paragraphs. */
|
||||
readonly paragraphs: readonly string[]
|
||||
/** Enter action label. */
|
||||
readonly continueLabel: string
|
||||
/** Hint shown when the prose is scrollable. */
|
||||
readonly scrollHint: string
|
||||
/** Status shown while the acknowledgement reaches disk. */
|
||||
readonly saving: string
|
||||
/** Retry message shown when the acknowledgement cannot be persisted. */
|
||||
readonly saveError: string
|
||||
}
|
||||
|
||||
/** Complete Chinese notice used for every locale. */
|
||||
const TUI_FIRST_RUN_WELCOME_CHINESE_COPY = Object.freeze<TuiFirstRunWelcomeNoticeCopy>({
|
||||
title: 'DeepSeek Harness',
|
||||
paragraphs: Object.freeze([
|
||||
'感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完善,体验难免有些粗糙。',
|
||||
'“如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。',
|
||||
'为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session Log;如需关闭,请设置环境变量 DSH_TELEMETRY_DISABLED=1。另外,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把它打磨得更好。',
|
||||
]),
|
||||
continueLabel: '继续',
|
||||
scrollHint: '↑/↓ 滚动',
|
||||
saving: '正在保存确认…',
|
||||
saveError: '无法保存确认,请按 Enter 重试。',
|
||||
})
|
||||
|
||||
/** Locale map whose entries deliberately share the single Chinese owner copy. */
|
||||
export const TUI_FIRST_RUN_WELCOME_NOTICE_COPY = Object.freeze({
|
||||
'zh-CN': TUI_FIRST_RUN_WELCOME_CHINESE_COPY,
|
||||
en: TUI_FIRST_RUN_WELCOME_CHINESE_COPY,
|
||||
})
|
||||
|
||||
/** Locale presented by the shipped first-run notice. */
|
||||
export const TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE = 'zh-CN' as const
|
||||
@@ -1,385 +0,0 @@
|
||||
/**
|
||||
* Effect-owned first-run overlay for the shipped `dsh` TUI.
|
||||
*
|
||||
* The launcher owns the per-DSH_HOME acknowledgement boundary; the component
|
||||
* reaches the terminal only through the mounted `ctx.tui` overlay service and
|
||||
* never touches the session or model context.
|
||||
* @module @deepseek-ai/dsh/tui-onboarding/tui-first-run-welcome
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { lstat, mkdir, open, rename, rm } from 'node:fs/promises'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import type { Context } from 'cordis'
|
||||
import {
|
||||
Key,
|
||||
matchesKey,
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
wrapTextWithAnsi,
|
||||
} from '@earendil-works/pi-tui'
|
||||
import {
|
||||
disposeRootAndExit,
|
||||
type TuiComponent,
|
||||
type TuiFocusable,
|
||||
type TuiOverlayHost,
|
||||
} from '@deepseek-ai/dsh-tui'
|
||||
import {
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_COPY,
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE,
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
|
||||
type TuiFirstRunWelcomeNoticeCopy,
|
||||
} from './tui-first-run-welcome-copy.ts'
|
||||
import {
|
||||
TUI_FIRST_RUN_WELCOME_WHALE,
|
||||
type TuiFirstRunWelcomeArtTier,
|
||||
} from './tui-first-run-welcome-art.ts'
|
||||
|
||||
// TODO: Move acknowledgement persistence behind @deepseek-ai/dsh-storage once
|
||||
// its backend contract supports concurrent host processes. This same-value
|
||||
// marker must not inherit JSON lost updates or SQLite busy failures.
|
||||
const ACKNOWLEDGEMENT_DIRECTORY = 'notices'
|
||||
const ACKNOWLEDGEMENT_BASENAME = 'tui-first-run-welcome'
|
||||
|
||||
/** Cordis plugin name. */
|
||||
export const name = 'tui-first-run-welcome'
|
||||
/** The notice can open only after the terminal-local overlay service mounts. */
|
||||
export const inject = ['tui']
|
||||
|
||||
/** Launcher-resolved configuration for the terminal-local notice. */
|
||||
interface Config {
|
||||
/** Absolute DeepSeek Harness home owning this acknowledgement. */
|
||||
readonly dshHome: string
|
||||
/** Render the bit-equivalent printable ASCII icon fallback. */
|
||||
readonly asciiArt?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect an explicitly non-Unicode terminal locale for the static ASCII art fallback.
|
||||
* @param env - Process environment carrying locale and terminal declarations.
|
||||
* @returns `true` only when the environment explicitly declares an ASCII-only locale or dumb terminal.
|
||||
*/
|
||||
export function needsTuiFirstRunWelcomeAsciiArt(
|
||||
env: Readonly<Record<string, string | undefined>> = process.env,
|
||||
): boolean {
|
||||
const locale = env.LC_ALL ?? env.LC_CTYPE ?? env.LANG
|
||||
return env.TERM === 'dumb' || locale === 'C' || locale === 'POSIX'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the immutable marker for one notice version.
|
||||
* @param dshHome - Resolved Harness home.
|
||||
* @param version - Copy version whose acknowledgement is queried.
|
||||
* @returns Absolute marker path beneath the Harness home.
|
||||
*/
|
||||
export function tuiFirstRunWelcomeAcknowledgementPath(dshHome: string, version: number): string {
|
||||
return join(
|
||||
dshHome,
|
||||
ACKNOWLEDGEMENT_DIRECTORY,
|
||||
`${ACKNOWLEDGEMENT_BASENAME}-v${String(version)}.ack`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether one notice version has been acknowledged.
|
||||
* @param dshHome - Resolved Harness home.
|
||||
* @param version - Copy version to inspect.
|
||||
* @returns `true` only for a regular marker file; a malformed marker fails loud.
|
||||
*/
|
||||
export async function hasTuiFirstRunWelcomeAcknowledgement(
|
||||
dshHome: string,
|
||||
version: number = TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
|
||||
): Promise<boolean> {
|
||||
const path = tuiFirstRunWelcomeAcknowledgementPath(dshHome, version)
|
||||
try {
|
||||
const info = await lstat(path)
|
||||
if (!info.isFile()) throw new Error(`TUI welcome acknowledgement is not a file: ${path}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException | null)?.code === 'ENOENT') return false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist one version acknowledgement by syncing a random same-directory file
|
||||
* before atomically replacing the immutable marker. Concurrent launches publish
|
||||
* the same fact, so same-value last-writer-wins replacement loses no state.
|
||||
* @param dshHome - Resolved Harness home.
|
||||
* @param version - Copy version being acknowledged.
|
||||
*/
|
||||
export async function acknowledgeTuiFirstRunWelcome(
|
||||
dshHome: string,
|
||||
version: number = TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
|
||||
): Promise<void> {
|
||||
const path = tuiFirstRunWelcomeAcknowledgementPath(dshHome, version)
|
||||
const directory = dirname(path)
|
||||
const temp = join(directory, `.${basename(path)}.${randomUUID()}.tmp`)
|
||||
await mkdir(directory, { recursive: true, mode: 0o700 })
|
||||
await syncDirectory(dirname(directory))
|
||||
let handle: Awaited<ReturnType<typeof open>> | undefined
|
||||
try {
|
||||
handle = await open(temp, 'wx', 0o600)
|
||||
await handle.sync()
|
||||
const created = handle
|
||||
handle = undefined
|
||||
await created.close()
|
||||
await rename(temp, path)
|
||||
} catch (error) {
|
||||
/* v8 ignore start -- fault-injected UI coverage proves failed acknowledgements stay uncommitted and retryable */
|
||||
try {
|
||||
await handle?.close()
|
||||
} finally {
|
||||
await rm(temp, { force: true })
|
||||
}
|
||||
throw error
|
||||
/* v8 ignore stop */
|
||||
}
|
||||
try {
|
||||
await syncDirectory(directory)
|
||||
/* v8 ignore next -- rename is the commit point; directory-fsync fault injection is platform-specific */
|
||||
} catch {
|
||||
// Swallow post-rename directory fsync failure: the marker is already committed,
|
||||
// and crash loss can only make the notice reappear on the safe side.
|
||||
}
|
||||
}
|
||||
|
||||
/** Sync one POSIX directory after publishing a child entry. */
|
||||
/* v8 ignore start -- Windows rejects directory opens; POSIX unit coverage owns this path. */
|
||||
async function syncDirectory(path: string): Promise<void> {
|
||||
if (process.platform === 'win32') return
|
||||
const handle = await open(path, 'r')
|
||||
try {
|
||||
await handle.sync()
|
||||
} finally {
|
||||
await handle.close()
|
||||
}
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
|
||||
/** Render one visible-width-padded line inside the notice frame. */
|
||||
function framed(content: string, innerWidth: number, host: TuiOverlayHost): string {
|
||||
const clipped = truncateToWidth(content, innerWidth, '')
|
||||
return `${host.theme.dim('│')} ${clipped}${' '.repeat(Math.max(0, innerWidth - visibleWidth(clipped)))} ${host.theme.dim('│')}`
|
||||
}
|
||||
|
||||
/** Center one line by terminal column width. */
|
||||
function centered(content: string, width: number): string {
|
||||
const clipped = truncateToWidth(content, width, '')
|
||||
const remaining = Math.max(0, width - visibleWidth(clipped))
|
||||
return `${' '.repeat(Math.floor(remaining / 2))}${clipped}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the art tier for the actual overlay width and viewport height.
|
||||
* @param innerWidth - Columns inside the frame.
|
||||
* @param viewportRows - Current terminal rows.
|
||||
* @returns full, compact, minimal, or no art when prose must take priority.
|
||||
*/
|
||||
export function tuiFirstRunWelcomeArtTier(
|
||||
innerWidth: number,
|
||||
viewportRows: number,
|
||||
): TuiFirstRunWelcomeArtTier | undefined {
|
||||
const compositionCapacity = Math.max(1, Math.max(7, Math.floor(viewportRows * 0.9)) - 5)
|
||||
if (innerWidth >= 96 && TUI_FIRST_RUN_WELCOME_WHALE.full.unicode.length <= compositionCapacity) return 'full'
|
||||
if (innerWidth >= 80 && TUI_FIRST_RUN_WELCOME_WHALE.compact.unicode.length + 4 <= compositionCapacity) return 'compact'
|
||||
if (innerWidth >= 64 && TUI_FIRST_RUN_WELCOME_WHALE.minimal.unicode.length + 4 <= compositionCapacity) return 'minimal'
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Wrap the centrally owned prose while promoting its opening quotation. */
|
||||
function proseLines(
|
||||
copy: TuiFirstRunWelcomeNoticeCopy,
|
||||
width: number,
|
||||
host: TuiOverlayHost,
|
||||
): string[] {
|
||||
const lines: string[] = []
|
||||
for (const [index, paragraph] of copy.paragraphs.entries()) {
|
||||
if (index > 0) lines.push('')
|
||||
const quoteEnd = paragraph.startsWith('“') ? paragraph.indexOf('”') : -1
|
||||
if (quoteEnd > 0) {
|
||||
const quote = paragraph.slice(0, quoteEnd + 1)
|
||||
const remainder = paragraph.slice(quoteEnd + 1).trimStart()
|
||||
lines.push(...wrapTextWithAnsi(host.theme.bold(host.theme.text(host.display(quote))), width))
|
||||
lines.push('')
|
||||
if (remainder !== '') lines.push(...wrapTextWithAnsi(host.theme.text(host.display(remainder)), width))
|
||||
} else {
|
||||
lines.push(...wrapTextWithAnsi(host.theme.text(host.display(paragraph)), width))
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
/** Render centered static brand art without putting ANSI into its owner file. */
|
||||
function artLines(
|
||||
tier: TuiFirstRunWelcomeArtTier,
|
||||
width: number,
|
||||
host: TuiOverlayHost,
|
||||
asciiArt: boolean,
|
||||
): string[] {
|
||||
const art = TUI_FIRST_RUN_WELCOME_WHALE[tier][asciiArt ? 'ascii' : 'unicode']
|
||||
return art.map(line => centered(host.theme.brand(line), width))
|
||||
}
|
||||
|
||||
/** Responsive, scrollable notice whose only completion input is Enter. */
|
||||
export class TuiFirstRunWelcomeComponent implements TuiComponent, TuiFocusable {
|
||||
focused = false
|
||||
private scrollOffset = 0
|
||||
private bodyCapacity = 1
|
||||
private maxScrollOffset = 0
|
||||
private saving = false
|
||||
private saveFailed = false
|
||||
|
||||
constructor(
|
||||
private readonly host: TuiOverlayHost,
|
||||
private readonly copy: TuiFirstRunWelcomeNoticeCopy,
|
||||
private readonly acknowledge: () => Promise<void>,
|
||||
private readonly exit: () => void,
|
||||
private readonly asciiArt = false,
|
||||
) {}
|
||||
|
||||
invalidate(): void {}
|
||||
|
||||
render(width: number): string[] {
|
||||
const frameWidth = Math.max(6, width)
|
||||
const innerWidth = Math.max(1, frameWidth - 4)
|
||||
const viewportRows = this.host.viewport.rows
|
||||
const tier = tuiFirstRunWelcomeArtTier(innerWidth, viewportRows)
|
||||
const availableRows = Math.max(7, Math.floor(viewportRows * 0.9))
|
||||
const title = this.host.theme.bold(this.host.theme.brand(this.copy.title))
|
||||
let fixedHeader: string[] = []
|
||||
let fullContentHeader: string[] = []
|
||||
let body: string[]
|
||||
let fullArt: string[] | undefined
|
||||
const fullArtWidth = 44
|
||||
|
||||
if (tier === 'full') {
|
||||
fullArt = artLines(tier, fullArtWidth, this.host, this.asciiArt)
|
||||
const contentWidth = Math.max(1, innerWidth - fullArtWidth - 3)
|
||||
fullContentHeader = [centered(title, contentWidth), '']
|
||||
body = proseLines(this.copy, contentWidth, this.host)
|
||||
} else {
|
||||
const art = tier === undefined ? [] : artLines(tier, innerWidth, this.host, this.asciiArt)
|
||||
fixedHeader = [...art, ...art.length === 0 ? [] : [''], centered(title, innerWidth), '']
|
||||
body = proseLines(this.copy, innerWidth, this.host)
|
||||
}
|
||||
|
||||
const compositionCapacity = Math.max(1, availableRows - 5)
|
||||
const bodyLimit = Math.max(1, compositionCapacity - fixedHeader.length - fullContentHeader.length)
|
||||
this.bodyCapacity = Math.min(body.length, bodyLimit)
|
||||
const maxOffset = Math.max(0, body.length - this.bodyCapacity)
|
||||
this.maxScrollOffset = maxOffset
|
||||
this.scrollOffset = Math.min(this.scrollOffset, maxOffset)
|
||||
const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + this.bodyCapacity)
|
||||
|
||||
const top = this.host.theme.dim(`╭${'─'.repeat(Math.max(0, frameWidth - 2))}╮`)
|
||||
const separator = this.host.theme.dim(`├${'─'.repeat(Math.max(0, frameWidth - 2))}┤`)
|
||||
const bottom = this.host.theme.dim(`╰${'─'.repeat(Math.max(0, frameWidth - 2))}╯`)
|
||||
const action = this.host.theme.bold(this.host.theme.accent(`Enter ${this.copy.continueLabel}`))
|
||||
const hasAbove = this.scrollOffset > 0
|
||||
const hasBelow = this.scrollOffset < maxOffset
|
||||
const scroll = hasAbove || hasBelow
|
||||
? `${hasAbove ? '↑' : ' '} ${this.copy.scrollHint} ${hasBelow ? '↓' : ' '}`
|
||||
: ''
|
||||
const status = this.saveFailed
|
||||
? this.host.theme.error(this.copy.saveError)
|
||||
: this.saving
|
||||
? this.host.theme.dim(this.copy.saving)
|
||||
: this.host.theme.dim(scroll)
|
||||
|
||||
const fullContent = [...fullContentHeader, ...visibleBody]
|
||||
const composition = fullArt === undefined
|
||||
? [...fixedHeader, ...visibleBody]
|
||||
: Array.from({ length: Math.max(fullArt.length, fullContent.length) }, (_, index) => {
|
||||
const art = fullArt[index] ?? ''
|
||||
const line = fullContent[index] ?? ''
|
||||
const left = `${art}${' '.repeat(Math.max(0, fullArtWidth - visibleWidth(art)))}`
|
||||
return `${left} ${line}`
|
||||
})
|
||||
|
||||
return [
|
||||
top,
|
||||
...composition.map(line => framed(line, innerWidth, this.host)),
|
||||
separator,
|
||||
framed(centered(action, innerWidth), innerWidth, this.host),
|
||||
framed(centered(status, innerWidth), innerWidth, this.host),
|
||||
bottom,
|
||||
]
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
if (matchesKey(data, Key.ctrl('c')) || matchesKey(data, Key.ctrl('d'))) {
|
||||
this.exit()
|
||||
return
|
||||
}
|
||||
if (matchesKey(data, Key.enter)) {
|
||||
if (!this.saving) void this.commit()
|
||||
return
|
||||
}
|
||||
if (this.saving || matchesKey(data, Key.escape)) return
|
||||
if (matchesKey(data, Key.up)) this.scrollBy(-1)
|
||||
else if (matchesKey(data, Key.down)) this.scrollBy(1)
|
||||
else if (matchesKey(data, Key.pageUp)) this.scrollBy(-this.bodyCapacity)
|
||||
else if (matchesKey(data, Key.pageDown)) this.scrollBy(this.bodyCapacity)
|
||||
else if (matchesKey(data, Key.home)) this.scrollTo(0)
|
||||
else if (matchesKey(data, Key.end)) this.scrollTo(this.maxScrollOffset)
|
||||
}
|
||||
|
||||
private scrollBy(delta: number): void {
|
||||
this.scrollTo(this.scrollOffset + delta)
|
||||
}
|
||||
|
||||
private scrollTo(offset: number): void {
|
||||
this.scrollOffset = Math.min(this.maxScrollOffset, Math.max(0, offset))
|
||||
this.host.invalidate()
|
||||
}
|
||||
|
||||
private async commit(): Promise<void> {
|
||||
this.saving = true
|
||||
this.saveFailed = false
|
||||
this.host.invalidate()
|
||||
try {
|
||||
await this.acknowledge()
|
||||
this.host.close()
|
||||
} catch {
|
||||
this.saving = false
|
||||
this.saveFailed = true
|
||||
this.host.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the first-run notice through the mounted TUI's FIFO overlay owner.
|
||||
* @param ctx - Plugin context carrying the terminal-local TUI service.
|
||||
* @param config - Launcher-resolved Harness home.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const copy = TUI_FIRST_RUN_WELCOME_NOTICE_COPY[TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE]
|
||||
const pending = new Set<Promise<void>>()
|
||||
const acknowledge = (): Promise<void> => {
|
||||
const task = acknowledgeTuiFirstRunWelcome(config.dshHome)
|
||||
pending.add(task)
|
||||
const settled = (): void => { pending.delete(task) }
|
||||
void task.then(settled, settled)
|
||||
return task
|
||||
}
|
||||
ctx.effect(() => async () => {
|
||||
await Promise.allSettled(pending)
|
||||
}, 'tui first-run welcome acknowledgement')
|
||||
ctx.tui.openOverlay({
|
||||
create: host => new TuiFirstRunWelcomeComponent(
|
||||
host,
|
||||
copy,
|
||||
acknowledge,
|
||||
() => { disposeRootAndExit(ctx, 0) },
|
||||
config.asciiArt ?? false,
|
||||
),
|
||||
options: {
|
||||
width: '100%',
|
||||
maxHeight: '90%',
|
||||
anchor: 'center',
|
||||
margin: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
/**
|
||||
* `dsh` default surface — the interactive TUI coding agent. Boots the shipped
|
||||
* shared base and TUI overlay, followed by either `--config` or the personal overlay
|
||||
* from the Harness home (`~/.dsh`): its `.env` fills environment gaps (precedence:
|
||||
* 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
|
||||
* directory: the session cwd, relative paths, and workspace instructions resolve
|
||||
* from it, so `dsh` acts on whatever project it is launched in. Session storage
|
||||
* is the exception — it lives under the Harness home so `/resume` reaches every
|
||||
* workspace, and an in-place resume enters the selected session's own directory.
|
||||
* `dsh meta` is the one exception — it makes this harness
|
||||
* checkout the workspace. `dsh upgrade` is a fresh session whose
|
||||
* first turn auto-invokes a bundled skill. After boot, the agent's system
|
||||
* prompt is told the path to this harness checkout so it can find its own
|
||||
* source.
|
||||
* @module @deepseek-ai/dsh/tui
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import {
|
||||
addHarnessSourceSection,
|
||||
boot,
|
||||
installFailLoud,
|
||||
loadOverlayPatches,
|
||||
loadPersonalPatches,
|
||||
resolveConfigPath,
|
||||
watchPersonalPatches,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
import type { PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { configHasTelemetryRow, resolveTelemetryPatch } from './app-cli-entry.ts'
|
||||
import { SESSION_QUERY_SQLITE_PATH_KEY } from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import { CONFIGURED_AGENT_IDENTITIES_KEY } from '@deepseek-ai/dsh-agent-loop'
|
||||
import type { Context } from 'cordis'
|
||||
import {
|
||||
INITIAL_SKILL_KEY,
|
||||
MAIN_SESSION_ID_KEY,
|
||||
TUI_GOODBYE_MESSAGE_KEY,
|
||||
type MainSessionIdentity,
|
||||
type TuiResumeHost,
|
||||
} from '@deepseek-ai/dsh-tui'
|
||||
import {
|
||||
apply as applyTuiFirstRunWelcome,
|
||||
hasTuiFirstRunWelcomeAcknowledgement,
|
||||
inject as tuiFirstRunWelcomeInject,
|
||||
name as tuiFirstRunWelcomeName,
|
||||
needsTuiFirstRunWelcomeAsciiArt,
|
||||
} from './tui-onboarding/tui-first-run-welcome.ts'
|
||||
import {
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
|
||||
} from './tui-onboarding/tui-first-run-welcome-copy.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
// The shared core every `dsh` surface mounts, and the TUI's own overlay over
|
||||
// it. Both the source tree (apps/cli/src) and the bundled bin (apps/cli/lib)
|
||||
// sit one directory under apps/cli, so each resolves with the same hop.
|
||||
const BASE_CONFIG = fileURLToPath(new URL('../config/base.cordis.yml', import.meta.url))
|
||||
const TUI_OVERLAY = fileURLToPath(new URL('../config/tui.cordis.yml', import.meta.url))
|
||||
|
||||
// The `agents` entry in tui.cordis.yml the TUI drives; the launcher binds its
|
||||
// session identity by this config id.
|
||||
const MAIN_AGENT_ID = 'main'
|
||||
|
||||
/** Per-process filename of the disposable `/resume` index. */
|
||||
const SESSION_QUERY_DB = `session-query-${String(process.pid)}-${randomUUID()}.db`
|
||||
|
||||
// The harness checkout root: three hops up from apps/cli/{src,lib}, resolved
|
||||
// from this bin's location so it holds however `dsh` is launched (a PATH
|
||||
// symlink, an arbitrary cwd). The agent is told where its own source lives.
|
||||
/** The harness checkout used as the `dsh meta` workspace and source prompt path. */
|
||||
export const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
|
||||
/* v8 ignore start -- composition over the unit-tested dsh-app-boot helpers;
|
||||
the CLI PTY smoke drives this path end to end, personal overlay included */
|
||||
/**
|
||||
* Run the interactive TUI from the invoking directory.
|
||||
* @param config - an overlay patch list applied over the shared base and the
|
||||
* TUI overlay, REPLACING the personal `~/.dsh/config.yaml` so a named tree never
|
||||
* inherits the user's route, or `undefined` to use the personal overlay;
|
||||
* already parsed from `--config`.
|
||||
* @param resumeSessionId - a persisted session id to resume, or `undefined` to
|
||||
* mint a fresh one; already parsed and non-empty-validated from `--resume`.
|
||||
* Either way the resulting identity reaches the booted app through
|
||||
* {@link CONFIGURED_AGENT_IDENTITIES_KEY}, so no config key selects the session
|
||||
* and an overlay replacing the agent row cannot drop it.
|
||||
* @param workspace - a directory to make the workspace instead of the invoking
|
||||
* one, or `undefined` to keep the cwd. Only `dsh meta` passes it.
|
||||
* @param initialSkill - a bundled skill to auto-invoke as a fresh session's
|
||||
* first turn, or `undefined`. Set only by `dsh upgrade` and
|
||||
* ignored on a resume, so it never re-fires; reaches the app through
|
||||
* {@link INITIAL_SKILL_KEY}.
|
||||
* @param configReplace - a config path to boot as the ENTIRE tree, bypassing the
|
||||
* shared base, the TUI overlay, and the personal overlay alike, or `undefined`
|
||||
* to compose them; already parsed from `--config-replace`.
|
||||
*/
|
||||
export async function runTui(
|
||||
config: string | undefined,
|
||||
resumeSessionId: string | undefined,
|
||||
workspace?: string,
|
||||
initialSkill?: string,
|
||||
configReplace?: string,
|
||||
): Promise<void> {
|
||||
// Refuse pipes BEFORE booting: a compose-time throw inside the Loader tree
|
||||
// is logged per-entry rather than rethrown, so a piped launch would
|
||||
// otherwise settle into an idle UI-less process instead of exiting nonzero.
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
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)
|
||||
}
|
||||
// The bin already loaded the invoking directory's .env, and that is the
|
||||
// whole environment: $DSH_HOME/.env is credentials-local's writable store,
|
||||
// and hoisting it would make every stored key read as a read-only ambient
|
||||
// override on the next run — unrotatable from the TUI or the web page.
|
||||
// The environment is settled, so switching the workspace here cannot alter
|
||||
// its precedence. The cwd IS the workspace seam: the shipped config
|
||||
// resolves the session cwd and the HMR watch root from it, so one chdir moves
|
||||
// both together. Sessions themselves live under the Harness home so `/resume`
|
||||
// spans every workspace, and are unaffected by this chdir.
|
||||
if (workspace !== undefined) process.chdir(workspace)
|
||||
const dshHome = resolveDshHome()
|
||||
const showFirstRunWelcome = !await hasTuiFirstRunWelcomeAcknowledgement(
|
||||
dshHome,
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
|
||||
)
|
||||
process.env.DSH_BUNDLED_SKILL_DIR = join(SOURCE_ROOT, 'skills')
|
||||
// The in-place `/resume` handoff re-execs `dsh` with a normalized `--resume`
|
||||
// flag, so the resumed process rehydrates through this same intake. The
|
||||
// selected session may belong to another workspace, so the handoff also enters
|
||||
// that directory. The host is offered only when Node exposes `process.execve`
|
||||
// and knows its own entry.
|
||||
const resolvedConfig = config === undefined ? undefined : resolve(config)
|
||||
const resolvedConfigReplace = configReplace === undefined ? undefined : resolve(configReplace)
|
||||
const entry = process.argv[1]
|
||||
const execve = process.execve?.bind(process)
|
||||
const app: { current?: Context } = {}
|
||||
// The Loader mounts entries concurrently, so `ui-tui` can already hold the
|
||||
// terminal (raw mode, bracketed paste, keyboard protocol) when something
|
||||
// else fails. A config-tree failure settles through `boot`, which disposes
|
||||
// the tree itself; this release covers the rejections `boot` cannot see — a
|
||||
// plugin's detached async work rejecting while mounting is still in flight
|
||||
// or after the tree settled. Disposing the tree runs the TUI's own shutdown,
|
||||
// which stops the terminal and hands the shell back; without it such a
|
||||
// failure returns to a corrupted prompt. `app.current` is captured from
|
||||
// boot's `prepare` hook, so it holds the root context for the whole mounting
|
||||
// window rather than only after boot resolves.
|
||||
installFailLoud(NAME, process, async () => {
|
||||
await app.current?.fiber.dispose()
|
||||
})
|
||||
// Resume always enters the default surface because meta rejects
|
||||
// parent options, including `--resume`. The resumed session already persists
|
||||
// its cwd.
|
||||
const resumeArgs = (sessionId: string): string[] => [
|
||||
`--resume=${sessionId}`,
|
||||
// Both config flags must survive the handoff: resuming into a different
|
||||
// tree than the session was created in would silently change the agent.
|
||||
...resolvedConfig !== undefined ? ['--config', resolvedConfig] : [],
|
||||
...resolvedConfigReplace !== undefined ? ['--config-replace', resolvedConfigReplace] : [],
|
||||
]
|
||||
// Mint the fresh id here rather than in the app bundle: the exit line names
|
||||
// the session to resume, so the launcher must know it before the tree boots.
|
||||
const identity: MainSessionIdentity = resumeSessionId === undefined
|
||||
? { id: SessionId(`main-session-${randomUUID()}`), resume: false }
|
||||
: { id: SessionId(resumeSessionId), resume: true }
|
||||
const goodbye = `To resume this session: ${NAME} ${resumeArgs(identity.id).join(' ')}`
|
||||
const resumeHost: TuiResumeHost | undefined = entry === undefined || execve === undefined ? undefined : {
|
||||
async handoff(sessionId, cwd): Promise<never> {
|
||||
const current = app.current
|
||||
if (current === undefined) throw new Error(`${NAME}: app boot has not completed`)
|
||||
const nextArgv = [
|
||||
process.execPath,
|
||||
...process.execArgv,
|
||||
entry,
|
||||
...resumeArgs(sessionId),
|
||||
]
|
||||
// `execve` inherits the cwd, and the target session may belong to another
|
||||
// workspace. Enter it BEFORE teardown commits: an unreachable directory
|
||||
// (deleted, unreadable) must reject while the caller can still restore the
|
||||
// terminal, and a chdir after disposal would have no owner to report to.
|
||||
try {
|
||||
process.chdir(cwd)
|
||||
} catch (error) {
|
||||
throw new Error(`${NAME}: cannot resume in "${cwd}": ${String(error)}`)
|
||||
}
|
||||
try {
|
||||
await current.fiber.dispose()
|
||||
execve(process.execPath, nextArgv, process.env)
|
||||
throw new Error('process replacement returned unexpectedly')
|
||||
} catch (error) {
|
||||
process.stderr.write(`${NAME}: resume handoff failed after terminal release: ${String(error)}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
// One include of the shared base, with every overlay applied as a sibling
|
||||
// patch list: patches never cross an include boundary, so stacking these as
|
||||
// nested includes would silently stop reaching base rows. Later lists win.
|
||||
//
|
||||
// `--config` REPLACES the personal overlay rather than layering under it: an
|
||||
// explicitly named tree must not inherit `~/.dsh/config.yaml`'s route, or a
|
||||
// demo or test config would silently run on the user's provider and model.
|
||||
// `--config-replace` additionally discards the base and the surface overlay.
|
||||
const replaceTree = configReplace !== undefined
|
||||
const bootConfig = resolvedConfigReplace === undefined ? BASE_CONFIG : resolveConfigPath(resolvedConfigReplace, undefined)
|
||||
// Same opt-out semantics as the web surface (resolveTelemetryPatch: any
|
||||
// non-empty value disables; setting the switch against a tree without the
|
||||
// row fails loud rather than silently no-opping a privacy switch). The row
|
||||
// presence is checked against the tree actually booting, so a
|
||||
// --config-replace tree is judged on its own rows, not the shipped base's.
|
||||
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, configHasTelemetryRow(bootConfig))
|
||||
const composePatches = (personalPatches: PatchOptions[]): PatchOptions[] => [
|
||||
...replaceTree ? [] : [
|
||||
...loadOverlayPatches(NAME, TUI_OVERLAY),
|
||||
...resolvedConfig === undefined
|
||||
? personalPatches
|
||||
: loadOverlayPatches(NAME, resolveConfigPath(resolvedConfig, undefined)),
|
||||
],
|
||||
...telemetryPatch === undefined ? [] : [telemetryPatch],
|
||||
]
|
||||
const patches = composePatches(loadPersonalPatches(NAME) ?? [])
|
||||
const queryIndexPath = join(tmpdir(), SESSION_QUERY_DB)
|
||||
const ctx = await boot(
|
||||
NAME,
|
||||
bootConfig,
|
||||
patches,
|
||||
(hostCtx) => {
|
||||
// Runs after the Loader installs and before any config-tree entry mounts,
|
||||
// so the fail-loud release hook can reach the tree for the whole window in
|
||||
// which an entry may reject.
|
||||
app.current = hostCtx
|
||||
// The launcher owns session identity and the exit line: a config-mounted
|
||||
// app bundle reads both from these slots, so no cordis.yml key can drop
|
||||
// resume.
|
||||
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 sees every workspace.
|
||||
// The bundle treats the slot as opaque.
|
||||
// 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
|
||||
// the session identity or desynchronise the two.
|
||||
hostCtx.provide(CONFIGURED_AGENT_IDENTITIES_KEY, { [MAIN_AGENT_ID]: identity })
|
||||
// The query database is a disposable derived index with single-process
|
||||
// ownership. Keep it process-local while it indexes the shared logs.
|
||||
hostCtx.provide(SESSION_QUERY_SQLITE_PATH_KEY, queryIndexPath)
|
||||
hostCtx.effect(() => async () => {
|
||||
await Promise.all([
|
||||
rm(queryIndexPath, { force: true }),
|
||||
rm(`${queryIndexPath}-wal`, { force: true }),
|
||||
rm(`${queryIndexPath}-shm`, { force: true }),
|
||||
])
|
||||
}, `${SESSION_QUERY_SQLITE_PATH_KEY}.cleanup`)
|
||||
if (resumeHost !== undefined) hostCtx.provide('tuiResumeHost', resumeHost)
|
||||
// Seed the first turn only for a fresh session, so resuming never
|
||||
// re-invokes the skill.
|
||||
if (initialSkill !== undefined && resumeSessionId === undefined) {
|
||||
hostCtx.provide(INITIAL_SKILL_KEY, initialSkill)
|
||||
}
|
||||
},
|
||||
)
|
||||
// The shipped tree includes HMR and keeps personal config live. An explicit
|
||||
// --config tree replaces the personal overlay (so there is nothing to keep
|
||||
// live), and a --config-replace or HMR-less tree remains a valid composition
|
||||
// that still receives the startup overlay but deliberately has no hidden
|
||||
// watcher.
|
||||
if (resolvedConfig === undefined && !replaceTree && ctx.get('hmr') !== undefined) {
|
||||
await watchPersonalPatches(ctx, { binName: NAME, compose: composePatches })
|
||||
}
|
||||
app.current = ctx
|
||||
addHarnessSourceSection(ctx, SOURCE_ROOT)
|
||||
if (showFirstRunWelcome) {
|
||||
await ctx.plugin({
|
||||
name: tuiFirstRunWelcomeName,
|
||||
inject: tuiFirstRunWelcomeInject,
|
||||
apply: applyTuiFirstRunWelcome,
|
||||
}, {
|
||||
dshHome,
|
||||
asciiArt: needsTuiFirstRunWelcomeAsciiArt(),
|
||||
})
|
||||
}
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
@@ -14,7 +14,7 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tool-bash'
|
||||
import { AppCLIEntry } from './app-cli-entry.ts'
|
||||
|
||||
// The shared core every `dsh` surface mounts, plus this surface's overlay over it.
|
||||
// The shipped base plus the Web application's overlay.
|
||||
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))
|
||||
const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { parseDshArgs } from '../src/args.ts'
|
||||
|
||||
const parse = (argv: string[], experimentalEnv = false) => parseDshArgs(argv, '1.2.3', experimentalEnv)
|
||||
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[], experimentalEnv = false): number {
|
||||
/** Capture the process exit code while muting Commander's 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, experimentalEnv)
|
||||
parse(argv)
|
||||
throw new Error(`expected ${JSON.stringify(argv)} to exit`)
|
||||
} catch {
|
||||
return exit.mock.calls.at(-1)?.[0] as number
|
||||
@@ -24,102 +21,52 @@ function exitCode(argv: string[], experimentalEnv = false): number {
|
||||
afterEach(() => { vi.restoreAllMocks() })
|
||||
|
||||
describe('parseDshArgs', () => {
|
||||
it('routes each mode by its shape: default TUI, -p headless, experimental and web subcommands', () => {
|
||||
expect(parse([])).toEqual({ mode: 'tui' })
|
||||
expect(parse(['--config', 'custom.yml'])).toEqual({ mode: 'tui', config: 'custom.yml' })
|
||||
expect(parse(['--config-replace', 'tree.yml'])).toEqual({ mode: 'tui', configReplace: 'tree.yml' })
|
||||
expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
|
||||
it('routes the required raw config, one-shot prompt, and Web command', () => {
|
||||
expect(parse(['--config', 'custom.yml'])).toEqual({ mode: 'config', config: 'custom.yml' })
|
||||
expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
|
||||
// Experimental subcommands run under the per-invocation flag or the env opt-in.
|
||||
expect(parse(['meta', '--experimental'])).toEqual({ mode: 'meta' })
|
||||
expect(parse(['meta'], true)).toEqual({ mode: 'meta' })
|
||||
// Bare `web` carries no host/port: the shipped Web overlay owns the default.
|
||||
expect(parse(['web'])).toEqual({ mode: 'web', dev: false })
|
||||
expect(parse(['web', '--config', 'web.yml'])).toEqual({ mode: 'web', dev: false, config: 'web.yml' })
|
||||
// Host/port are unvalidated pass-throughs (the webserver schema gates them
|
||||
// at boot); the adapter only coerces the port string to a number.
|
||||
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
|
||||
.toEqual({ mode: 'web', host: '0.0.0.0', port: 8080, dev: true, workspaceRoot: '/w' })
|
||||
// Guided fresh-session entries carry nothing: bare mode discriminant only.
|
||||
expect(parse(['upgrade', '--experimental'])).toEqual({ mode: 'upgrade' })
|
||||
expect(parse(['upgrade'], true)).toEqual({ mode: 'upgrade' })
|
||||
// --trusted-host is variadic and repeatable; authorities pass through unvalidated.
|
||||
expect(parse(['web', '--trusted-host', 'harness.internal:3080', 'lab.internal', '--trusted-host', '10.0.0.9']))
|
||||
.toEqual({ mode: 'web', dev: false, trustedHosts: ['harness.internal:3080', 'lab.internal', '10.0.0.9'] })
|
||||
})
|
||||
|
||||
it('routes the dump flags per surface: composed with the user layer, or shipped only', () => {
|
||||
expect(parse(['--dump-config'])).toEqual({ mode: 'dump-config', surface: 'tui', defaultOnly: false })
|
||||
expect(parse(['--dump-config', '--config', 'c.yml']))
|
||||
.toEqual({ mode: 'dump-config', surface: 'tui', defaultOnly: false, config: 'c.yml' })
|
||||
expect(parse(['--dump-default-config'])).toEqual({ mode: 'dump-config', surface: 'tui', defaultOnly: true })
|
||||
expect(parse(['web', '--dump-config'])).toEqual({ mode: 'dump-config', surface: 'web', defaultOnly: false })
|
||||
it('routes raw and Web config dumps', () => {
|
||||
expect(parse(['--config', 'c.yml', '--dump-config']))
|
||||
.toEqual({ mode: 'dump-config', surface: 'config', defaultOnly: false, config: 'c.yml' })
|
||||
expect(parse(['--dump-default-config']))
|
||||
.toEqual({ mode: 'dump-config', surface: 'config', defaultOnly: true })
|
||||
expect(parse(['web', '--dump-config']))
|
||||
.toEqual({ mode: 'dump-config', surface: 'web', defaultOnly: false })
|
||||
expect(parse(['web', '--dump-config', '--config', 'w.yml']))
|
||||
.toEqual({ mode: 'dump-config', surface: 'web', defaultOnly: false, config: 'w.yml' })
|
||||
expect(parse(['web', '--dump-default-config'])).toEqual({ mode: 'dump-config', surface: 'web', defaultOnly: true })
|
||||
// The two dump flags contradict each other; boot-only flags alongside a
|
||||
// dump would be silently ignored; the shipped tree takes no user overlay.
|
||||
expect(exitCode(['--dump-config', '--dump-default-config'])).toBe(1)
|
||||
expect(exitCode(['--dump-default-config', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['--dump-config', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['--dump-config', '-p', 'task'])).toBe(1)
|
||||
expect(exitCode(['--dump-config', '--config-replace', 'tree.yml'])).toBe(1)
|
||||
expect(exitCode(['web', '--dump-config', '--dump-default-config'])).toBe(1)
|
||||
expect(exitCode(['web', '--dump-default-config', '--config', 'w.yml'])).toBe(1)
|
||||
// A leaked dump flag on a subcommand that has none is a mistyped invocation.
|
||||
expect(exitCode(['meta', '--experimental', '--dump-config'])).toBe(1)
|
||||
expect(exitCode(['upgrade', '--experimental', '--dump-config'])).toBe(1)
|
||||
expect(parse(['web', '--dump-default-config']))
|
||||
.toEqual({ mode: 'dump-config', surface: 'web', defaultOnly: true })
|
||||
})
|
||||
|
||||
it('exits nonzero instead of silently starting fresh or dropping inputs', () => {
|
||||
// Empty resume/prompt would be swallowed downstream; --prompt mixed with
|
||||
// TUI inputs must not lose them. (Bad host/port are gated by the webserver
|
||||
// schema at boot, not here.)
|
||||
expect(exitCode(['--resume='])).toBe(1)
|
||||
expect(exitCode(['-p', ''])).toBe(1)
|
||||
expect(exitCode(['-p', 'x', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['-p', 'x', '--config-replace', 'tree.yml'])).toBe(1)
|
||||
expect(exitCode(['--config', 'c.yml', '--config-replace', 'tree.yml'])).toBe(1)
|
||||
expect(exitCode(['-p', 'x', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['--bogus'])).toBe(1)
|
||||
expect(exitCode(['bogus-positional'])).toBe(1)
|
||||
// A default-surface flag on either side of `web` leaks into program.opts()
|
||||
// but the web subcommand shares none of them: reject rather than serve.
|
||||
expect(exitCode(['web', '-p', 'task'])).toBe(1)
|
||||
expect(exitCode(['web', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['--config', 'c.yml', 'web'])).toBe(1)
|
||||
expect(exitCode(['--config-replace', 'tree.yml', 'web'])).toBe(1)
|
||||
// Same rule for each subcommand that shares no option with the default
|
||||
// surface, so a leaked flag is a typo, not something to ignore.
|
||||
// `meta` fixes its own config tree and always starts fresh,
|
||||
// so every default-surface option is rejected.
|
||||
expect(exitCode(['meta', '--experimental', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['meta', '--experimental', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['meta', '--experimental', '--config-replace', 'tree.yml'])).toBe(1)
|
||||
expect(exitCode(['meta', '--experimental', '-p', 'task'])).toBe(1)
|
||||
// `upgrade` takes no options beyond the gate: any leaked default-surface
|
||||
// flag is a mistyped invocation, not a silently-dropped input.
|
||||
expect(exitCode(['upgrade', '--experimental', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['upgrade', '--experimental', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['-p', 'task', 'upgrade', '--experimental'])).toBe(1)
|
||||
// The pre-release command names have no compatibility aliases.
|
||||
expect(exitCode(['experimental-meta'])).toBe(1)
|
||||
expect(exitCode(['experimental-upgrade'])).toBe(1)
|
||||
})
|
||||
|
||||
it('gates experimental subcommands behind --experimental or the env opt-in', () => {
|
||||
// Bare `meta`/`upgrade` without either opt-in must fail loud, not run.
|
||||
it('rejects missing config, removed commands, and contradictory inputs', () => {
|
||||
expect(exitCode([])).toBe(1)
|
||||
expect(exitCode(['tui'])).toBe(1)
|
||||
expect(exitCode(['meta'])).toBe(1)
|
||||
expect(exitCode(['upgrade'])).toBe(1)
|
||||
// A leaked default-surface flag stays a typo even when the gate is passed
|
||||
// by the environment alone.
|
||||
expect(exitCode(['meta', '--resume', 's'], true)).toBe(1)
|
||||
// The flag and the env opt-in may coexist.
|
||||
expect(parse(['meta', '--experimental'], true)).toEqual({ mode: 'meta' })
|
||||
expect(parse(['upgrade', '--experimental'], true)).toEqual({ mode: 'upgrade' })
|
||||
expect(exitCode(['--dump-config'])).toBe(1)
|
||||
expect(exitCode(['--dump-config', '--dump-default-config', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['--dump-default-config', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['--dump-config', '--config', 'c.yml', '-p', 'task'])).toBe(1)
|
||||
expect(exitCode(['-p', ''])).toBe(1)
|
||||
expect(exitCode(['--config='])).toBe(1)
|
||||
expect(exitCode(['-p', 'x', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['--bogus'])).toBe(1)
|
||||
expect(exitCode(['bogus-positional'])).toBe(1)
|
||||
expect(exitCode(['web', '-p', 'task'])).toBe(1)
|
||||
expect(exitCode(['--config', 'c.yml', 'web'])).toBe(1)
|
||||
expect(exitCode(['web', '--dump-config', '--dump-default-config'])).toBe(1)
|
||||
expect(exitCode(['web', '--dump-default-config', '--config', 'w.yml'])).toBe(1)
|
||||
expect(exitCode(['web', '--config='])).toBe(1)
|
||||
})
|
||||
|
||||
it('exits 0 for --help (disclosing web) and --version', () => {
|
||||
it('exits 0 for help and version', () => {
|
||||
expect(exitCode(['--help'])).toBe(0)
|
||||
expect(exitCode(['--version'])).toBe(0)
|
||||
})
|
||||
|
||||
@@ -1,32 +1,16 @@
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { execa } from 'execa'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
* Published-entry smoke for the `dsh` bin: run the built `lib/bin.js` under
|
||||
* plain Node (no tsx) with PIPED stdio and assert the TUI refuses to boot.
|
||||
* `dsh` is the sole terminal front door; the TUI owns no non-TTY fallback, so a
|
||||
* piped launch must exit nonzero with a stderr pointer at the one-shot `-p`
|
||||
* mode. The guard fires inside `runTui` BEFORE the Loader resolves the config
|
||||
* tree — a compose-time throw inside the tree is logged per-entry, not
|
||||
* rethrown, so without this guard a piped launch would settle into an idle
|
||||
* UI-less process. The bin resolves its workspace deps through the repo's
|
||||
* node_modules, so no external consumer is assembled; missing-config fail-loud
|
||||
* and full-boot coverage for the shared dsh-app-boot glue live in cli-demo's
|
||||
* built-bin suite, and interactive TTY behavior is PTY-covered by
|
||||
* apps/cli/tests. Skips before the bin is built.
|
||||
*/
|
||||
|
||||
/** Published-entry acceptance for raw argument errors and boot-free config dumps. */
|
||||
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
|
||||
const rawOverlay = fileURLToPath(new URL('./fixtures/raw-overlay.cordis.yml', import.meta.url))
|
||||
const rawInvalidProvider = fileURLToPath(new URL('./fixtures/raw-invalid-provider.cordis.yml', import.meta.url))
|
||||
|
||||
/**
|
||||
* Run the built bin with PIPED stdio (stdin closed at EOF); resolve with output
|
||||
* + exit code. `env` isolates the Harness home for surfaces that read it.
|
||||
*/
|
||||
async function runBuiltBin(
|
||||
args: readonly string[] = [],
|
||||
env: Record<string, string> = {},
|
||||
@@ -44,106 +28,178 @@ async function runBuiltBin(
|
||||
return { stdout: result.stdout, code: result.exitCode ?? -1, stderr: result.stderr }
|
||||
}
|
||||
|
||||
async function waitForFile(file: string): Promise<void> {
|
||||
const deadline = Date.now() + 20_000
|
||||
while (!existsSync(file)) {
|
||||
if (Date.now() >= deadline) throw new Error(`dsh raw lifecycle marker did not appear: ${file}`)
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
}
|
||||
}
|
||||
|
||||
interface RawLifecycleFixture {
|
||||
home: string
|
||||
ready: string
|
||||
settled: string
|
||||
disposed: string
|
||||
overlay: string
|
||||
}
|
||||
|
||||
function createRawLifecycleFixture(): RawLifecycleFixture {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-raw-lifecycle-'))
|
||||
const ready = join(home, 'ready')
|
||||
const settled = join(home, 'settled')
|
||||
const disposed = join(home, 'disposed')
|
||||
const plugin = join(home, 'lifecycle.mjs')
|
||||
const overlay = join(home, 'overlay.cordis.yml')
|
||||
writeFileSync(plugin, [
|
||||
"import { writeFileSync } from 'node:fs'",
|
||||
"export const name = 'raw-lifecycle-fixture'",
|
||||
"export const inject = ['sessionQuery']",
|
||||
'export function apply(ctx) {',
|
||||
' let active = true',
|
||||
" writeFileSync(process.env.RAW_READY_FILE, 'ready')",
|
||||
' void ctx.loader.await().then(() => {',
|
||||
" if (active) writeFileSync(process.env.RAW_SETTLED_FILE, 'settled')",
|
||||
' })',
|
||||
' ctx.effect(() => () => {',
|
||||
' active = false',
|
||||
" writeFileSync(process.env.RAW_DISPOSED_FILE, 'disposed')",
|
||||
' })',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'))
|
||||
writeFileSync(overlay, [
|
||||
'- insert:',
|
||||
' - id: raw-lifecycle-fixture',
|
||||
` name: ${pathToFileURL(plugin).href}`,
|
||||
'',
|
||||
].join('\n'))
|
||||
return { home, ready, settled, disposed, overlay }
|
||||
}
|
||||
|
||||
function startRawLifecycle(fixture: RawLifecycleFixture) {
|
||||
return execa(process.execPath, [dshBin, '--config', fixture.overlay], {
|
||||
cwd: fixture.home,
|
||||
input: '',
|
||||
reject: false,
|
||||
env: {
|
||||
DSH_HOME: fixture.home,
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
RAW_READY_FILE: fixture.ready,
|
||||
RAW_SETTLED_FILE: fixture.settled,
|
||||
RAW_DISPOSED_FILE: fixture.disposed,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {
|
||||
it('refuses pipes LOUD (non-zero exit + stderr) before booting the Loader', async () => {
|
||||
const { stdout, code, stderr } = await runBuiltBin()
|
||||
expect(code).not.toBe(0)
|
||||
expect(stderr).toContain('requires stdin and stdout to be interactive TTYs')
|
||||
expect(stderr).toContain('dsh -p')
|
||||
// The refusal happens before any plugin mounts: stdout stays silent.
|
||||
expect(stdout).toBe('')
|
||||
it('requires --config for the raw command and rejects removed commands', async () => {
|
||||
const bare = await runBuiltBin()
|
||||
expect(bare.code).toBe(1)
|
||||
expect(bare.stdout).toBe('')
|
||||
expect(bare.stderr).toContain('--config <path> is required')
|
||||
const help = await runBuiltBin(['--help'])
|
||||
expect(help.code).toBe(0)
|
||||
expect(help.stdout).toContain('dsh --config ./app.cordis.yml')
|
||||
expect(help.stdout).not.toMatch(/^\s+(?:tui|meta|upgrade)\b/mu)
|
||||
for (const command of ['tui', 'meta', 'upgrade']) {
|
||||
const removed = await runBuiltBin([command])
|
||||
expect(removed.code).toBe(1)
|
||||
expect(removed.stderr).not.toContain('experimental')
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
describe('experimental subcommand gate', () => {
|
||||
// The gate has two halves: a per-invocation --experimental flag parsed by
|
||||
// Commander and an env opt-in read by bin.ts as exactly '1'. Passing the
|
||||
// gate is proven by reaching the NEXT failure — the TUI's piped-stdio
|
||||
// refusal — instead of the gate diagnostic.
|
||||
it('rejects bare `meta`/`upgrade` LOUD, naming both opt-ins', async () => {
|
||||
for (const command of ['meta', 'upgrade']) {
|
||||
const { code, stderr } = await runBuiltBin([command], { DSH_EXPERIMENTAL: '' })
|
||||
expect(code).toBe(1)
|
||||
expect(stderr).toContain(`${command} is experimental; pass --experimental or set DSH_EXPERIMENTAL=1`)
|
||||
}
|
||||
}, 30_000)
|
||||
it('reports a raw overlay boot failure without hanging', async () => {
|
||||
const result = await runBuiltBin(['--config', rawInvalidProvider], {
|
||||
DEEPSEEK_API_KEY: 'keyless-invalid-config',
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
})
|
||||
expect(result.code).toBe(1)
|
||||
expect(result.stdout).toBe('')
|
||||
expect(result.stderr).toContain('llm-pi-ai')
|
||||
}, 30_000)
|
||||
|
||||
it('admits --experimental and DSH_EXPERIMENTAL=1, but not other env values', async () => {
|
||||
const flagged = await runBuiltBin(['meta', '--experimental'], { DSH_EXPERIMENTAL: '' })
|
||||
expect(flagged.stderr).toContain('requires stdin and stdout to be interactive TTYs')
|
||||
const env = await runBuiltBin(['meta'], { DSH_EXPERIMENTAL: '1' })
|
||||
expect(env.stderr).toContain('requires stdin and stdout to be interactive TTYs')
|
||||
// The env opt-in is exact: '0' (or any other value) does not enable.
|
||||
const zero = await runBuiltBin(['meta'], { DSH_EXPERIMENTAL: '0' })
|
||||
expect(zero.code).toBe(1)
|
||||
expect(zero.stderr).toContain('meta is experimental')
|
||||
}, 30_000)
|
||||
})
|
||||
it('applies an inserted raw plugin and disposes it on a startup-time signal', async () => {
|
||||
const fixture = createRawLifecycleFixture()
|
||||
const child = startRawLifecycle(fixture)
|
||||
try {
|
||||
await waitForFile(fixture.ready)
|
||||
child.kill('SIGTERM')
|
||||
const result = await child
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.signal).toBeUndefined()
|
||||
expect(existsSync(fixture.disposed)).toBe(true)
|
||||
} finally {
|
||||
child.kill('SIGKILL')
|
||||
rmSync(fixture.home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
describe('dsh --dump-config', () => {
|
||||
it('fully settles a valid raw overlay and disposes it on a signal', async () => {
|
||||
const fixture = createRawLifecycleFixture()
|
||||
const child = startRawLifecycle(fixture)
|
||||
try {
|
||||
await waitForFile(fixture.settled)
|
||||
child.kill('SIGTERM')
|
||||
const result = await child
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.signal).toBeUndefined()
|
||||
expect(existsSync(fixture.disposed)).toBe(true)
|
||||
} finally {
|
||||
child.kill('SIGKILL')
|
||||
rmSync(fixture.home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
describe('config dump', () => {
|
||||
let home: string
|
||||
beforeEach(() => { home = mkdtempSync(join(tmpdir(), 'dsh-dump-bin-')) })
|
||||
afterEach(() => { rmSync(home, { recursive: true, force: true }) })
|
||||
|
||||
it('prints the shipped TUI composition without booting or needing a TTY', async () => {
|
||||
it('prints the shipped base without a user layer', async () => {
|
||||
const { stdout, code, stderr } = await runBuiltBin(['--dump-default-config'], { DSH_HOME: home })
|
||||
expect(code).toBe(0)
|
||||
expect(stderr).toBe('')
|
||||
// Base rows composed with the TUI overlay's surface values, `!!js`
|
||||
// expressions verbatim (unevaluated), and TUI-only inserted rows present.
|
||||
expect(stdout).toContain("name: '@deepseek-ai/dsh-agent-loop'")
|
||||
expect(stdout).toContain('model: deepseek-v4-pro')
|
||||
expect(stdout).toContain('cwd: !!js process.cwd()')
|
||||
expect(stdout).toContain("name: '@deepseek-ai/dsh-tui'")
|
||||
expect(stdout).not.toMatch(/name: ['"]@deepseek-ai\/dsh-invariants['"]/)
|
||||
expect(stdout).not.toMatch(/name: ['"]@deepseek-ai\/dsh-[^'"]+\/invariant['"]/)
|
||||
expect(stdout).toContain([
|
||||
'- id: tool-web',
|
||||
" name: '@deepseek-ai/dsh-tool-web'",
|
||||
' config:',
|
||||
' fetch: false',
|
||||
' searchTimeoutMs: 60000',
|
||||
].join('\n'))
|
||||
// Provenance comment separators name each section's source file.
|
||||
expect(stdout).toContain('agents: []')
|
||||
expect(stdout).toContain('# == base.cordis.yml')
|
||||
expect(stdout).toContain('# == base.cordis.yml, patched by tui.cordis.yml')
|
||||
expect(stdout).toContain('# == tui.cordis.yml')
|
||||
}, 30_000)
|
||||
|
||||
it('layers the personal overlay in --dump-config and reports an unmatched patch on stderr', async () => {
|
||||
it('composes the required raw overlay directly over the base', async () => {
|
||||
writeFileSync(join(home, 'config.yaml'), [
|
||||
'- id: agent-loop',
|
||||
' config:',
|
||||
' agents:',
|
||||
' - id: main',
|
||||
' provider: custom-provider',
|
||||
' model: custom-model',
|
||||
'- id: only-on-web',
|
||||
' config:',
|
||||
' value: 1',
|
||||
' - id: personal',
|
||||
' provider: personal-provider',
|
||||
' model: personal-model',
|
||||
'',
|
||||
].join('\n'))
|
||||
const { stdout, code, stderr } = await runBuiltBin(['--dump-config'], { DSH_HOME: home })
|
||||
const { stdout, code, stderr } = await runBuiltBin(
|
||||
['--config', rawOverlay, '--dump-config'],
|
||||
{ DSH_HOME: home },
|
||||
)
|
||||
expect(code).toBe(0)
|
||||
expect(stdout).toContain('provider: custom-provider')
|
||||
expect(stdout).not.toContain('model: deepseek-v4-pro')
|
||||
// The personal layer appears in the patched row's provenance and the
|
||||
// skipped-patch warning carries its label.
|
||||
expect(stdout).toContain(`patched by tui.cordis.yml, ${join(home, 'config.yaml')}`)
|
||||
expect(stderr).toContain('patch: entry "only-on-web" not found')
|
||||
|
||||
// The shipped view ignores the personal overlay entirely.
|
||||
const shipped = await runBuiltBin(['--dump-default-config'], { DSH_HOME: home })
|
||||
expect(shipped.stdout).not.toContain('custom-provider')
|
||||
expect(shipped.stdout).toContain('model: deepseek-v4-pro')
|
||||
expect(stdout).toContain('provider: configured-provider')
|
||||
expect(stdout).not.toContain('personal-provider')
|
||||
expect(stdout).toContain(`patched by ${rawOverlay}`)
|
||||
expect(stderr).toContain('patch: entry "absent-row" not found')
|
||||
}, 30_000)
|
||||
|
||||
it('composes the web overlay for `dsh web --dump-config`', async () => {
|
||||
it('keeps the Web overlay and personal layer on the Web command', async () => {
|
||||
writeFileSync(join(home, 'config.yaml'), [
|
||||
'- id: agent-loop',
|
||||
' config:',
|
||||
' agents:',
|
||||
' - id: personal',
|
||||
' provider: personal-provider',
|
||||
' model: personal-model',
|
||||
'',
|
||||
].join('\n'))
|
||||
const { stdout, code } = await runBuiltBin(['web', '--dump-config'], { DSH_HOME: home })
|
||||
expect(code).toBe(0)
|
||||
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
|
||||
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-tui'")
|
||||
expect(stdout).not.toMatch(/name: ['"]@deepseek-ai\/dsh-invariants['"]/)
|
||||
expect(stdout).not.toMatch(/name: ['"]@deepseek-ai\/dsh-[^'"]+\/invariant['"]/)
|
||||
expect(stdout).toContain('provider: personal-provider')
|
||||
}, 30_000)
|
||||
})
|
||||
})
|
||||
|
||||
51
apps/cli/tests/fixtures/composition-echo-llm.ts
vendored
51
apps/cli/tests/fixtures/composition-echo-llm.ts
vendored
@@ -1,51 +0,0 @@
|
||||
import type { Context } from 'cordis'
|
||||
import type {
|
||||
GenerateOptions,
|
||||
LlmModelInfo,
|
||||
LlmResolvedModelInfo,
|
||||
StreamChunk,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
/** Terminal marker the preset smoke waits for before it asks the TUI to exit. */
|
||||
export const COMPOSITION_REPLY_TEXT = 'Shipped composition acknowledged.'
|
||||
|
||||
// Provider id and model the keyless tail routes `main` to; that overlay is the
|
||||
// only caller, so the pair lives here as plain constants.
|
||||
const COMPOSITION_PROVIDER = 'composition-keyless'
|
||||
const COMPOSITION_MODEL = 'composition-keyless-model'
|
||||
|
||||
/**
|
||||
* Network-free adapter for the shipped-composition smoke. It answers every
|
||||
* request — tool-ful agent turns and the tool-less auxiliary calls alike — with
|
||||
* one fixed text and never calls a tool, because the assertion under test is the
|
||||
* assembled tool catalog the loop logs, not any tool's behavior.
|
||||
*/
|
||||
class CompositionEchoAdapter extends LlmAdapter {
|
||||
override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
|
||||
return Promise.resolve([{ provider, id: COMPOSITION_MODEL, name: 'Preset Keyless' }])
|
||||
}
|
||||
|
||||
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
|
||||
return Promise.resolve({ provider, id: model, name: 'Preset Keyless', context: { contextWindow: 128_000 } })
|
||||
}
|
||||
|
||||
override async * stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
for (const char of COMPOSITION_REPLY_TEXT) yield { type: 'text-delta', index: 0, text: char }
|
||||
yield { type: 'block-end', index: 0, block: { type: 'text', text: COMPOSITION_REPLY_TEXT } }
|
||||
yield { type: 'usage', usage: { inputTokens: 20, outputTokens: COMPOSITION_REPLY_TEXT.length } }
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'composition-echo-llm'
|
||||
export const inject = ['llm']
|
||||
|
||||
/**
|
||||
* Register the network-free adapter the shipped-composition smoke routes through.
|
||||
* @param ctx - the loader-mounted plugin context.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.llm.registerAdapter([COMPOSITION_PROVIDER], new CompositionEchoAdapter())
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
# Keyless tail for the shipped-composition smoke, applied as `--config` so the
|
||||
# launcher boots `base.cordis.yml` + `tui.cordis.yml` and then this file.
|
||||
#
|
||||
# Everything below is test isolation, never composition under test: the model is
|
||||
# replaced so no request leaves the process, the settle marker gates the smoke's
|
||||
# first prompt, and the session artifacts move into the smoke's temporary
|
||||
# workspace so the log inspection can read them.
|
||||
|
||||
# A patch's `name` is an assertion rather than a replacement, so the base
|
||||
# adapter row is disabled and the scripted one inserted. Relative specifiers
|
||||
# resolve against the INCLUDED file's directory (apps/cli/config), not this
|
||||
# file's, because the include moves baseUrl there.
|
||||
- id: llm-deepseek
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: composition-echo-llm
|
||||
name: '../tests/fixtures/composition-echo-llm.ts'
|
||||
- id: composition-settled
|
||||
name: '../tests/fixtures/composition-settled.ts'
|
||||
|
||||
- id: agent-loop
|
||||
config:
|
||||
agents:
|
||||
- id: main
|
||||
provider: composition-keyless
|
||||
model: composition-keyless-model
|
||||
cwd: !!js process.cwd()
|
||||
|
||||
- id: session-persistence-jsonl
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: none
|
||||
|
||||
- id: session-query-sqlite
|
||||
config:
|
||||
path: './.sessions/session-query.db'
|
||||
|
||||
# The title call is a second, tool-less request that would race the log
|
||||
# inspection for no coverage: the catalog under test rides the agent turn.
|
||||
- id: session-title-llm
|
||||
disabled: true
|
||||
|
||||
- id: tui
|
||||
config:
|
||||
sessionId: !!js configuredAgentIdentities?.main?.id ?? 'main'
|
||||
welcome: 'composition smoke ready.'
|
||||
showReasoning: true
|
||||
|
||||
# HMR watches the repository; a PTY subprocess test must not start a watcher.
|
||||
- id: hmr
|
||||
disabled: true
|
||||
24
apps/cli/tests/fixtures/composition-settled.ts
vendored
24
apps/cli/tests/fixtures/composition-settled.ts
vendored
@@ -1,24 +0,0 @@
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
/**
|
||||
* Marker the shipped-composition smoke gates its first prompt on. The TUI renders as soon as
|
||||
* its own fiber starts, so a prompt typed at the banner can reach the loop while
|
||||
* later rows — tool plugins, persistence — are still activating, and would
|
||||
* assemble a partial catalog. Waiting for this line makes the turn observe the
|
||||
* settled tree.
|
||||
*/
|
||||
export const COMPOSITION_SETTLED_MARKER = 'COMPOSITION_TREE_SETTLED'
|
||||
|
||||
export const name = 'composition-settled'
|
||||
|
||||
/**
|
||||
* Announce settled Loader activation on the terminal byte stream, after every
|
||||
* entry in the booted tree has started. The write is detached: awaiting the
|
||||
* Loader from inside an entry would wait on this entry's own activation.
|
||||
* @param ctx - the loader-mounted plugin context.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
void ctx.loader.await().then(() => {
|
||||
process.stdout.write(`\n${COMPOSITION_SETTLED_MARKER}\n`)
|
||||
})
|
||||
}
|
||||
7
apps/cli/tests/fixtures/raw-invalid-provider.cordis.yml
vendored
Normal file
7
apps/cli/tests/fixtures/raw-invalid-provider.cordis.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Invalid raw overlay used to prove boot failures settle and exit.
|
||||
|
||||
- id: llm-pi-ai
|
||||
config:
|
||||
providers:
|
||||
- provider: openai
|
||||
apiKey: keyless-invalid-shape
|
||||
12
apps/cli/tests/fixtures/raw-overlay.cordis.yml
vendored
Normal file
12
apps/cli/tests/fixtures/raw-overlay.cordis.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Raw CLI overlay used by the built config-dump acceptance test.
|
||||
|
||||
- id: agent-loop
|
||||
config:
|
||||
agents:
|
||||
- id: configured
|
||||
provider: configured-provider
|
||||
model: configured-model
|
||||
|
||||
- id: absent-row
|
||||
config:
|
||||
value: unmatched
|
||||
@@ -1,10 +0,0 @@
|
||||
# An overlay whose `llm-pi-ai` config fails schema validation: `providers` is a
|
||||
# dict keyed by provider name, and a list is the shape users reach for. The
|
||||
# entry rejects while `ui-tui` — mounted concurrently by the Loader — already
|
||||
# holds the terminal, which is the boot failure the fail-loud release hook
|
||||
# exists for.
|
||||
- id: llm-pi-ai
|
||||
config:
|
||||
providers:
|
||||
- provider: openai
|
||||
apiKey: keyless-invalid-shape
|
||||
183
apps/cli/tests/fixtures/tui-scripted-llm.ts
vendored
183
apps/cli/tests/fixtures/tui-scripted-llm.ts
vendored
@@ -1,183 +0,0 @@
|
||||
import type { Context } from 'cordis'
|
||||
import type {
|
||||
GenerateOptions,
|
||||
LlmModelInfo,
|
||||
LlmResolvedModelInfo,
|
||||
StreamChunk,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { CallId, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
const CONTROL_PROBE = '\u001b]2;MODEL_CONTROLLED\u0007\u001b[999CMODEL_CURSOR\u009b31mMODEL_C1'
|
||||
const INITIAL_TEXT = `I need one decision before I continue. ${CONTROL_PROBE}`
|
||||
const FINAL_TEXT = 'Decision received. Scripted TUI run complete.'
|
||||
const DEFAULT_MODE_PROBE = 'Confirm the scripted run left plan mode.'
|
||||
const DEFAULT_MODE_TEXT = 'Default mode confirmed.'
|
||||
// The `skill` scenario types `/skill:scripted-skill`; the manual-invocation front
|
||||
// door delivers the loaded skill as a user turn wrapped in `<skill name="…">`. The
|
||||
// body marker below lives in the fixture skill, so echoing it back proves the whole
|
||||
// block (name attribute plus body) reached the model, not just the command text.
|
||||
const SKILL_BLOCK_OPEN = '<skill name="scripted-skill">'
|
||||
const SKILL_BODY_MARKER = 'SCRIPTED SKILL BODY MARKER'
|
||||
const SKILL_RECEIVED_TEXT = 'Scripted skill body received.'
|
||||
const TITLE_TEXT = 'scripted session title'
|
||||
// The failing-bash scenario proves the terminal card reports a non-zero exit
|
||||
// exactly once: the model-facing result carries the `[exit code: N]` marker, and
|
||||
// the card turns it into its own `[exit N]` pill instead of showing both.
|
||||
const BASH_FAILURE_PROBE = 'Run the failing scripted command.'
|
||||
const BASH_FAILURE_COMMAND = 'printf "SCRIPTED_BASH_FAILED\\n"; exit 3'
|
||||
const BASH_FAILURE_TEXT = 'Scripted bash failure observed.'
|
||||
const BASH_FAILURE_CALL_ID = CallId('call-bash-failure')
|
||||
|
||||
function textChunks(text: string): StreamChunk[] {
|
||||
return [
|
||||
{ type: 'block-start', index: 0, blockType: 'text' },
|
||||
...Array.from(text, (char): StreamChunk => ({ type: 'text-delta', index: 0, text: char })),
|
||||
{ type: 'block-end', index: 0, block: { type: 'text', text } },
|
||||
{ type: 'usage', usage: { inputTokens: 20, outputTokens: text.length } },
|
||||
{ type: 'finish', reason: { kind: 'stop' } },
|
||||
]
|
||||
}
|
||||
|
||||
/** Keyless adapter for the real-PTY TUI tests: the two-step conversation and the `/skill:` round-trip. */
|
||||
class ScriptedTuiAdapter extends LlmAdapter {
|
||||
override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
|
||||
return Promise.resolve([
|
||||
{ provider, id: 'tui-scripted-model', name: 'Scripted Base' },
|
||||
{ provider, id: 'tui-scripted-model-pro', name: 'Scripted Pro' },
|
||||
])
|
||||
}
|
||||
|
||||
override resolveModel(
|
||||
provider: string,
|
||||
model: string,
|
||||
): Promise<LlmResolvedModelInfo> {
|
||||
return Promise.resolve({
|
||||
provider,
|
||||
id: model,
|
||||
name: model === 'tui-scripted-model-pro' ? 'Scripted Pro' : 'Scripted Base',
|
||||
context: { contextWindow: 128_000 },
|
||||
...model !== 'tui-scripted-model-pro'
|
||||
? {}
|
||||
: {
|
||||
reasoning: {
|
||||
efforts: [
|
||||
{ id: ReasoningEffortId('off'), name: 'Off' },
|
||||
{ id: ReasoningEffortId('high'), name: 'High' },
|
||||
{ id: ReasoningEffortId('max'), name: 'Max' },
|
||||
],
|
||||
defaultEffort: ReasoningEffortId('high'),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
// The session-title provider's auxiliary request carries no tool schemas,
|
||||
// unlike every agent turn; answer it with a fixed title so the PTY test can
|
||||
// assert the logged title reaches the terminal window title.
|
||||
if ((options.tools?.length ?? 0) === 0) {
|
||||
for (const chunk of textChunks(TITLE_TEXT)) yield chunk
|
||||
return
|
||||
}
|
||||
if (
|
||||
options.model !== 'tui-scripted-model-pro'
|
||||
|| !options.system?.includes('tui-scripted-model-pro')
|
||||
|| options.reasoningEffort !== ReasoningEffortId('max')
|
||||
) {
|
||||
throw new Error('the scripted TUI request did not apply the selected model and reasoning effort')
|
||||
}
|
||||
const lastMessage = options.messages.at(-1)
|
||||
// The loop appends plugin-sourced context (the plan-mode notice, the
|
||||
// tool-skill catalog) AFTER the admitted prompt, so the scripted trigger
|
||||
// may sit one or more user messages back: scan the whole trailing run of
|
||||
// user-role messages since the last assistant message.
|
||||
const trailingUserTexts: string[] = []
|
||||
for (let index = options.messages.length - 1; index >= 0; index--) {
|
||||
const message = options.messages[index]
|
||||
if (message?.role !== 'user') break
|
||||
for (const block of message.content) {
|
||||
if (block.type === 'text') trailingUserTexts.push(block.text)
|
||||
}
|
||||
}
|
||||
const lastText = trailingUserTexts.join('\n')
|
||||
if (lastText.includes(DEFAULT_MODE_PROBE)) {
|
||||
if (options.system?.includes('Stay in plan mode for this scripted TUI test.')) {
|
||||
throw new Error('the scripted TUI request retained plan guidance after /plan off')
|
||||
}
|
||||
for (const chunk of textChunks(DEFAULT_MODE_TEXT)) yield chunk
|
||||
return
|
||||
}
|
||||
if (lastText.includes(SKILL_BLOCK_OPEN)) {
|
||||
const ack = lastText.includes(SKILL_BODY_MARKER)
|
||||
? SKILL_RECEIVED_TEXT
|
||||
: 'Scripted skill block arrived without its body.'
|
||||
for (const chunk of textChunks(ack)) yield chunk
|
||||
return
|
||||
}
|
||||
|
||||
const blocks = lastMessage?.content ?? []
|
||||
if (blocks.some(block => block.type === 'tool-result')) {
|
||||
const answeredBash = blocks.some(block =>
|
||||
block.type === 'tool-result' && block.toolCallId === BASH_FAILURE_CALL_ID)
|
||||
if (answeredBash) {
|
||||
for (const chunk of textChunks(BASH_FAILURE_TEXT)) yield chunk
|
||||
return
|
||||
}
|
||||
const toolResultText = blocks.flatMap(block => block.type === 'tool-result'
|
||||
? block.content.flatMap(content => content.type === 'text' ? [content.text] : [])
|
||||
: []).join('\n')
|
||||
if (toolResultText !== '{"answers":[{"id":"mode","selected":["Safe"],"custom":"Release notes"}]}') {
|
||||
throw new Error(`the scripted TUI request received an unexpected question answer: ${toolResultText}`)
|
||||
}
|
||||
for (const chunk of textChunks(FINAL_TEXT)) yield chunk
|
||||
return
|
||||
}
|
||||
if (lastText.includes(BASH_FAILURE_PROBE)) {
|
||||
const bashArgs = JSON.stringify({ command: BASH_FAILURE_COMMAND, description: 'Run the failing scripted command' })
|
||||
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
|
||||
yield { type: 'tool-call-delta', index: 0, id: BASH_FAILURE_CALL_ID, name: 'bash', argumentsDelta: bashArgs }
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: { type: 'tool-call', id: BASH_FAILURE_CALL_ID, name: 'bash', arguments: bashArgs },
|
||||
}
|
||||
yield { type: 'usage', usage: { inputTokens: 20, outputTokens: 10 } }
|
||||
yield { type: 'finish', reason: { kind: 'tool-calls' } }
|
||||
return
|
||||
}
|
||||
|
||||
const args = JSON.stringify({
|
||||
questions: [{
|
||||
id: 'mode',
|
||||
header: 'Execution mode',
|
||||
question: 'How should the scripted run proceed?',
|
||||
multi_select: true,
|
||||
options: [
|
||||
{ label: 'Safe', description: 'Use the guarded path.' },
|
||||
{ label: 'Fast', description: 'Use the shorter path.' },
|
||||
],
|
||||
}],
|
||||
})
|
||||
const callId = CallId('call-ask-mode')
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
for (const char of INITIAL_TEXT) yield { type: 'text-delta', index: 0, text: char }
|
||||
yield { type: 'block-end', index: 0, block: { type: 'text', text: INITIAL_TEXT } }
|
||||
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
|
||||
yield { type: 'tool-call-delta', index: 1, id: callId, name: 'ask_user_question', argumentsDelta: args }
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 1,
|
||||
block: { type: 'tool-call', id: callId, name: 'ask_user_question', arguments: args },
|
||||
}
|
||||
yield { type: 'usage', usage: { inputTokens: 20, outputTokens: 10 } }
|
||||
yield { type: 'finish', reason: { kind: 'tool-calls' } }
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'tui-scripted-llm'
|
||||
export const inject = ['llm']
|
||||
|
||||
/** Register the network-free adapter used by the PTY fixture. */
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.llm.registerAdapter(['tui-scripted'], new ScriptedTuiAdapter())
|
||||
}
|
||||
71
apps/cli/tests/fixtures/tui-scripted.cordis.yml
vendored
71
apps/cli/tests/fixtures/tui-scripted.cordis.yml
vendored
@@ -1,71 +0,0 @@
|
||||
# Overlay for the keyless conversational PTY test: the shipped composition with
|
||||
# only the model replaced, so the terminal interaction is deterministic and
|
||||
# network-free while the agent/TUI/user-question stack stays the production one.
|
||||
#
|
||||
# Passed as `--config`, so the launcher includes `base.cordis.yml`, applies
|
||||
# `tui.cordis.yml`, then this file — all sibling patch lists at one include
|
||||
# level. A patch replaces the targeted row's whole `config`, so each row below
|
||||
# restates every key it owns.
|
||||
|
||||
# The scripted adapter replaces the DeepSeek one: no key, no network. A patch's
|
||||
# `name` is an assertion rather than a replacement, so the base row is disabled
|
||||
# and the adapter inserted. Relative specifiers resolve against the INCLUDED
|
||||
# file's directory (apps/cli/config), because the include moves baseUrl there.
|
||||
- id: llm-deepseek
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: scripted-llm
|
||||
name: '../tests/fixtures/tui-scripted-llm.ts'
|
||||
|
||||
- id: agent-loop
|
||||
config:
|
||||
agents:
|
||||
- id: main
|
||||
provider: tui-scripted
|
||||
model: tui-scripted-model
|
||||
# `cwd` scopes the session to this workspace, which is what `/resume`
|
||||
# filters on; dropping it would hide the seeded session.
|
||||
cwd: !!js process.cwd()
|
||||
|
||||
- id: system-prompt
|
||||
config:
|
||||
persona: 'Scripted model {{model}}.'
|
||||
|
||||
# The smoke's log inspection reads plain `.jsonl` under the workspace, so this
|
||||
# fixture pins a project-local root instead of the launcher's shared store, and
|
||||
# keeps the artifacts uncompressed like the other snapshot-facing configs.
|
||||
- id: session-persistence-jsonl
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: none
|
||||
|
||||
# The derived index must sit under the same root as the logs it indexes; this
|
||||
# fixture pins both to the workspace instead of the launcher's shared store.
|
||||
- id: session-query-sqlite
|
||||
config:
|
||||
path: './.sessions/session-query.db'
|
||||
|
||||
- id: plan-mode
|
||||
config:
|
||||
section: 'Stay in plan mode for this scripted TUI test.'
|
||||
|
||||
# The scripted adapter answers the tool-less title request with a fixed string,
|
||||
# so the PTY test can assert the logged title reaches the terminal window title.
|
||||
- id: session-title-llm
|
||||
config:
|
||||
targetWords: 5
|
||||
targetCjkCharacters: 10
|
||||
maxInputBytes: 4096
|
||||
maxOutputTokens: 64
|
||||
timeoutMs: 10000
|
||||
|
||||
- id: tui
|
||||
config:
|
||||
sessionId: !!js configuredAgentIdentities?.main?.id ?? 'main'
|
||||
welcome: 'scripted TUI ready.'
|
||||
showReasoning: true
|
||||
|
||||
# HMR watches the repository; a PTY subprocess test must not start a watcher.
|
||||
- id: hmr
|
||||
disabled: true
|
||||
@@ -125,31 +125,16 @@ async function runInstaller(fixture: Fixture, actions: readonly Action[]): Promi
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
describe.runIf(process.platform !== 'win32')('one-line installer interface choice', { timeout: 25_000 }, () => {
|
||||
it('builds and launches the Web UI when the default choice is accepted', async () => {
|
||||
describe.runIf(process.platform !== 'win32')('one-line installer launch', { timeout: 25_000 }, () => {
|
||||
it('builds and launches the Web UI', async () => {
|
||||
const fixture = await createFixture()
|
||||
|
||||
const output = await runInstaller(fixture, [
|
||||
{ waitFor: 'Replace it?', send: '\n' },
|
||||
{ waitFor: 'Choose an interface [1/2]:', send: '\n' },
|
||||
])
|
||||
|
||||
expect(output).toContain('launching Web UI')
|
||||
expect(readFileSync(fixture.pnpmLog, 'utf8')).toBe('install\nrun build\n')
|
||||
expect(readFileSync(fixture.launchLog, 'utf8')).toBe('web\n')
|
||||
})
|
||||
|
||||
it('rejects an unknown choice, then launches the TUI without building', async () => {
|
||||
const fixture = await createFixture()
|
||||
|
||||
const output = await runInstaller(fixture, [
|
||||
{ waitFor: 'Replace it?', send: '\n' },
|
||||
{ waitFor: 'Choose an interface [1/2]:', send: 'terminal\n' },
|
||||
{ waitFor: 'choose 1 for Web UI or 2 for TUI', send: '2\n' },
|
||||
])
|
||||
|
||||
expect(output).toContain('launching TUI')
|
||||
expect(readFileSync(fixture.pnpmLog, 'utf8')).toBe('install\n')
|
||||
expect(readFileSync(fixture.launchLog, 'utf8')).toBe('\n')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { execa } from 'execa'
|
||||
import { resolveExampleLaunch, type ExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
const POSIX_PTY_DRIVER = String.raw`
|
||||
import errno, fcntl, json, os, pty, select, signal, struct, sys, termios, time
|
||||
node, launch_args_json, launch_env_json, cwd, actions_json, expected_exit, timeout_seconds, columns, rows = sys.argv[1:]
|
||||
env = os.environ.copy()
|
||||
env.update(json.loads(launch_env_json))
|
||||
env.update({"COLUMNS": columns, "LINES": rows})
|
||||
# Deterministic banner: a developer shell's COLORTERM=truecolor would switch the
|
||||
# banner to the per-letter gradient (one SGR per letter), breaking literal
|
||||
# DEEPSEEK assertions. The gradient path has its own unit and snapshot coverage.
|
||||
env.pop("COLORTERM", None)
|
||||
actions = json.loads(actions_json)
|
||||
pid, fd = pty.fork()
|
||||
if pid == 0:
|
||||
os.chdir(cwd)
|
||||
os.execvpe(node, [node, *json.loads(launch_args_json)], env)
|
||||
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", int(rows), int(columns), 0, 0))
|
||||
|
||||
output = bytearray()
|
||||
action_index = 0
|
||||
deadline = time.monotonic() + float(timeout_seconds)
|
||||
status = None
|
||||
while time.monotonic() < deadline:
|
||||
ready, _, _ = select.select([fd], [], [], 0.05)
|
||||
if ready:
|
||||
try:
|
||||
chunk = os.read(fd, 65536)
|
||||
except OSError as error:
|
||||
if error.errno != errno.EIO:
|
||||
raise
|
||||
chunk = b""
|
||||
if chunk:
|
||||
output.extend(chunk)
|
||||
while action_index < len(actions):
|
||||
marker = actions[action_index]["waitFor"].encode()
|
||||
if output.count(marker) < actions[action_index].get("occurrence", 1):
|
||||
break
|
||||
action = actions[action_index]
|
||||
if "signal" in action:
|
||||
os.kill(pid, getattr(signal, action["signal"]))
|
||||
elif "writeFile" in action:
|
||||
target = os.path.join(cwd, action["writeFile"]["path"])
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
with open(target, "w", encoding="utf-8") as handle:
|
||||
handle.write(action["writeFile"]["content"])
|
||||
if "send" in action:
|
||||
os.write(fd, action["send"].encode())
|
||||
else:
|
||||
os.write(fd, action["send"].encode())
|
||||
action_index += 1
|
||||
waited, candidate = os.waitpid(pid, os.WNOHANG)
|
||||
if waited == pid:
|
||||
status = candidate
|
||||
break
|
||||
|
||||
if status is None:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
_, status = os.waitpid(pid, 0)
|
||||
sys.stdout.buffer.write(output)
|
||||
if action_index != len(actions):
|
||||
sys.stderr.write(f"completed {action_index}/{len(actions)} PTY actions before timeout\n")
|
||||
sys.exit(124)
|
||||
actual_exit = os.waitstatus_to_exitcode(status)
|
||||
if actual_exit != int(expected_exit):
|
||||
sys.stderr.write(f"expected exit {expected_exit}, got {actual_exit}\n")
|
||||
sys.exit(125)
|
||||
`
|
||||
|
||||
/** One terminal input or workspace mutation performed after its marker renders. */
|
||||
type TuiPtyAction =
|
||||
| {
|
||||
readonly waitFor: string
|
||||
readonly occurrence?: number
|
||||
readonly send: string
|
||||
}
|
||||
| { readonly waitFor: string; readonly occurrence?: number; readonly signal: 'SIGTERM' }
|
||||
| {
|
||||
readonly waitFor: string
|
||||
readonly occurrence?: number
|
||||
readonly writeFile: { readonly path: string; readonly content: string }
|
||||
readonly send?: string
|
||||
}
|
||||
|
||||
/** Inputs for a keyless real-Loader TUI process smoke. */
|
||||
export interface TuiPtySmokeOptions {
|
||||
readonly label: string
|
||||
readonly tempDirPrefix: string
|
||||
readonly binScript: string
|
||||
/** Config argument; ignored when {@link configArgs} is set. */
|
||||
readonly configPath?: string
|
||||
/** Full argument vector for the bin (e.g. `[]` for a bin with a built-in default config). */
|
||||
readonly configArgs?: readonly string[]
|
||||
readonly tsconfigPath: string
|
||||
readonly actions?: readonly TuiPtyAction[]
|
||||
readonly env?: Readonly<NodeJS.ProcessEnv>
|
||||
readonly expectedExitCode?: number
|
||||
readonly timeoutMs?: number
|
||||
/** Existing isolated workspace to reuse; when omitted the harness creates and removes one. */
|
||||
readonly cwd?: string
|
||||
/** Pseudo-terminal columns; defaults to 100. */
|
||||
readonly columns?: number
|
||||
/** Pseudo-terminal rows; defaults to 30. */
|
||||
readonly rows?: number
|
||||
/** Seed the isolated workspace (`cwd`, with `$DSH_HOME` at `.dsh` and the agents home at `.agents`) before launch. */
|
||||
readonly prepare?: (cwd: string) => Promise<void>
|
||||
/** Inspect the workspace after a passing run, before the temp dir is removed. */
|
||||
readonly inspect?: (cwd: string) => Promise<void>
|
||||
}
|
||||
|
||||
function definedEnv(env: NodeJS.ProcessEnv): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
||||
)
|
||||
}
|
||||
|
||||
async function runPosixPtySmoke(
|
||||
launch: ExampleLaunch,
|
||||
cwd: string,
|
||||
options: TuiPtySmokeOptions,
|
||||
timeoutMs: number,
|
||||
): Promise<string> {
|
||||
// The driver owns the PTY deadline (`timeoutMs`); the outer execa deadline
|
||||
// only backstops a wedged python3 process itself.
|
||||
const result = await execa('python3', [
|
||||
'-c',
|
||||
POSIX_PTY_DRIVER,
|
||||
launch.command,
|
||||
JSON.stringify(launch.args),
|
||||
JSON.stringify(launch.env),
|
||||
cwd,
|
||||
JSON.stringify(options.actions ?? []),
|
||||
String(options.expectedExitCode ?? 0),
|
||||
String(timeoutMs / 1_000),
|
||||
String(options.columns ?? 100),
|
||||
String(options.rows ?? 30),
|
||||
], {
|
||||
stdin: 'ignore',
|
||||
timeout: timeoutMs + 5_000,
|
||||
killSignal: 'SIGKILL',
|
||||
reject: false,
|
||||
stripFinalNewline: false,
|
||||
})
|
||||
if (result.timedOut) {
|
||||
throw new Error(`${options.label} PTY driver did not exit. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
|
||||
}
|
||||
if (result.failed) {
|
||||
throw new Error(`${options.label} PTY driver exited ${String(result.exitCode)}. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
|
||||
}
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
async function runWindowsPtySmoke(
|
||||
launch: ExampleLaunch,
|
||||
cwd: string,
|
||||
options: TuiPtySmokeOptions,
|
||||
timeoutMs: number,
|
||||
): Promise<string> {
|
||||
const pty = await import('node-pty')
|
||||
return await new Promise((resolve, reject) => {
|
||||
const actions = options.actions ?? []
|
||||
const expectedExitCode = options.expectedExitCode ?? 0
|
||||
let output = ''
|
||||
let actionIndex = 0
|
||||
let timedOut = false
|
||||
const terminal = pty.spawn(launch.command, launch.args, {
|
||||
name: 'xterm-256color',
|
||||
cols: options.columns ?? 100,
|
||||
rows: options.rows ?? 30,
|
||||
cwd,
|
||||
env: definedEnv({
|
||||
...process.env,
|
||||
...launch.env,
|
||||
// Match the POSIX driver: no COLORTERM, so the banner never takes the
|
||||
// truecolor gradient path under a developer's shell.
|
||||
COLORTERM: undefined,
|
||||
COLUMNS: String(options.columns ?? 100),
|
||||
LINES: String(options.rows ?? 30),
|
||||
}),
|
||||
})
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true
|
||||
terminal.kill()
|
||||
}, timeoutMs)
|
||||
terminal.onData((chunk) => {
|
||||
output += chunk
|
||||
while (
|
||||
actionIndex < actions.length
|
||||
&& output.split(actions[actionIndex]!.waitFor).length - 1 >= (actions[actionIndex]!.occurrence ?? 1)
|
||||
) {
|
||||
const action = actions[actionIndex]!
|
||||
if ('signal' in action) {
|
||||
terminal.kill(action.signal)
|
||||
} else if ('writeFile' in action) {
|
||||
const target = join(cwd, action.writeFile.path)
|
||||
mkdirSync(dirname(target), { recursive: true })
|
||||
writeFileSync(target, action.writeFile.content)
|
||||
const input = action.send
|
||||
if (input !== undefined) terminal.write(input)
|
||||
} else {
|
||||
terminal.write(action.send)
|
||||
}
|
||||
actionIndex += 1
|
||||
}
|
||||
})
|
||||
terminal.onExit(({ exitCode, signal }) => {
|
||||
clearTimeout(timer)
|
||||
if (timedOut) {
|
||||
reject(new Error(`${options.label} PTY process did not exit before ${String(timeoutMs)}ms. output:\n${output}`))
|
||||
} else if (actionIndex !== actions.length) {
|
||||
reject(new Error(`${options.label} completed ${String(actionIndex)}/${String(actions.length)} PTY actions. output:\n${output}`))
|
||||
} else if (exitCode !== expectedExitCode) {
|
||||
reject(new Error(`${options.label} expected exit ${String(expectedExitCode)}, got ${String(exitCode)} (signal ${String(signal)}). output:\n${output}`))
|
||||
} else {
|
||||
resolve(output)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot an example in a real pseudo-terminal (ConPTY on Windows), drive
|
||||
* marker-gated input, and return captured bytes after the expected process exit.
|
||||
* @param options - launch paths, environment, actions, and expected exit code.
|
||||
* @returns complete pseudo-terminal output.
|
||||
*/
|
||||
export async function runTuiPtySmoke(options: TuiPtySmokeOptions): Promise<string> {
|
||||
const ownedCwd = options.cwd === undefined
|
||||
const cwd = options.cwd ?? await mkdtemp(join(tmpdir(), options.tempDirPrefix))
|
||||
const timeoutMs = options.timeoutMs ?? 25_000
|
||||
try {
|
||||
await options.prepare?.(cwd)
|
||||
const launch = resolveExampleLaunch({
|
||||
srcBin: options.binScript,
|
||||
// `configPath` is the dsh `--config <path>` tree override; `configArgs`
|
||||
// is the raw-args escape (e.g. `['--resume', <id>]`) for other flags.
|
||||
configArgs: options.configArgs !== undefined
|
||||
? [...options.configArgs]
|
||||
/* v8 ignore next -- every caller passes configPath or configArgs; the fallback keeps the type total */
|
||||
: options.configPath !== undefined ? ['--config', options.configPath] : [],
|
||||
tsconfigPath: options.tsconfigPath,
|
||||
env: {
|
||||
DSH_HOME: join(cwd, '.dsh'),
|
||||
DSH_AGENTS_HOME: join(cwd, '.agents'),
|
||||
...options.env,
|
||||
},
|
||||
})
|
||||
const output = process.platform === 'win32'
|
||||
? await runWindowsPtySmoke(launch, cwd, options, timeoutMs)
|
||||
: await runPosixPtySmoke(launch, cwd, options, timeoutMs)
|
||||
// Inspect the workspace before `finally` removes it (e.g. the session log).
|
||||
await options.inspect?.(cwd)
|
||||
return output
|
||||
} finally {
|
||||
if (ownedCwd) await rm(cwd, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { readdir, readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS } from '@deepseek-ai/dsh-loader-smoke'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { COMPOSITION_REPLY_TEXT } from './fixtures/composition-echo-llm.ts'
|
||||
import { COMPOSITION_SETTLED_MARKER } from './fixtures/composition-settled.ts'
|
||||
import { runTuiPtySmoke } from './pty-harness.ts'
|
||||
import { acknowledgeTuiFirstRunWelcome } from '../src/tui-onboarding/tui-first-run-welcome.ts'
|
||||
|
||||
const dshBinScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const PERMISSION_SUMMARY = 'current preset workspace-write (available: read-only, workspace-write, danger-full-access)'
|
||||
// An overlay over the shipped tree, so the catalog under test is the one
|
||||
// `base.cordis.yml` + `tui.cordis.yml` assemble; the tail only swaps the model
|
||||
// and redirects session artifacts.
|
||||
const keylessTail = fileURLToPath(new URL('./fixtures/composition-keyless-tail.cordis.yml', import.meta.url))
|
||||
|
||||
/**
|
||||
* The catalog the shipped `dsh` TUI puts in front of the model, as the loop
|
||||
* logged it, minus the ripgrep-dependent pair below.
|
||||
* The absences are the composition's security decisions, not incidental gaps:
|
||||
* the `cordis_*` toolset executes model-written JavaScript that no sandbox row
|
||||
* confines, `web_fetch` chooses its own request target, and `mcp_*` servers
|
||||
* spawn outside `ctx.bash`. The composition Agent Note owns the rationale and
|
||||
* its sources.
|
||||
*/
|
||||
const EXPECTED_TUI_TOOLS = [
|
||||
'ask_user_question',
|
||||
'bash',
|
||||
'create_goal',
|
||||
'edit',
|
||||
'exit_plan_mode',
|
||||
'get_goal',
|
||||
'list_agents',
|
||||
'ralph',
|
||||
'read',
|
||||
'send_message',
|
||||
'skill',
|
||||
'str_replace_editor',
|
||||
'subagent',
|
||||
'subagent_fork',
|
||||
'task_kill',
|
||||
'task_list',
|
||||
'task_output',
|
||||
'todo_write',
|
||||
'update_goal',
|
||||
'web_search',
|
||||
'workflow',
|
||||
'write',
|
||||
]
|
||||
|
||||
/**
|
||||
* `glob` and `grep` come from `dsh-tool-fs-search`, which spawns the PACKAGED
|
||||
* ripgrep binary (`@vscode/ripgrep`) through the subprocess seam, so the pair
|
||||
* is always present on every host — asserted as fixed members, not a host
|
||||
* dependency.
|
||||
*/
|
||||
const RIPGREP_TOOLS = ['glob', 'grep']
|
||||
|
||||
/** The assembled request header the smoke asserts on. */
|
||||
interface LoggedHeader {
|
||||
/** Assembled tool names, sorted. */
|
||||
names: string[]
|
||||
/** `bash`'s assembled parameter properties; the escalation pair is present only under a confining executor. */
|
||||
bashArguments: Record<string, unknown>
|
||||
/** Initial permission facts pinned by the shipped composition. */
|
||||
permissionEvents: Array<[string, unknown]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the request header the loop assembled for its first request from the
|
||||
* session log the smoke's workspace persisted — the model-visible composition
|
||||
* itself, not a registry projection taken beside it.
|
||||
* @param cwd - the smoke's temporary workspace.
|
||||
* @returns the assembled catalog, system prompt, and `bash` argument shape.
|
||||
*/
|
||||
async function loggedHeader(cwd: string): Promise<LoggedHeader> {
|
||||
const sessionsDir = join(cwd, '.sessions')
|
||||
const entries = await readdir(sessionsDir, { recursive: true })
|
||||
// A single keyless run writes one session log.
|
||||
const logRelPath = entries.find(name => name.endsWith('.jsonl'))
|
||||
if (logRelPath === undefined) throw new Error(`no session log written under ${sessionsDir}`)
|
||||
const events = (await readFile(join(sessionsDir, logRelPath), 'utf8')).split('\n').filter(Boolean)
|
||||
.map(line => JSON.parse(line) as SessionEvent)
|
||||
const header = events.find(event => event.type === 'request/header')
|
||||
if (header === undefined || header.type !== 'request/header') {
|
||||
throw new Error(`session log ${logRelPath} has no request/header event`)
|
||||
}
|
||||
const tools = header.data.header.tools ?? []
|
||||
const bash = tools.find(schema => schema.name === 'bash')
|
||||
return {
|
||||
names: tools.map(schema => schema.name).sort(),
|
||||
bashArguments: (bash?.parameters as { properties?: Record<string, unknown> } | undefined)?.properties ?? {},
|
||||
permissionEvents: events.flatMap(event =>
|
||||
event.type === 'permission/preset' || event.type === 'sandbox/mode' || event.type === 'approval/policy'
|
||||
? [[event.type, event.data] as [string, unknown]]
|
||||
: []),
|
||||
}
|
||||
}
|
||||
|
||||
describe('shipped dsh composition (real Loader tree in a PTY)', () => {
|
||||
it('assembles exactly the shipped TUI catalog', async () => {
|
||||
let observed: LoggedHeader | undefined
|
||||
const output = await runTuiPtySmoke({
|
||||
label: 'dsh shipped composition',
|
||||
tempDirPrefix: 'dsh-shipped-tui-',
|
||||
binScript: dshBinScript,
|
||||
tsconfigPath,
|
||||
configPath: keylessTail,
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-composition-no-call', DSH_TELEMETRY_DISABLED: '1' },
|
||||
prepare: cwd => acknowledgeTuiFirstRunWelcome(join(cwd, '.dsh')),
|
||||
// Artifact CI builds and smokes concurrently on a contended runner.
|
||||
...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}),
|
||||
actions: [
|
||||
{ waitFor: COMPOSITION_SETTLED_MARKER, send: '/permission\r' },
|
||||
{ waitFor: PERMISSION_SUMMARY, send: 'Describe the shipped composition.\r' },
|
||||
{ waitFor: COMPOSITION_REPLY_TEXT, send: '/exit\r' },
|
||||
],
|
||||
inspect: async (cwd) => { observed = await loggedHeader(cwd) },
|
||||
})
|
||||
expect(output).toContain(COMPOSITION_REPLY_TEXT)
|
||||
expect(output).toContain(PERMISSION_SUMMARY)
|
||||
expect(observed?.names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TUI_TOOLS)
|
||||
// The packaged ripgrep binary ships with the dependency, so the pair is a
|
||||
// fixed roster member on every host.
|
||||
expect(observed?.names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS)
|
||||
expect(observed?.bashArguments).toHaveProperty('sandbox_permissions')
|
||||
expect(observed?.bashArguments).toHaveProperty('justification')
|
||||
expect(observed?.permissionEvents).toEqual([
|
||||
['permission/preset', { preset: 'workspace-write' }],
|
||||
['sandbox/mode', { mode: 'workspace-write' }],
|
||||
['approval/policy', { policy: 'ask' }],
|
||||
])
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
{"type":"session","version":0,"id":"e128dda9-ed11-4868-8266-0ef90d03c3d6","createdAt":1783352050748,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":1783352050753,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783352050753,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo TERMINAL_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":1783352050755,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":3,"time":1783352050756,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1783352051421,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":5,"time0":1783352051422,"data":{"turn":1,"step":1,"index":0,"dt":[168,28,0,1,0,0,26,30,0,0,1,0,27,1,0,0,0],"texts":["The"," user"," wants"," me"," to"," run"," a"," simple"," bash"," command"," and"," then"," reply"," with"," \"","D","ONE","\"."]}}
|
||||
{"type":"assistant/chunk","seq":23,"time":1783352051790,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":24,"time0":1783352051791,"data":{"turn":1,"step":1,"index":1,"dt":[29,0,0,0,0,28,0,0,0,29,0,0,28,1,0,29,0,0,0,32,0,0,0,0,0,74,0,0,13,0],"id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","args":["","{","\"","command","\"",": ","\"","echo"," TER","MIN","AL","_OK","\"",", ","\"","description","\"",": ","\"","E","cho"," TER","MIN","AL","_OK"," to"," verify"," terminal"," access","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":55,"time":1783352052117,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a simple bash command and then reply with \"DONE\"."}}}}
|
||||
{"type":"assistant/chunk","seq":56,"time":1783352052118,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","arguments":"{\"command\": \"echo TERMINAL_OK\", \"description\": \"Echo TERMINAL_OK to verify terminal access\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":57,"time":1783352052118,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2877,"outputTokens":90,"cacheReadTokens":0,"reasoningTokens":18}}}}
|
||||
{"type":"assistant/chunk","seq":58,"time":1783352052118,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":59,"time":1783352052121,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a simple bash command and then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","arguments":"{\"command\": \"echo TERMINAL_OK\", \"description\": \"Echo TERMINAL_OK to verify terminal access\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":2877,"outputTokens":90,"cacheReadTokens":0,"reasoningTokens":18}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":60,"time":1783352052121,"data":{"turn":1,"step":1,"callId":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","name":"bash","arguments":"{\"command\": \"echo TERMINAL_OK\", \"description\": \"Echo TERMINAL_OK to verify terminal access\"}"}}
|
||||
{"type":"tool/result","seq":61,"time":1783352052136,"data":{"turn":1,"step":1,"callId":"call_00_fkbBRJsUrGKd1pWVc4Gn8233","content":[{"type":"text","text":"TERMINAL_OK\n"}],"isError":false},"sourceEventSeqs":[60],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":62,"time":1783352052137,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":63,"time":1783352052137,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":64,"time":1783352052701,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":65,"time0":1783352052702,"data":{"turn":1,"step":2,"index":0,"dt":[78,29,29,0,0,29,0,0,0,0,0,28,1,28,1,0,0,32,0,0,0],"texts":["The"," command"," ran"," successfully"," and"," output"," \"","TER","MIN","AL","_OK","\"."," I"," should"," now"," reply"," with"," just"," \"","D","ONE","\"."]}}
|
||||
{"type":"assistant/chunk","seq":87,"time":1783352052957,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":88,"time":1783352052957,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}}
|
||||
{"type":"assistant/chunk","seq":89,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}}
|
||||
{"type":"assistant/chunk","seq":90,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The command ran successfully and output \"TERMINAL_OK\". I should now reply with just \"DONE\"."}}}}
|
||||
{"type":"assistant/chunk","seq":91,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":92,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":168,"outputTokens":25,"cacheReadTokens":2816,"reasoningTokens":22}}}}
|
||||
{"type":"assistant/chunk","seq":93,"time":1783352052986,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":94,"time":1783352052987,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The command ran successfully and output \"TERMINAL_OK\". I should now reply with just \"DONE\"."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":168,"outputTokens":25,"cacheReadTokens":2816,"reasoningTokens":22}},"sourceEventSeqs":[64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":95,"time":1783352052987,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":96,"time":1783352052987,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,55 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Use the bash tool to — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=24 bufferRow=24
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Use the bash tool to"
|
||||
style 1-20 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "Use the bash tool to run exactly: echo TERMINAL_OK. Then reply with the single word DONE and stop. "
|
||||
6| <blank>
|
||||
7| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
8| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
9| "The user wants me to run a simple bash command and then reply with \"DONE\". "
|
||||
style 0-73 dim italic
|
||||
10| <blank>
|
||||
11| "● Tool / bash / Echo TERMINAL_OK to verify terminal access"
|
||||
style 0-57 fg=green
|
||||
12| "$ echo TERMINAL_OK "
|
||||
style 0-17 dim
|
||||
13| "TERMINAL_OK "
|
||||
style 0-10 dim
|
||||
14| "[exit 0] "
|
||||
style 0-7 dim
|
||||
15| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
16| <blank>
|
||||
17| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
18| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
19| "The command ran successfully and output \"TERMINAL_OK\". I should now reply with just \"DONE\". "
|
||||
style 0-90 dim italic
|
||||
20| "DONE "
|
||||
21| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
22| <blank>
|
||||
23| "/workspace/project deepseek-v4-flash ↑3.0k ↓115 cache 48% 3% contex"
|
||||
style 0-46 fg=bright-magenta bold
|
||||
style 49-65 dim
|
||||
style 68-88 dim
|
||||
style 91-99 dim
|
||||
24| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
25-35| <blank>
|
||||
@@ -1,32 +0,0 @@
|
||||
{"type":"session","version":0,"id":"main-session","createdAt":1785052797743,"cwd":"{{cwd}}"}
|
||||
{"type":"turn/start","seq":0,"time":1785052797817,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785052797818,"data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool exactly once with the command `seq 1 200 | awk '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}'`, then return ONLY the number of lines in its output. Reply with just that number and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785052797825,"data":{"title":"Using ONE run_code program: call","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785052797826,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785052797827,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785052798220,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1785052798221,"data":{"turn":1,"step":1,"index":0,"dt":[170,30,0,0,0,30,1,0,0,28,0,0,0,29,30,0,30,0,30,0,0,0,30,0,0,0,0,0,30,30,1],"texts":["The"," user"," wants"," me"," to"," write"," a"," single"," run","_code"," program"," that"," calls"," bash"," exactly"," once"," with"," a"," specific"," command",","," then"," returns"," only"," the"," number"," of"," lines"," in"," its"," output","."]}}
|
||||
{"type":"assistant/chunk","seq":38,"time":1785052798781,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":39,"time0":1785052798781,"data":{"turn":1,"step":1,"index":1,"dt":[0,0,28,0,0,0,30,29,1,0,30,30,1,0,30,0,0,28,1,0,0,30,0,0,0,0,1,28,1,0,0,0,0,58,1,0,15,0,0,0,0,39,0,1,0,28,0,0,29,0,0,0,0,0,30,0,0,0,0,1,29,1,0,0,0,29,0,0,40,0,0,20,1,29,0,30,0,30,0,0,31,0,0,30,0,0,30,0,0,30,0,1,0,29,0,0,30,31,0,0,0,0,0,30,0,0,0,1,0,28,0,0],"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","args":["","{","\"","description","\"",": ","\"","Count"," lines"," in"," seq","/","awk"," output","\"",", ","\"","code","\"",": ","\"","const"," result"," ="," await"," tools",".b","ash","({\\n"," "," command",":"," \\\"","seq"," ","1"," ","200"," |"," awk"," '{","printf"," \\\\\\\"","line"," %","04","d",":"," the"," quick"," brown"," fox"," jumps"," over"," the"," lazy"," dog","\\\\\\\\","n","\\\\\\\","," $","1","}'","\\\",\\n"," "," description",":"," \\\"","Generate"," ","200"," lines"," of"," text","\\\"\\n","});\\n\\n","//"," Count"," lines"," in"," stdout","\\n","const"," lines"," ="," result",".k","ind"," ==="," \\\"","fore","ground","\\\""," ?"," result",".stdout",".text",".trim","().","split","(\\\"\\\\","n","\\\").","length"," :"," ","0",";\\n","return"," lines",";","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":152,"time":1785052799793,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single run_code program that calls bash exactly once with a specific command, then returns only the number of lines in its output."}}}}
|
||||
{"type":"assistant/chunk","seq":153,"time":1785052799793,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","arguments":"{\"description\": \"Count lines in seq/awk output\", \"code\": \"const result = await tools.bash({\\n command: \\\"seq 1 200 | awk '{printf \\\\\\\"line %04d: the quick brown fox jumps over the lazy dog\\\\\\\\n\\\\\\\", $1}'\\\",\\n description: \\\"Generate 200 lines of text\\\"\\n});\\n\\n// Count lines in stdout\\nconst lines = result.kind === \\\"foreground\\\" ? result.stdout.text.trim().split(\\\"\\\\n\\\").length : 0;\\nreturn lines;\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":154,"time":1785052799794,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":90,"outputTokens":186,"cacheReadTokens":3968,"reasoningTokens":32}}}}
|
||||
{"type":"assistant/chunk","seq":155,"time":1785052799794,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":156,"time":1785052799798,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to write a single run_code program that calls bash exactly once with a specific command, then returns only the number of lines in its output."},{"type":"tool-call","id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","arguments":"{\"description\": \"Count lines in seq/awk output\", \"code\": \"const result = await tools.bash({\\n command: \\\"seq 1 200 | awk '{printf \\\\\\\"line %04d: the quick brown fox jumps over the lazy dog\\\\\\\\n\\\\\\\", $1}'\\\",\\n description: \\\"Generate 200 lines of text\\\"\\n});\\n\\n// Count lines in stdout\\nconst lines = result.kind === \\\"foreground\\\" ? result.stdout.text.trim().split(\\\"\\\\n\\\").length : 0;\\nreturn lines;\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":90,"outputTokens":186,"cacheReadTokens":3968,"reasoningTokens":32}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":157,"time":1785052799799,"data":{"turn":1,"step":1,"callId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","arguments":"{\"description\": \"Count lines in seq/awk output\", \"code\": \"const result = await tools.bash({\\n command: \\\"seq 1 200 | awk '{printf \\\\\\\"line %04d: the quick brown fox jumps over the lazy dog\\\\\\\\n\\\\\\\", $1}'\\\",\\n description: \\\"Generate 200 lines of text\\\"\\n});\\n\\n// Count lines in stdout\\nconst lines = result.kind === \\\"foreground\\\" ? result.stdout.text.trim().split(\\\"\\\\n\\\").length : 0;\\nreturn lines;\"}"}}
|
||||
{"type":"tool/code-dispatch-start","seq":158,"time":1785052799893,"data":{"parentCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","subCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490:code:1","name":"bash","arguments":{"command":"seq 1 200 | awk '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}'","description":"Generate 200 lines of text"}}}
|
||||
{"type":"tool/code-dispatch","seq":159,"time":1785052799923,"data":{"parentCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","subCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490:code:1","name":"bash","arguments":{"command":"seq 1 200 | awk '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}'","description":"Generate 200 lines of text"},"isError":false,"content":[{"type":"text","text":"line 0001: the quick brown fox jumps over the lazy dog\nline 0002: the quick brown fox jumps over the lazy dog\nline 0003: the quick brown fox jumps over the lazy dog\nline 0004: the quick s over the lazy dog\nline 0198: the quick brown fox jumps over the lazy dog\nline 0199: the quick brown fox jumps over the lazy dog\nline 0200: the quick brown fox jumps over the lazy dog\n\n\n(Omitted 10629 bytes. Full formatted result stored at: {{cwd}}/.spill/session-2d2b9e84a250/825a63550249-bash.txt. Use read with offset/limit, or grep this path to search within it.)"}]}}
|
||||
{"type":"tool/result","seq":160,"time":1785052799925,"data":{"turn":1,"step":1,"callId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","content":[{"type":"text","text":"200"}],"isError":false},"sourceEventSeqs":[157],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":161,"time":1785052799926,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":162,"time":1785052799928,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":163,"time":1785052800414,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":164,"time0":1785052800415,"data":{"turn":1,"step":2,"index":0,"dt":[157,32,0,0,0,1,30,1,30,0,0,0,33,1,0,0,0,31,0],"texts":["The"," result"," is"," ","200"," lines","."," The"," user"," wants"," me"," to"," reply"," with"," just"," that"," number"," and"," stop","."]}}
|
||||
{"type":"assistant/chunk","seq":184,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":185,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"200"}}}
|
||||
{"type":"assistant/chunk","seq":186,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The result is 200 lines. The user wants me to reply with just that number and stop."}}}}
|
||||
{"type":"assistant/chunk","seq":187,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"200"}}}}
|
||||
{"type":"assistant/chunk","seq":188,"time":1785052800732,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":33,"outputTokens":22,"cacheReadTokens":4224,"reasoningTokens":20}}}}
|
||||
{"type":"assistant/chunk","seq":189,"time":1785052800732,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":190,"time":1785052800733,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The result is 200 lines. The user wants me to reply with just that number and stop."},{"type":"text","text":"200"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":33,"outputTokens":22,"cacheReadTokens":4224,"reasoningTokens":20}},"sourceEventSeqs":[163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":191,"time":1785052800733,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":192,"time":1785052800733,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,59 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Using ONE run_code program: call — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=26 bufferRow=26
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Using ONE run_code program: call"
|
||||
style 1-32 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "Using ONE run_code program: call the bash tool exactly once with the command seq 1 200 | awk "
|
||||
style 77-99 fg=cyan
|
||||
6| "'{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}', then return ONLY the "
|
||||
style 0-72 fg=cyan
|
||||
7| "number of lines in its output. Reply with just that number and stop. "
|
||||
8| <blank>
|
||||
9| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
10| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
11| "The user wants me to write a single run_code program that calls bash exactly once with a specific "
|
||||
style 0-99 dim italic
|
||||
12| "command, then returns only the number of lines in its output. "
|
||||
style 0-60 dim italic
|
||||
13| <blank>
|
||||
14| "● Tool / run_code"
|
||||
style 0-16 fg=green
|
||||
15| "Count lines in seq/awk output "
|
||||
style 0-99 dim
|
||||
16| "200 "
|
||||
style 0-99 dim
|
||||
17| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
18| <blank>
|
||||
19| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
20| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
21| "The result is 200 lines. The user wants me to reply with just that number and stop. "
|
||||
style 0-82 dim italic
|
||||
22| "200 "
|
||||
23| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
24| <blank>
|
||||
25| "/workspace/project deepseek-v4-flash ↑123 ↓208 cache 99% 3% c"
|
||||
style 0-52 fg=bright-magenta bold
|
||||
style 55-71 dim
|
||||
style 74-93 dim
|
||||
style 96-99 dim
|
||||
26| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
27-35| <blank>
|
||||
@@ -1,34 +0,0 @@
|
||||
{"type":"session","version":0,"id":"main-session","createdAt":1785014512062,"cwd":"{{cwd}}"}
|
||||
{"type":"turn/start","seq":0,"time":1785014512139,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785014512140,"data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool twice — exactly `echo CODE_ONE` then exactly `echo CODE_TWO`. Inside that same program, console.log exactly `captured output`, then return the two outputs joined with a plus sign. Reply with that joined string only and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785014512146,"data":{"title":"Using ONE run_code program: call","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785014512147,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785014512148,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785014512526,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1785014512527,"data":{"turn":1,"step":1,"index":0,"dt":[92,26,0,0,0,27,0,1,20,1,0,0,0,25,1,0,0,0,24,1,24,26,0,24,1,25,0,0,0,1,0,24,0,1,0,0,0,24,1,0,0,0,24,0,1,0,24,1,0,0,0,0,24,1,0,24,0,0,0,1,1,23,0,0,0,0,1,24,25,1,24,1,0,0,0,25,0,25,1,0,0,25,0,0,24,1,0,0,0,25,0,0,24,1,0,25,1,0,0,25,23,26,1,0,0,25,0,0,24,1,0,0,24,0,1,0,24,1,0,0,25,0,0,1,0,0,23,0,1,0,0,0,24,1,0,0,0,0,24,0,0,0,0,1,24,1,0,0,0,0,25,0,0,0,0,1,24,0,0,24,1,0,0,0,24,0,1,0,0,0,33,0,0,0,16,1,0,0,24,1,0,0,0,26,1,0,23,25,0,0,25,1,0,24,0,1,0,0,24,1,0],"texts":["The"," user"," wants"," me"," to"," write"," a"," single"," `","run","_code","`"," program"," that",":\n","1","."," Calls"," `","bash","`"," tool"," twice"," -"," first"," with"," `","echo"," CODE","_","ONE","`,"," then"," with"," `","echo"," CODE","_T","WO","`\n","2","."," `","console",".log","`"," exactly"," `","capt","ured"," output","`\n","3","."," Returns"," the"," two"," outputs"," joined"," with"," a"," plus"," sign","\n\n","Let"," me"," think"," about"," the"," structure","."," The"," `","bash","`"," tool"," returns"," an"," object"," with"," stdout","/st","derr","."," I"," need"," to"," extract"," the"," stdout"," text"," from"," each"," call",".\n\n","Looking"," at"," the"," bash"," output"," type",":\n","```\n","{\n"," "," kind",":"," \"","fore","ground","\";\n"," "," exit","Code",":"," number"," |"," null",";\n"," "," signal",":"," string"," |"," null",";\n"," "," timed","Out",":"," boolean",";\n"," "," ab","orted",":"," boolean",";\n"," "," timeout","Ms",":"," number",";\n"," "," stdout",":"," {\n"," "," text",":"," string",";\n"," "," truncated",":"," boolean",";\n"," "," spill","Path","?:"," string",";\n"," "," };\n"," "," st","derr",":"," {"," ..."," };\n"," "," sand","box","?:"," {"," ..."," };\n","}\n","```\n\n","So"," I"," need"," to"," access"," `.","std","out",".text","`"," from"," each"," result",".\n\n","Let"," me"," write"," the"," program","."]}}
|
||||
{"type":"assistant/chunk","seq":208,"time":1785014513974,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":209,"time0":1785014513974,"data":{"turn":1,"step":1,"index":1,"dt":[24,1,0,0,0,24,1,0,24,0,0,40,10,26,0,25,1,0,0,24,1,0,0,0,29,0,0,0,0,0,20,1,0,0,0,24,0,0,0,0,1,25,0,1,0,26,0,0,0,0,0,23,0,0,0,0,0,25,0,0,0,0,0,24,0,0,0,0,1,25,0,0,0,0,0,24,1,0,0,0,0,31,1,17,1,24,0,25,1,0,0,0,24,1,0,0,0,25,25,25,0,0,0,0,0,26,0,0,0,0,1,23,0,1,0,0,0,24,1,24,1,0,24,1,0,0,0,25,0],"id":"call_00_D5QaUXWyA2cPRIFIT6o05977","name":"run_code","args":["","{","\"","description","\"",": ","\"","Call"," bash"," twice",","," log",","," join"," outputs","\"",", ","\"","code","\"",": ","\"","\\n","const"," r","1"," ="," await"," tools",".b","ash","({\\n"," "," command",":"," \\\"","echo"," CODE","_","ONE","\\\",\\n"," "," description",":"," \\\"","E","cho"," CODE","_","ONE","\\\"\\n","});\\n\\n","const"," r","2"," ="," await"," tools",".b","ash","({\\n"," "," command",":"," \\\"","echo"," CODE","_T","WO","\\\",\\n"," "," description",":"," \\\"","E","cho"," CODE","_T","WO","\\\"\\n","});\\n\\n","//"," Extract"," stdout"," text"," and"," trim"," trailing"," new","lines","\\n","const"," out","1"," ="," r","1",".stdout",".text",".trim","();\\n","const"," out","2"," ="," r","2",".stdout",".text",".trim","();\\n\\n","console",".log","(\\\"","capt","ured"," output","\\\");\\n\\n","return"," out","1"," +"," \\\"+","\\\""," +"," out","2",";\\n","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":339,"time":1785014514829,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single `run_code` program that:\n1. Calls `bash` tool twice - first with `echo CODE_ONE`, then with `echo CODE_TWO`\n2. `console.log` exactly `captured output`\n3. Returns the two outputs joined with a plus sign\n\nLet me think about the structure. The `bash` tool returns an object with stdout/stderr. I need to extract the stdout text from each call.\n\nLooking at the bash output type:\n```\n{\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: { ... };\n sandbox?: { ... };\n}\n```\n\nSo I need to access `.stdout.text` from each result.\n\nLet me write the program."}}}}
|
||||
{"type":"assistant/chunk","seq":340,"time":1785014514830,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_D5QaUXWyA2cPRIFIT6o05977","name":"run_code","arguments":"{\"description\": \"Call bash twice, log, join outputs\", \"code\": \"\\nconst r1 = await tools.bash({\\n command: \\\"echo CODE_ONE\\\",\\n description: \\\"Echo CODE_ONE\\\"\\n});\\n\\nconst r2 = await tools.bash({\\n command: \\\"echo CODE_TWO\\\",\\n description: \\\"Echo CODE_TWO\\\"\\n});\\n\\n// Extract stdout text and trim trailing newlines\\nconst out1 = r1.stdout.text.trim();\\nconst out2 = r2.stdout.text.trim();\\n\\nconsole.log(\\\"captured output\\\");\\n\\nreturn out1 + \\\"+\\\" + out2;\\n\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":341,"time":1785014514830,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":85,"outputTokens":373,"cacheReadTokens":3968,"reasoningTokens":202}}}}
|
||||
{"type":"assistant/chunk","seq":342,"time":1785014514830,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":343,"time":1785014514837,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to write a single `run_code` program that:\n1. Calls `bash` tool twice - first with `echo CODE_ONE`, then with `echo CODE_TWO`\n2. `console.log` exactly `captured output`\n3. Returns the two outputs joined with a plus sign\n\nLet me think about the structure. The `bash` tool returns an object with stdout/stderr. I need to extract the stdout text from each call.\n\nLooking at the bash output type:\n```\n{\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: { ... };\n sandbox?: { ... };\n}\n```\n\nSo I need to access `.stdout.text` from each result.\n\nLet me write the program."},{"type":"tool-call","id":"call_00_D5QaUXWyA2cPRIFIT6o05977","name":"run_code","arguments":"{\"description\": \"Call bash twice, log, join outputs\", \"code\": \"\\nconst r1 = await tools.bash({\\n command: \\\"echo CODE_ONE\\\",\\n description: \\\"Echo CODE_ONE\\\"\\n});\\n\\nconst r2 = await tools.bash({\\n command: \\\"echo CODE_TWO\\\",\\n description: \\\"Echo CODE_TWO\\\"\\n});\\n\\n// Extract stdout text and trim trailing newlines\\nconst out1 = r1.stdout.text.trim();\\nconst out2 = r2.stdout.text.trim();\\n\\nconsole.log(\\\"captured output\\\");\\n\\nreturn out1 + \\\"+\\\" + out2;\\n\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":85,"outputTokens":373,"cacheReadTokens":3968,"reasoningTokens":202}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":344,"time":1785014514839,"data":{"turn":1,"step":1,"callId":"call_00_D5QaUXWyA2cPRIFIT6o05977","name":"run_code","arguments":"{\"description\": \"Call bash twice, log, join outputs\", \"code\": \"\\nconst r1 = await tools.bash({\\n command: \\\"echo CODE_ONE\\\",\\n description: \\\"Echo CODE_ONE\\\"\\n});\\n\\nconst r2 = await tools.bash({\\n command: \\\"echo CODE_TWO\\\",\\n description: \\\"Echo CODE_TWO\\\"\\n});\\n\\n// Extract stdout text and trim trailing newlines\\nconst out1 = r1.stdout.text.trim();\\nconst out2 = r2.stdout.text.trim();\\n\\nconsole.log(\\\"captured output\\\");\\n\\nreturn out1 + \\\"+\\\" + out2;\\n\"}"}}
|
||||
{"type":"tool/code-dispatch-start","seq":345,"time":1785014514956,"data":{"parentCallId":"call_00_D5QaUXWyA2cPRIFIT6o05977","subCallId":"call_00_D5QaUXWyA2cPRIFIT6o05977:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Echo CODE_ONE"}}}
|
||||
{"type":"tool/code-dispatch","seq":346,"time":1785014514990,"data":{"parentCallId":"call_00_D5QaUXWyA2cPRIFIT6o05977","subCallId":"call_00_D5QaUXWyA2cPRIFIT6o05977:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Echo CODE_ONE"},"isError":false,"content":[{"type":"text","text":"CODE_ONE\n"}]}}
|
||||
{"type":"tool/code-dispatch-start","seq":347,"time":1785014514991,"data":{"parentCallId":"call_00_D5QaUXWyA2cPRIFIT6o05977","subCallId":"call_00_D5QaUXWyA2cPRIFIT6o05977:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Echo CODE_TWO"}}}
|
||||
{"type":"tool/code-dispatch","seq":348,"time":1785014515013,"data":{"parentCallId":"call_00_D5QaUXWyA2cPRIFIT6o05977","subCallId":"call_00_D5QaUXWyA2cPRIFIT6o05977:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Echo CODE_TWO"},"isError":false,"content":[{"type":"text","text":"CODE_TWO\n"}]}}
|
||||
{"type":"tool/result","seq":349,"time":1785014515017,"data":{"turn":1,"step":1,"callId":"call_00_D5QaUXWyA2cPRIFIT6o05977","content":[{"type":"text","text":"captured output\nCODE_ONE+CODE_TWO"}],"isError":false},"sourceEventSeqs":[344],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":350,"time":1785014515018,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":351,"time":1785014515022,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":352,"time":1785014515610,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":353,"time0":1785014515611,"data":{"turn":1,"step":2,"index":0,"dt":[116,25,0,26,1,0,25,26,1,0,0,26,1,0,0,25,0,0,26,0,0,26,0,0,0,1,1,24,0,1,0,0,25,26,0,0,0,0,2,24,0,1,0,25,1,0,0,0,25,1,0,0,0,25,0,0,0,0,0,26,1,0,0,0],"texts":["The"," program"," ran"," successfully","."," The"," output"," shows",":\n","-"," `","capt","ured"," output","`"," (","from"," console",".log",")\n","-"," `","CODE","_","ONE","+","CODE","_T","WO","`"," (","the"," returned"," joined"," string",")\n\n","The"," user"," asked"," me"," to"," reply"," with"," that"," joined"," string"," only"," and"," stop","."," So"," I","'ll"," reply"," with"," just"," `","CODE","_","ONE","+","CODE","_T","WO","`."]}}
|
||||
{"type":"assistant/chunk","seq":418,"time":1785014516169,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"text-chunks","seq0":419,"time0":1785014516169,"data":{"turn":1,"step":2,"index":1,"dt":[0,0,0,0,1,27],"texts":["CODE","_","ONE","+","CODE","_T","WO"]}}
|
||||
{"type":"assistant/chunk","seq":426,"time":1785014516199,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The program ran successfully. The output shows:\n- `captured output` (from console.log)\n- `CODE_ONE+CODE_TWO` (the returned joined string)\n\nThe user asked me to reply with that joined string only and stop. So I'll reply with just `CODE_ONE+CODE_TWO`."}}}}
|
||||
{"type":"assistant/chunk","seq":427,"time":1785014516200,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CODE_ONE+CODE_TWO"}}}}
|
||||
{"type":"assistant/chunk","seq":428,"time":1785014516200,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":97,"outputTokens":73,"cacheReadTokens":4352,"reasoningTokens":65}}}}
|
||||
{"type":"assistant/chunk","seq":429,"time":1785014516200,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":430,"time":1785014516201,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The program ran successfully. The output shows:\n- `captured output` (from console.log)\n- `CODE_ONE+CODE_TWO` (the returned joined string)\n\nThe user asked me to reply with that joined string only and stop. So I'll reply with just `CODE_ONE+CODE_TWO`."},{"type":"text","text":"CODE_ONE+CODE_TWO"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":97,"outputTokens":73,"cacheReadTokens":4352,"reasoningTokens":65}},"sourceEventSeqs":[352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":431,"time":1785014516202,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":432,"time":1785014516202,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,143 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=62 base=26 viewport=26
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Using ONE run_code program: call — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=35 bufferRow=61
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Using ONE run_code program: call"
|
||||
style 1-32 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "Using ONE run_code program: call the bash tool twice — exactly echo CODE_ONE then exactly echo "
|
||||
style 63-75 fg=cyan
|
||||
style 90-99 fg=cyan
|
||||
6| "CODE_TWO. Inside that same program, console.log exactly captured output, then return the two outputs"
|
||||
style 0-7 fg=cyan
|
||||
style 56-70 fg=cyan
|
||||
7| "joined with a plus sign. Reply with that joined string only and stop. "
|
||||
8| <blank>
|
||||
9| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
10| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
11| "The user wants me to write a single run_code program that: "
|
||||
style 0-35 dim italic
|
||||
style 36-43 fg=cyan
|
||||
style 44-57 dim italic
|
||||
12| "1. Calls bash tool twice - first with echo CODE_ONE, then with echo CODE_TWO "
|
||||
style 0-2 fg=bright-magenta
|
||||
style 3-8 dim italic
|
||||
style 9-12 fg=cyan
|
||||
style 13-37 dim italic
|
||||
style 38-50 fg=cyan
|
||||
style 51-62 dim italic
|
||||
style 63-75 fg=cyan
|
||||
13| "2. console.log exactly captured output "
|
||||
style 0-2 fg=bright-magenta
|
||||
style 3-13 fg=cyan
|
||||
style 14-22 dim italic
|
||||
style 23-37 fg=cyan
|
||||
14| "3. Returns the two outputs joined with a plus sign "
|
||||
style 0-2 fg=bright-magenta
|
||||
style 3-49 dim italic
|
||||
15| " "
|
||||
16| "Let me think about the structure. The bash tool returns an object with stdout/stderr. I need to "
|
||||
style 0-37 dim italic
|
||||
style 38-41 fg=cyan
|
||||
style 42-99 dim italic
|
||||
17| "extract the stdout text from each call. "
|
||||
style 0-38 dim italic
|
||||
18| " "
|
||||
19| "Looking at the bash output type: "
|
||||
style 0-31 dim italic
|
||||
20| " "
|
||||
21| " "
|
||||
22| " { "
|
||||
style 2-2 fg=cyan
|
||||
23| " kind: \"foreground\"; "
|
||||
style 2-22 fg=cyan
|
||||
24| " exitCode: number | null; "
|
||||
style 2-27 fg=cyan
|
||||
25| " signal: string | null; "
|
||||
style 2-25 fg=cyan
|
||||
26| " timedOut: boolean; "
|
||||
style 2-21 fg=cyan
|
||||
27| " aborted: boolean; "
|
||||
style 2-20 fg=cyan
|
||||
28| " timeoutMs: number; "
|
||||
style 2-21 fg=cyan
|
||||
29| " stdout: { "
|
||||
style 2-12 fg=cyan
|
||||
30| " text: string; "
|
||||
style 2-18 fg=cyan
|
||||
31| " truncated: boolean; "
|
||||
style 2-24 fg=cyan
|
||||
32| " spillPath?: string; "
|
||||
style 2-24 fg=cyan
|
||||
33| " }; "
|
||||
style 2-5 fg=cyan
|
||||
34| " stderr: { ... }; "
|
||||
style 2-19 fg=cyan
|
||||
35| " sandbox?: { ... }; "
|
||||
style 2-21 fg=cyan
|
||||
36| " } "
|
||||
style 2-2 fg=cyan
|
||||
37| " "
|
||||
38| " "
|
||||
39| "So I need to access .stdout.text from each result. "
|
||||
style 0-19 dim italic
|
||||
style 20-31 fg=cyan
|
||||
style 32-49 dim italic
|
||||
40| " "
|
||||
41| "Let me write the program. "
|
||||
style 0-24 dim italic
|
||||
42| <blank>
|
||||
43| "● Tool / run_code"
|
||||
style 0-16 fg=green
|
||||
44| "Call bash twice, log, join outputs "
|
||||
style 0-99 dim
|
||||
45| "captured output "
|
||||
style 0-99 dim
|
||||
46| "CODE_ONE+CODE_TWO "
|
||||
style 0-99 dim
|
||||
47| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
48| <blank>
|
||||
49| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
50| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
51| "The program ran successfully. The output shows: "
|
||||
style 0-46 dim italic
|
||||
52| "- captured output (from console.log) "
|
||||
style 0-1 fg=bright-magenta
|
||||
style 2-16 fg=cyan
|
||||
style 17-35 dim italic
|
||||
53| "- CODE_ONE+CODE_TWO (the returned joined string) "
|
||||
style 0-1 fg=bright-magenta
|
||||
style 2-18 fg=cyan
|
||||
style 19-47 dim italic
|
||||
54| " "
|
||||
55| "The user asked me to reply with that joined string only and stop. So I'll reply with just "
|
||||
style 0-99 dim italic
|
||||
56| "CODE_ONE+CODE_TWO. "
|
||||
style 0-16 fg=cyan
|
||||
style 17-17 dim italic
|
||||
57| "CODE_ONE+CODE_TWO "
|
||||
58| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
59| <blank>
|
||||
60| "/workspace/project deepseek-v4-flash ↑182 ↓446 cache 98% 4% context"
|
||||
style 0-37 fg=bright-magenta bold
|
||||
style 40-56 dim
|
||||
style 59-78 dim
|
||||
style 81-90 dim
|
||||
61| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
@@ -1,13 +0,0 @@
|
||||
{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","delegationDepth":1}
|
||||
{"type":"turn/start","seq":0,"time":1783957884563,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783957884563,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":1783957884564,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":3,"time":1783957884564,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1783950001005,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}}
|
||||
{"type":"assistant/chunk","seq":6,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}}
|
||||
{"type":"assistant/chunk","seq":7,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":8,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":9,"time":1783957884564,"data":{"turn":1,"step":1,"content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":10,"time":1783957884564,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":11,"time":1783957884564,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,13 +0,0 @@
|
||||
{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","delegationDepth":1}
|
||||
{"type":"turn/start","seq":0,"time":1783957884700,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783957884700,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":1783957884700,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":3,"time":1783957884701,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1783950002005,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}}
|
||||
{"type":"assistant/chunk","seq":6,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}}
|
||||
{"type":"assistant/chunk","seq":7,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":8,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":9,"time":1783957884701,"data":{"turn":1,"step":1,"content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":10,"time":1783957884701,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":11,"time":1783957884701,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,64 +0,0 @@
|
||||
{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":1783957884479,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783957884479,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: try a no-op temporary Cordis Plugin named snapshot-marker; use run_code to inspect the live temporary Plugins through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; stop dyn-1; then reply with exactly ADVANCED_ACP_OK."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":1783957884486,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":3,"time":1783957884486,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1783950000005,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1783950000006,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-mount","name":"cordis_mount","argumentsDelta":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}}}
|
||||
{"type":"assistant/chunk","seq":6,"time":1783950000007,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":7,"time":1783950000008,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":8,"time":1783950000009,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":9,"time":1783957884487,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":10,"time":1783957884487,"data":{"turn":1,"step":1,"callId":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}}
|
||||
{"type":"tool/result","seq":11,"time":1783957884488,"data":{"turn":1,"step":1,"callId":"advanced-mount","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"snapshot-marker\"; available until unmounted or DSH restarts)."}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":12,"time":1783957884489,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":13,"time":1783957884489,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":14,"time":1783950000015,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":15,"time":1783950000016,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.cordis_inspect({ what: 'temporary' })\", \"description\": \"Verify the temporary marker Plugin\"}"}}}
|
||||
{"type":"assistant/chunk","seq":16,"time":1783950000017,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.cordis_inspect({ what: 'temporary' })\", \"description\": \"Verify the temporary marker Plugin\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":17,"time":1783950000018,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":18,"time":1783950000019,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":19,"time":1783957884490,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.cordis_inspect({ what: 'temporary' })\", \"description\": \"Verify the temporary marker Plugin\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":20,"time":1783957884490,"data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.cordis_inspect({ what: 'temporary' })\", \"description\": \"Verify the temporary marker Plugin\"}"}}
|
||||
{"type":"tool/code-dispatch","seq":21,"time":1783957884560,"data":{"parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_inspect","arguments":{"what":"temporary"},"isError":false,"resultSummary":"## dynamic\n- dyn-1: snapshot-marker [active]"}}
|
||||
{"type":"tool/result","seq":22,"time":1783957884561,"data":{"turn":1,"step":2,"callId":"advanced-code","content":[{"type":"text","text":"## dynamic\n- dyn-1: snapshot-marker [active]"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[20],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":23,"time":1783957884561,"data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","seq":24,"time":1783957884562,"data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/chunk","seq":25,"time":1783950000026,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":26,"time":1783950000027,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}
|
||||
{"type":"assistant/chunk","seq":27,"time":1783950000028,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":28,"time":1783950000029,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1783950000030,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":30,"time":1783957884562,"data":{"turn":1,"step":3,"content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[25,26,27,28,29],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":31,"time":1783957884562,"data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
|
||||
{"type":"tool/result","seq":32,"time":1783957884593,"data":{"turn":1,"step":3,"callId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false},"sourceEventSeqs":[31],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":33,"time":1783957884593,"data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","seq":34,"time":1783957884594,"data":{"turn":1,"step":4}}
|
||||
{"type":"assistant/chunk","seq":35,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":36,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}}
|
||||
{"type":"assistant/chunk","seq":37,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}}}
|
||||
{"type":"assistant/chunk","seq":38,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":39,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":40,"time":1783957884594,"data":{"turn":1,"step":4,"content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[35,36,37,38,39],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":41,"time":1783957884594,"data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-acp-snapshot\",\"description\":\"exercise one workflow child through ACP\"}}"}}
|
||||
{"type":"tool/result","seq":42,"time":1783957884717,"data":{"turn":1,"step":4,"callId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-acp-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false},"sourceEventSeqs":[41],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":43,"time":1783957884718,"data":{"turn":1,"step":4}}
|
||||
{"type":"step/start","seq":44,"time":1783957884718,"data":{"turn":1,"step":5}}
|
||||
{"type":"assistant/chunk","seq":45,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":46,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-unmount","name":"cordis_unmount","argumentsDelta":"{\"id\":\"dyn-1\"}"}}}
|
||||
{"type":"assistant/chunk","seq":47,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":48,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":49,"time":1783957884719,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":50,"time":1783957884719,"data":{"turn":1,"step":5,"content":[{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":51,"time":1783957884719,"data":{"turn":1,"step":5,"callId":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}}
|
||||
{"type":"tool/result","seq":52,"time":1783957884719,"data":{"turn":1,"step":5,"callId":"advanced-unmount","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false},"sourceEventSeqs":[51],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":53,"time":1783957884719,"data":{"turn":1,"step":5}}
|
||||
{"type":"step/start","seq":54,"time":1783957884720,"data":{"turn":1,"step":6}}
|
||||
{"type":"assistant/chunk","seq":55,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":56,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_ACP_OK"}}}
|
||||
{"type":"assistant/chunk","seq":57,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_ACP_OK"}}}}
|
||||
{"type":"assistant/chunk","seq":58,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":59,"time":1783957884720,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":60,"time":1783957884720,"data":{"turn":1,"step":6,"content":[{"type":"text","text":"ADVANCED_ACP_OK"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[55,56,57,58,59],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":61,"time":1783957884721,"data":{"turn":1,"step":6}}
|
||||
{"type":"turn/end","seq":62,"time":1783957884721,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,110 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=59 base=23 viewport=23
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Run this advanced flow exactly — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=35 bufferRow=58
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Run this advanced flow exactly"
|
||||
style 1-30 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "Run this advanced flow exactly once: try a no-op temporary Cordis Plugin named snapshot-marker; use "
|
||||
6| "run_code to inspect the live temporary Plugins through tools.cordis_inspect; delegate once to a "
|
||||
7| "direct spawn child; run one workflow that delegates to another spawn child; stop dyn-1; then reply "
|
||||
8| "with exactly ADVANCED_ACP_OK. "
|
||||
9| <blank>
|
||||
10| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
11| <blank>
|
||||
12| "● Tool / cordis_mount"
|
||||
style 0-20 fg=green
|
||||
13| "Mount temporary Cordis Plugin "
|
||||
style 0-99 dim
|
||||
14| "Temporary Plugin dyn-1 is running (plugin \"snapshot-marker\"; available until unmounted or DSH "
|
||||
style 0-99 dim
|
||||
15| "restarts). "
|
||||
style 0-99 dim
|
||||
16| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
17| <blank>
|
||||
18| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
19| <blank>
|
||||
20| "● Tool / run_code"
|
||||
style 0-16 fg=green
|
||||
21| "Verify the temporary marker Plugin "
|
||||
style 0-99 dim
|
||||
22| " "
|
||||
23| "Temporary Plugins "
|
||||
style 0-16 fg=bright-magenta bold dim
|
||||
24| " "
|
||||
25| "- Temporary Plugin dyn-1: snapshot-marker [running] — provides: none; waiting for: none; lifetime: "
|
||||
style 0-1 fg=bright-magenta dim
|
||||
style 2-99 dim
|
||||
26| " until unmounted or DSH restarts "
|
||||
style 0-99 dim
|
||||
27| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
28| <blank>
|
||||
29| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
30| <blank>
|
||||
31| "● Tool / subagent"
|
||||
style 0-16 fg=green
|
||||
32| "DIRECT_CHILD_OK "
|
||||
style 0-99 dim
|
||||
33| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
34| <blank>
|
||||
35| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
36| <blank>
|
||||
37| "● Tool / workflow"
|
||||
style 0-16 fg=green
|
||||
38| "workflow: advanced-acp-snapshot "
|
||||
style 0-99 dim
|
||||
39| "workflow \"advanced-acp-snapshot\" completed (1 agent). "
|
||||
style 0-99 dim
|
||||
40| "Return value: "
|
||||
style 0-99 dim
|
||||
41| "{ "
|
||||
style 0-99 dim
|
||||
42| " \"reply\": \"WORKFLOW_CHILD_OK\" "
|
||||
style 0-99 dim
|
||||
43| "} "
|
||||
style 0-99 dim
|
||||
44| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
45| <blank>
|
||||
46| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
47| <blank>
|
||||
48| "● Tool / cordis_unmount"
|
||||
style 0-22 fg=green
|
||||
49| "Unmount temporary Cordis Plugin dyn-1 "
|
||||
style 0-99 dim
|
||||
50| "Temporary Plugin dyn-1 was unmounted and removed. "
|
||||
style 0-99 dim
|
||||
51| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
52| <blank>
|
||||
53| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
54| "ADVANCED_ACP_OK "
|
||||
55| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
56| <blank>
|
||||
57| "/workspace/project deepseek-v4-flash ↑18 ↓18 cache 0% 8% cont"
|
||||
style 0-52 fg=bright-magenta bold
|
||||
style 55-71 dim
|
||||
style 74-90 dim
|
||||
style 93-99 dim
|
||||
58| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
@@ -1,16 +0,0 @@
|
||||
{"type":"session","version":0,"id":"583a4db2-3350-436c-b4a5-5615fd159052","createdAt":1783600636316,"cwd":"{{cwd}}","parentSession":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","delegationDepth":1}
|
||||
{"type":"turn/start","seq":0,"time":1783600636316,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783600636316,"data":{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":1783600636316,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":3,"time":1783600636317,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1783600638073,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":5,"time0":1783600638073,"data":{"turn":1,"step":1,"index":0,"dt":[100,16,0,0,0,0,24,0,0,0,0,29,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," \"","WF","_CH","ILD","_OK","\""," and"," nothing"," else","."]}}
|
||||
{"type":"assistant/chunk","seq":23,"time":1783600638276,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"text-chunks","seq0":24,"time0":1783600638276,"data":{"turn":1,"step":1,"index":1,"dt":[0,0,0],"texts":["WF","_CH","ILD","_OK"]}}
|
||||
{"type":"assistant/chunk","seq":28,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"WF_CHILD_OK\" and nothing else."}}}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WF_CHILD_OK"}}}}
|
||||
{"type":"assistant/chunk","seq":30,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":17,"outputTokens":23,"cacheReadTokens":3072,"reasoningTokens":18}}}}
|
||||
{"type":"assistant/chunk","seq":31,"time":1783600638280,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":32,"time":1783600638281,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"WF_CHILD_OK\" and nothing else."},{"type":"text","text":"WF_CHILD_OK"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":17,"outputTokens":23,"cacheReadTokens":3072,"reasoningTokens":18}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":33,"time":1783600638281,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":34,"time":1783600638281,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,29 +0,0 @@
|
||||
{"type":"session","version":0,"id":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","createdAt":1783600631835,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":1783600631838,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783600631838,"data":{"content":[{"type":"text","text":"Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim):\nphase('Run')\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\nreturn { reply }\nAfter the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any other tool."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":1783600631839,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":3,"time":1783600631839,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1783600634643,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":5,"time0":1783600634643,"data":{"turn":1,"step":1,"index":0,"dt":[991,0,0,0,0,0,0,0,0,0,1,0,0,0,108,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,8,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," workflow"," tool"," exactly"," once"," with"," specific"," parameters","."," Let"," me"," carefully"," follow"," the"," instructions",":\n\n","1","."," args"," omitted"," (","so"," I"," don","'t"," include"," it",")\n","2","."," meta"," ="," {"," \"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\""," }\n","3","."," script"," ="," as"," given"," verb","atim","\n","4","."," After"," it"," returns",","," reply"," with"," \"","WORK","FL","OW","_D","ONE","\"\n\n","Let"," me"," do"," exactly"," that","."]}}
|
||||
{"type":"assistant/chunk","seq":93,"time":1783600635756,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":94,"time0":1783600635756,"data":{"turn":1,"step":1,"index":1,"dt":[0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,5,0,275,0,0,0,0,206,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0],"id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","args":["","{","\"","meta","\"",": ","{\"","name","\":"," \"","sn","apshot","-flow","\","," \"","description","\":"," \"","one"," child"," for"," the"," snapshot","\"","}",", ","\"","script","\"",": ","\"","\\n","phase","('","Run","')\\n","const"," reply"," ="," await"," agent","('","Reply"," with"," exactly"," the"," word"," WF","_CH","ILD","_OK"," and"," nothing"," else",".')\\n","return"," {"," reply"," }\\n","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":155,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."}}}}
|
||||
{"type":"assistant/chunk","seq":156,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":157,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}}}}
|
||||
{"type":"assistant/chunk","seq":158,"time":1783600636246,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":159,"time":1783600636247,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:\n\n1. args omitted (so I don't include it)\n2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }\n3. script = as given verbatim\n4. After it returns, reply with \"WORKFLOW_DONE\"\n\nLet me do exactly that."},{"type":"tool-call","id":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":3174,"outputTokens":191,"cacheReadTokens":0,"reasoningTokens":88}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":160,"time":1783600636247,"data":{"turn":1,"step":1,"callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","name":"workflow","arguments":"{\"meta\": {\"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\"}, \"script\": \"\\nphase('Run')\\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\\nreturn { reply }\\n\"}"}}
|
||||
{"type":"tool/result","seq":161,"time":1783600638304,"data":{"turn":1,"step":1,"callId":"call_00_dD2BLuNeJCTh2iiYC1QR3449","content":[{"type":"text","text":"workflow \"snapshot-flow\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WF_CHILD_OK\"\n}"}],"isError":false},"sourceEventSeqs":[160],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":162,"time":1783600638304,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":163,"time":1783600638305,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":164,"time":1783600640028,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":165,"time0":1783600640028,"data":{"turn":1,"step":2,"index":0,"dt":[106,28,33,667,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"texts":["The"," workflow"," returned"," successfully"," with"," the"," reply"," \"","WF","_CH","ILD","_OK","\"."," Now"," I"," need"," to"," reply"," with"," exactly"," \"","WORK","FL","OW","_D","ONE","\""," and"," stop","."]}}
|
||||
{"type":"assistant/chunk","seq":195,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"text-chunks","seq0":196,"time0":1783600640865,"data":{"turn":1,"step":2,"index":1,"dt":[0,0,0,0],"texts":["WORK","FL","OW","_D","ONE"]}}
|
||||
{"type":"assistant/chunk","seq":201,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."}}}}
|
||||
{"type":"assistant/chunk","seq":202,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"WORKFLOW_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":203,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}}}}
|
||||
{"type":"assistant/chunk","seq":204,"time":1783600640865,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":205,"time":1783600640865,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop."},{"type":"text","text":"WORKFLOW_DONE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":328,"outputTokens":36,"cacheReadTokens":3072,"reasoningTokens":30}},"sourceEventSeqs":[164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":206,"time":1783600640865,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":207,"time":1783600640865,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,86 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=43 base=7 viewport=7
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Use the workflow tool exactly — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=35 bufferRow=42
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Use the workflow tool exactly"
|
||||
style 1-29 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", "
|
||||
6| "\"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim): "
|
||||
7| "phase('Run') "
|
||||
8| "const reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.') "
|
||||
9| "return { reply } "
|
||||
10| "After the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any other "
|
||||
11| "tool. "
|
||||
12| <blank>
|
||||
13| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
14| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
15| "The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully "
|
||||
style 0-99 dim italic
|
||||
16| "follow the instructions: "
|
||||
style 0-23 dim italic
|
||||
17| " "
|
||||
18| "1. args omitted (so I don't include it) "
|
||||
style 0-2 fg=bright-magenta
|
||||
style 3-38 dim italic
|
||||
19| "2. meta = { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" } "
|
||||
style 0-2 fg=bright-magenta
|
||||
style 3-81 dim italic
|
||||
20| "3. script = as given verbatim "
|
||||
style 0-2 fg=bright-magenta
|
||||
style 3-28 dim italic
|
||||
21| "4. After it returns, reply with \"WORKFLOW_DONE\" "
|
||||
style 0-2 fg=bright-magenta
|
||||
style 3-46 dim italic
|
||||
22| " "
|
||||
23| "Let me do exactly that. "
|
||||
style 0-22 dim italic
|
||||
24| <blank>
|
||||
25| "● Tool / workflow"
|
||||
style 0-16 fg=green
|
||||
26| "workflow: snapshot-flow "
|
||||
style 0-99 dim
|
||||
27| "workflow \"snapshot-flow\" completed (1 agent). "
|
||||
style 0-99 dim
|
||||
28| "Return value: "
|
||||
style 0-99 dim
|
||||
29| "{ "
|
||||
style 0-99 dim
|
||||
30| " \"reply\": \"WF_CHILD_OK\" "
|
||||
style 0-99 dim
|
||||
31| "} "
|
||||
style 0-99 dim
|
||||
32| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
33| <blank>
|
||||
34| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
35| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
36| "The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly "
|
||||
style 0-99 dim italic
|
||||
37| "\"WORKFLOW_DONE\" and stop. "
|
||||
style 0-24 dim italic
|
||||
38| "WORKFLOW_DONE "
|
||||
39| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
40| <blank>
|
||||
41| "/workspace/project deepseek-v4-flash ↑3.5k ↓227 cache 47% 3% context"
|
||||
style 0-44 fg=bright-magenta bold
|
||||
style 47-63 dim
|
||||
style 66-86 dim
|
||||
style 89-98 dim
|
||||
42| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
@@ -1,31 +0,0 @@
|
||||
{"type":"session","version":0,"id":"228b7b82-84ed-49b7-a567-981c03b28c77","createdAt":1783352113760,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":1783352113765,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783352113765,"data":{"content":[{"type":"text","text":"Reply with exactly the word: ONE. No tools."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":1783352113767,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":3,"time":1783352113768,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1783352114428,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":5,"time0":1783352114428,"data":{"turn":1,"step":1,"index":0,"dt":[114,28,1,0,0,1,28,1,1,0,0,1,24,1,29,1,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","ONE","\""," and"," use"," no"," tools","."]}}
|
||||
{"type":"assistant/chunk","seq":23,"time":1783352114658,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":24,"time":1783352114658,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}}
|
||||
{"type":"assistant/chunk","seq":25,"time":1783352114687,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"ONE\" and use no tools."}}}}
|
||||
{"type":"assistant/chunk","seq":26,"time":1783352114687,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"ONE"}}}}
|
||||
{"type":"assistant/chunk","seq":27,"time":1783352114687,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2864,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":18}}}}
|
||||
{"type":"assistant/chunk","seq":28,"time":1783352114687,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":29,"time":1783352114690,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"ONE\" and use no tools."},{"type":"text","text":"ONE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":2864,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":18}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":30,"time":1783352114690,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":31,"time":1783352114690,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"turn/start","seq":32,"time":1783352114699,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":33,"time":1783352114699,"data":{"content":[{"type":"text","text":"Reply with exactly the word: TWO. No tools."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":34,"time":1783352114700,"data":{"turn":2,"step":1}}
|
||||
{"type":"assistant/chunk","seq":35,"time":1783352115341,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":36,"time0":1783352115341,"data":{"turn":2,"step":1,"index":0,"dt":[124,27,1,0,0,28,0,0,31,0,0,0,0,28,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," the"," word"," \"","T","WO","\""," and"," no"," tools","."]}}
|
||||
{"type":"assistant/chunk","seq":54,"time":1783352115609,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":55,"time":1783352115609,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"T"}}}
|
||||
{"type":"assistant/chunk","seq":56,"time":1783352115609,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"WO"}}}
|
||||
{"type":"assistant/chunk","seq":57,"time":1783352115610,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"TWO\" and no tools."}}}}
|
||||
{"type":"assistant/chunk","seq":58,"time":1783352115610,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"TWO"}}}}
|
||||
{"type":"assistant/chunk","seq":59,"time":1783352115610,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":18}}}}
|
||||
{"type":"assistant/chunk","seq":60,"time":1783352115610,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":61,"time":1783352115611,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"TWO\" and no tools."},{"type":"text","text":"TWO"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":64,"outputTokens":21,"cacheReadTokens":2816,"reasoningTokens":18}},"sourceEventSeqs":[35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":62,"time":1783352115611,"data":{"turn":2,"step":1}}
|
||||
{"type":"turn/end","seq":63,"time":1783352115611,"data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
@@ -1,62 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Reply with exactly the word: — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=30 bufferRow=30
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Reply with exactly the word:"
|
||||
style 1-28 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "Reply with exactly the word: ONE. No tools. "
|
||||
6| <blank>
|
||||
7| "Plan mode on. Use /plan off to leave. "
|
||||
style 0-36 dim
|
||||
8| <blank>
|
||||
9| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
10| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
11| "The user wants me to reply with exactly the word \"ONE\" and use no tools. "
|
||||
style 0-71 dim italic
|
||||
12| "ONE "
|
||||
13| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
14| <blank>
|
||||
15| "Context · plan-mode"
|
||||
style 0-18 dim
|
||||
16| "The user switched this session back to the default mode. "
|
||||
style 0-55 dim
|
||||
17| <blank>
|
||||
18| "Plan mode off. "
|
||||
style 0-13 dim
|
||||
19| <blank>
|
||||
20| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
21| "Reply with exactly the word: TWO. No tools. "
|
||||
22| <blank>
|
||||
23| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
24| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
25| "The user wants me to reply with exactly the word \"TWO\" and no tools. "
|
||||
style 0-67 dim italic
|
||||
26| "TWO "
|
||||
27| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
28| <blank>
|
||||
29| "/workspace/project deepseek-v4-flash ↑2.9k ↓41 cache 49% 3% co"
|
||||
style 0-51 fg=bright-magenta bold
|
||||
style 54-70 dim
|
||||
style 73-92 dim
|
||||
style 95-99 dim
|
||||
30| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
31-35| <blank>
|
||||
@@ -1,28 +0,0 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Use the read tool twice in the same assistant message: read a.txt and b.txt. Then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":3,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":4,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_read_a","name":"read","argumentsDelta":"{\"file_path\":\"a.txt\"}"}}}
|
||||
{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_read_a","name":"read","arguments":"{\"file_path\":\"a.txt\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_read_b","name":"read","argumentsDelta":"{\"file_path\":\"b.txt\"}"}}}
|
||||
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_read_b","name":"read","arguments":"{\"file_path\":\"b.txt\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":12,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_read_a","name":"read","arguments":"{\"file_path\":\"a.txt\"}"},{"type":"tool-call","id":"call_read_b","name":"read","arguments":"{\"file_path\":\"b.txt\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8,9,10,11],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":13,"time":0,"data":{"turn":1,"step":1,"callId":"call_read_a","name":"read","arguments":"{\"file_path\":\"a.txt\"}"}}
|
||||
{"type":"tool/call","seq":14,"time":0,"data":{"turn":1,"step":1,"callId":"call_read_b","name":"read","arguments":"{\"file_path\":\"b.txt\"}"}}
|
||||
{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"call_read_a","content":[{"type":"text","text":"<path>{{cwd}}/a.txt</path>\n<type>file</type>\n<content>\n1: alpha\n\n(End of file - total 1 lines)\n</content>"}],"isError":false},"sourceEventSeqs":[13],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":16,"time":0,"data":{"turn":1,"step":1,"callId":"call_read_b","content":[{"type":"text","text":"<path>{{cwd}}/b.txt</path>\n<type>file</type>\n<content>\n1: beta\n\n(End of file - total 1 lines)\n</content>"}],"isError":false},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":17,"time":0,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":18,"time":0,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
|
||||
{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":1}}}}
|
||||
{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":1}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":25,"time":0,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":26,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,58 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Use the read tool twice — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=27 bufferRow=27
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Use the read tool twice"
|
||||
style 1-23 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "Use the read tool twice in the same assistant message: read a.txt and b.txt. Then reply DONE. "
|
||||
6| <blank>
|
||||
7| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
8| <blank>
|
||||
9| "● Tool / read"
|
||||
style 0-12 fg=green
|
||||
10| "Read a.txt "
|
||||
style 0-99 dim
|
||||
11| "1: alpha "
|
||||
style 0-99 dim
|
||||
12| " "
|
||||
13| "(End of file - total 1 lines) "
|
||||
style 0-99 dim
|
||||
14| <blank>
|
||||
15| "● Tool / read"
|
||||
style 0-12 fg=green
|
||||
16| "Read b.txt "
|
||||
style 0-99 dim
|
||||
17| "1: beta "
|
||||
style 0-99 dim
|
||||
18| " "
|
||||
19| "(End of file - total 1 lines) "
|
||||
style 0-99 dim
|
||||
20| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
21| <blank>
|
||||
22| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
23| "DONE "
|
||||
24| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
25| <blank>
|
||||
26| "/workspace/project deepseek-v4-flash ↑20 ↓6 cache 0% 3% context"
|
||||
style 0-47 fg=bright-magenta bold
|
||||
style 50-66 dim
|
||||
style 69-84 dim
|
||||
style 87-96 dim
|
||||
27| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
28-35| <blank>
|
||||
@@ -1 +0,0 @@
|
||||
alpha
|
||||
@@ -1 +0,0 @@
|
||||
beta
|
||||
@@ -1,132 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=68 base=32 viewport=32
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Reply with exactly the word: — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=35 bufferRow=67
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Reply with exactly the word:"
|
||||
style 1-28 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Context · snapshot-seed"
|
||||
style 0-22 dim
|
||||
5| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
6| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
7| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
8| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
9| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
10| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
11| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
12| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
13| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
14| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
15| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
16| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
17| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
18| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-99 dim
|
||||
19| "Older snapshot context. Older snapshot context. Older snapshot context. Older snapshot context. "
|
||||
style 0-94 dim
|
||||
20| <blank>
|
||||
21| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
22| "Reply with exactly the word: ONE. No tools. "
|
||||
23| <blank>
|
||||
24| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
25| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
26| "The user wants me to reply with exactly the word \"ONE\" and use no tools. "
|
||||
style 0-71 dim italic
|
||||
27| "ONE "
|
||||
28| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
29| <blank>
|
||||
30| "Keyboard shortcuts "
|
||||
style 0-17 fg=bright-magenta bold
|
||||
31| "Enter send • Shift/Alt+Enter newline • Up/Down prompt history "
|
||||
style 0-60 dim
|
||||
32| "Esc cancel turn • Ctrl+O cycle cards (collapse/expand/hide) • Ctrl+R toggle reasoning • Ctrl+L "
|
||||
style 0-99 dim
|
||||
33| "redraw "
|
||||
style 0-5 dim
|
||||
34| "Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit "
|
||||
style 0-72 dim
|
||||
35| " "
|
||||
36| "/clear — Clear the transcript view (session history is unchanged) "
|
||||
style 0-64 dim
|
||||
37| "/compact — Compact older conversation history "
|
||||
style 0-44 dim
|
||||
38| "/details [collapsed|expanded|hidden] [reasoning [on|off]] — Select tool-card visibility and "
|
||||
style 0-99 dim
|
||||
39| "reasoning display "
|
||||
style 0-16 dim
|
||||
40| "/exit — Exit after the active turn reaches idle "
|
||||
style 0-46 dim
|
||||
41| "/help — Show keyboard shortcuts and commands "
|
||||
style 0-43 dim
|
||||
42| "/model [[provider/]model] — Show or switch this session's model "
|
||||
style 0-62 dim
|
||||
43| "/palette — Show every color and attribute role this terminal renders "
|
||||
style 0-67 dim
|
||||
44| "/quit — Exit after the active turn reaches idle "
|
||||
style 0-46 dim
|
||||
45| "/reload — EXPERIMENTAL (dev): re-read loader config files and apply the diff (idle only) "
|
||||
style 0-87 dim
|
||||
46| "/resume — List this workspace's resumable sessions "
|
||||
style 0-49 dim
|
||||
47| "/status — Show session diagnostics, system prompt, and registered tools "
|
||||
style 0-70 dim
|
||||
48| "/skill:<name> [instructions] — load a skill into the conversation "
|
||||
style 0-64 dim
|
||||
49| <blank>
|
||||
50| "Context · snapshot-injector"
|
||||
style 0-26 dim
|
||||
51| "Injected while compaction was running. "
|
||||
style 0-37 dim
|
||||
52| <blank>
|
||||
53| "… earlier context was compacted … "
|
||||
style 0-32 dim
|
||||
54| <blank>
|
||||
55| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
56| "Reply with exactly the word: TWO. No tools. "
|
||||
57| <blank>
|
||||
58| "Compacted 2 history items (~387 tokens). "
|
||||
style 0-39 dim
|
||||
59| <blank>
|
||||
60| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
61| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
62| "The user wants me to reply with exactly the word \"TWO\" and no tools. "
|
||||
style 0-67 dim italic
|
||||
63| "TWO "
|
||||
64| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
65| <blank>
|
||||
66| "/workspace/project deepseek-v4-flash ↑2.9k ↓41 cache 49% 3% cont"
|
||||
style 0-49 fg=bright-magenta bold
|
||||
style 52-68 dim
|
||||
style 71-90 dim
|
||||
style 93-99 dim
|
||||
67| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
@@ -1,5 +0,0 @@
|
||||
{"type":"session","version":0,"id":"31f63cc0-0198-4ab2-bfde-79a4eb4f1867","createdAt":1783352180000,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"assistant/chunk","seq":0,"time":1783352180001,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":1,"time":1783352180002,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"USER-ONLY SKILL LOADED"}}}
|
||||
{"type":"assistant/chunk","seq":2,"time":1783352180003,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"USER-ONLY SKILL LOADED"}}}}
|
||||
{"type":"assistant/chunk","seq":3,"time":1783352180004,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
@@ -1,157 +0,0 @@
|
||||
=== skill autocomplete ===
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "DSH TUI snapshot"
|
||||
cursor hidden column=13 viewportRow=5 bufferRow=5
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Recorded replay: skill-invocation-policy"
|
||||
style 1-40 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "/workspace/project deepseek-v4-flash ↑0 ↓0 0% context"
|
||||
style 0-51 fg=bright-magenta bold
|
||||
style 54-70 dim
|
||||
style 73-77 dim
|
||||
style 80-89 dim
|
||||
5| " dsh > /skill "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 13-13 inverse
|
||||
6| " → skill:user-only-skill (project) — User-only assembled snapshot skill. "
|
||||
style 7-78 fg=bright-magenta
|
||||
7-35| <blank>
|
||||
|
||||
|
||||
=== loaded exact invocation ===
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "<skill name=\"user-only-skill\"> Reference — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=30 bufferRow=30
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " <skill name=\"user-only-skill\"> Reference"
|
||||
style 1-40 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "<skill name=\"user-only-skill\"> "
|
||||
6| "References in this skill are relative to "
|
||||
7| "/workspace/project/.agents/skills/user-only-skill. "
|
||||
8| " "
|
||||
9| "USER-ONLY BODY "
|
||||
10| "</skill> "
|
||||
11| <blank>
|
||||
12| "Context · dsh-tool-skill"
|
||||
style 0-23 dim
|
||||
13| "A skill is a reusable set of task-specific instructions. The following skills are available in this "
|
||||
style 0-99 dim
|
||||
14| "session: "
|
||||
style 0-7 dim
|
||||
15| " "
|
||||
16| "<available_skills> "
|
||||
style 0-17 dim
|
||||
17| "- `model-only-skill`: Model-only assembled snapshot skill. "
|
||||
style 0-57 dim
|
||||
18| "</available_skills> "
|
||||
style 0-18 dim
|
||||
19| " "
|
||||
20| "If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool "
|
||||
style 0-99 dim
|
||||
21| "with the exact skill name before taking task actions. Load all applicable skills, then follow their "
|
||||
style 0-99 dim
|
||||
22| "full instructions. This catalog contains summaries only; do not infer or follow a skill's "
|
||||
style 0-99 dim
|
||||
23| "instructions until it has been loaded. "
|
||||
style 0-37 dim
|
||||
24| <blank>
|
||||
25| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
26| "USER-ONLY SKILL LOADED "
|
||||
27| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
28| <blank>
|
||||
29| "/workspace/project deepseek-v4-flash ↑0 ↓0 3% context"
|
||||
style 0-51 fg=bright-magenta bold
|
||||
style 54-70 dim
|
||||
style 73-77 dim
|
||||
style 80-89 dim
|
||||
30| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
31-35| <blank>
|
||||
|
||||
|
||||
=== denied exact invocation ===
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "<skill name=\"user-only-skill\"> Reference — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=32 bufferRow=32
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " <skill name=\"user-only-skill\"> Reference"
|
||||
style 1-40 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "<skill name=\"user-only-skill\"> "
|
||||
6| "References in this skill are relative to "
|
||||
7| "/workspace/project/.agents/skills/user-only-skill. "
|
||||
8| " "
|
||||
9| "USER-ONLY BODY "
|
||||
10| "</skill> "
|
||||
11| <blank>
|
||||
12| "Context · dsh-tool-skill"
|
||||
style 0-23 dim
|
||||
13| "A skill is a reusable set of task-specific instructions. The following skills are available in this "
|
||||
style 0-99 dim
|
||||
14| "session: "
|
||||
style 0-7 dim
|
||||
15| " "
|
||||
16| "<available_skills> "
|
||||
style 0-17 dim
|
||||
17| "- `model-only-skill`: Model-only assembled snapshot skill. "
|
||||
style 0-57 dim
|
||||
18| "</available_skills> "
|
||||
style 0-18 dim
|
||||
19| " "
|
||||
20| "If the user names a skill, or the task clearly matches a skill's description, call the `skill` tool "
|
||||
style 0-99 dim
|
||||
21| "with the exact skill name before taking task actions. Load all applicable skills, then follow their "
|
||||
style 0-99 dim
|
||||
22| "full instructions. This catalog contains summaries only; do not infer or follow a skill's "
|
||||
style 0-99 dim
|
||||
23| "instructions until it has been loaded. "
|
||||
style 0-37 dim
|
||||
24| <blank>
|
||||
25| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
26| "USER-ONLY SKILL LOADED "
|
||||
27| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
28| <blank>
|
||||
29| "Skill \"model-only-skill\" is not available for user invocation. "
|
||||
style 0-61 fg=yellow
|
||||
30| <blank>
|
||||
31| "/workspace/project deepseek-v4-flash ↑0 ↓0 3% context"
|
||||
style 0-51 fg=bright-magenta bold
|
||||
style 54-70 dim
|
||||
style 73-77 dim
|
||||
style 80-89 dim
|
||||
32| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
33-35| <blank>
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
name: model-only-skill
|
||||
description: Model-only assembled snapshot skill.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
MODEL-ONLY BODY MUST NOT LOAD
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
name: user-only-skill
|
||||
description: User-only assembled snapshot skill.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
USER-ONLY BODY
|
||||
@@ -1,30 +0,0 @@
|
||||
{"type":"session","version":0,"id":"main-session","createdAt":1784606400000,"cwd":"{{cwd}}"}
|
||||
{"type":"turn/start","seq":0,"time":1784606400000,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1784606400000,"data":{"content":[{"type":"text","text":"what's the workdir?"}],"source":{"kind":"user"},"role":"user","id":"3fdc2885-1bea-4c6c-b4af-dbd5af7594f8"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1784606400000,"data":{"title":"what's the workdir?","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1784606400000,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1784606400000,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash","reasoningEffort":"high"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1784606400000,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," is"," asking"," about"," the"," current"," working"," directory","."," Let"," me"," check"," using"," p","wd","."]}}
|
||||
{"type":"assistant/chunk","seq":23,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":24,"time0":1784606400000,"data":{"turn":1,"step":1,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_AqoWTncquNel5ZHsJHOo7491","name":"bash","args":["","{","\"","command","\"",": ","\"","p","wd","\"",", ","\"","description","\"",": ","\"","Print"," current"," working"," directory","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":46,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user is asking about the current working directory. Let me check using pwd."}}}}
|
||||
{"type":"assistant/chunk","seq":47,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_AqoWTncquNel5ZHsJHOo7491","name":"bash","arguments":"{\"command\": \"pwd\", \"description\": \"Print current working directory\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":48,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3332,"outputTokens":80,"cacheReadTokens":0,"reasoningTokens":17}}}}
|
||||
{"type":"assistant/chunk","seq":49,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":50,"time":1784606400000,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user is asking about the current working directory. Let me check using pwd."},{"type":"tool-call","id":"call_00_AqoWTncquNel5ZHsJHOo7491","name":"bash","arguments":"{\"command\": \"pwd\", \"description\": \"Print current working directory\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"063a9245-32c3-4551-9ace-d43f10ed5582"},"usage":{"inputTokens":3332,"outputTokens":80,"cacheReadTokens":0,"reasoningTokens":17}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":51,"time":1784606400000,"data":{"turn":1,"step":1,"callId":"call_00_AqoWTncquNel5ZHsJHOo7491","name":"bash","arguments":"{\"command\": \"pwd\", \"description\": \"Print current working directory\"}"}}
|
||||
{"type":"tool/result","seq":52,"time":1784606400000,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_AqoWTncquNel5ZHsJHOo7491"},"content":[{"type":"tool-result","toolCallId":"call_00_AqoWTncquNel5ZHsJHOo7491","content":[{"type":"text","text":"{{cwd}}\n"}],"isError":false}],"role":"user","id":"16086d3b-6dfa-4970-a06e-78561475af8c"}},"sourceEventSeqs":[51],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":53,"time":1784606400000,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":54,"time":1784606400000,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":55,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":56,"time0":1784606400000,"data":{"turn":1,"step":2,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," current"," working"," directory"," is"," `/","private","/t","mp","/d","sh","-t","ui","-s","n","apshot","-source","-check","out","-work","dir","-","61","I","wn","K","`."]}}
|
||||
{"type":"assistant/chunk","seq":83,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"text-chunks","seq0":84,"time0":1784606400000,"data":{"turn":1,"step":2,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," current"," working"," directory"," is",":\n\n","```\n","/","private","/t","mp","/d","sh","-t","ui","-s","n","apshot","-source","-check","out","-work","dir","-","61","I","wn","K","\n","```\n\n","Note"," that"," the"," Deep","Se","ek"," Har","ness"," SDK"," checkout"," is"," at"," `/","opt","/d","sh","-source","`"," —"," that","'s"," a"," separate"," location"," used"," for"," inspecting"," or"," extending"," D","SH"," itself",","," while"," the"," working"," directory"," above"," is"," the"," current"," working"," directory"," for"," this"," session","."]}}
|
||||
{"type":"assistant/chunk","seq":161,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The current working directory is `{{cwd}}`."}}}}
|
||||
{"type":"assistant/chunk","seq":162,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The current working directory is:\n\n```\n{{cwd}}\n```\n\nNote that the DeepSeek Harness SDK checkout is at `/opt/dsh-source` — that's a separate location used for inspecting or extending DSH itself, while the working directory above is the current working directory for this session."}}}}
|
||||
{"type":"assistant/chunk","seq":163,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":117,"outputTokens":105,"cacheReadTokens":3328,"reasoningTokens":27}}}}
|
||||
{"type":"assistant/chunk","seq":164,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":165,"time":1784606400000,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The current working directory is `{{cwd}}`."},{"type":"text","text":"The current working directory is:\n\n```\n{{cwd}}\n```\n\nNote that the DeepSeek Harness SDK checkout is at `/opt/dsh-source` — that's a separate location used for inspecting or extending DSH itself, while the working directory above is the current working directory for this session."}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"76c18b85-00aa-45f6-a32f-a2bd75412470"},"usage":{"inputTokens":117,"outputTokens":105,"cacheReadTokens":3328,"reasoningTokens":27}},"sourceEventSeqs":[55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":166,"time":1784606400000,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":167,"time":1784606400000,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,67 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "what's the workdir? — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=32 bufferRow=32
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " what's the workdir?"
|
||||
style 1-19 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "what's the workdir? "
|
||||
6| <blank>
|
||||
7| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
8| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
9| "The user is asking about the current working directory. Let me check using pwd. "
|
||||
style 0-78 dim italic
|
||||
10| <blank>
|
||||
11| "● Tool / bash / Print current working directory"
|
||||
style 0-46 fg=green
|
||||
12| "$ pwd "
|
||||
style 0-4 dim
|
||||
13| "/workspace/project "
|
||||
style 0-17 dim
|
||||
14| "[exit 0] "
|
||||
style 0-7 dim
|
||||
15| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
16| <blank>
|
||||
17| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
18| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
19| "The current working directory is /workspace/project. "
|
||||
style 0-32 dim italic
|
||||
style 33-84 fg=cyan
|
||||
style 85-85 dim italic
|
||||
20| "The current working directory is: "
|
||||
21| " "
|
||||
22| " "
|
||||
23| " /workspace/project "
|
||||
style 2-53 fg=cyan
|
||||
24| " "
|
||||
25| " "
|
||||
26| "Note that the DeepSeek Harness SDK checkout is at /opt/dsh-source — that's a separate location used "
|
||||
style 50-64 fg=cyan
|
||||
27| "for inspecting or extending DSH itself, while the working directory above is the current working "
|
||||
28| "directory for this session. "
|
||||
29| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
30| <blank>
|
||||
31| "/workspace/project deepseek-v4-flash ↑3.4k ↓185 cache 49% 3% c"
|
||||
style 0-51 fg=bright-magenta bold
|
||||
style 54-70 dim
|
||||
style 73-93 dim
|
||||
style 96-99 dim
|
||||
32| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
33-35| <blank>
|
||||
@@ -1,31 +0,0 @@
|
||||
{"type":"session","version":0,"id":"b0f1f758-dcf0-474e-851d-e62c11ec0a09","createdAt":1783352057652,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":1783352057655,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783352057655,"data":{"content":[{"type":"text","text":"Use the todo_write tool to record a plan with exactly three todos: \"read the code\" (in_progress), \"write the fix\" (pending), \"run the tests\" (pending). Send all three in one todo_write call. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":1783352057657,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":3,"time":1783352057657,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1783352058320,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":5,"time0":1783352058320,"data":{"turn":1,"step":1,"index":0,"dt":[106,40,1,0,0,0,17,0,0,0,1,26,1,1,0,0,1,26,0,31,1,25,0,0,0,29,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," use"," the"," todo","_write"," tool"," to"," record"," a"," plan"," with"," exactly"," three"," todos"," in"," the"," specified"," status","es",","," then"," reply"," with"," \"","D","ONE","\"."]}}
|
||||
{"type":"assistant/chunk","seq":36,"time":1783352058717,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":37,"time0":1783352058717,"data":{"turn":1,"step":1,"index":1,"dt":[29,1,0,0,28,0,1,29,1,0,0,0,0,26,0,0,0,0,0,30,1,0,0,0,0,26,1,0,0,0,0,28,0,0,0,0,0,29,0,0,0,0,1,28,0,0,0,1,0,27,1,0,28],"id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","args":["","{","\"","t","odos","\"",": ","[","{\"","content","\":"," \"","read"," the"," code","\","," \"","status","\":"," \"","in","_pro","gress","\"},"," {\"","content","\":"," \"","write"," the"," fix","\","," \"","status","\":"," \"","pending","\"},"," {\"","content","\":"," \"","run"," the"," tests","\","," \"","status","\":"," \"","pending","\"","}]","}"]}}
|
||||
{"type":"assistant/chunk","seq":91,"time":1783352059095,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the todo_write tool to record a plan with exactly three todos in the specified statuses, then reply with \"DONE\"."}}}}
|
||||
{"type":"assistant/chunk","seq":92,"time":1783352059096,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"read the code\", \"status\": \"in_progress\"}, {\"content\": \"write the fix\", \"status\": \"pending\"}, {\"content\": \"run the tests\", \"status\": \"pending\"}]}"}}}}
|
||||
{"type":"assistant/chunk","seq":93,"time":1783352059096,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2913,"outputTokens":121,"cacheReadTokens":0,"reasoningTokens":31}}}}
|
||||
{"type":"assistant/chunk","seq":94,"time":1783352059096,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":95,"time":1783352059099,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the todo_write tool to record a plan with exactly three todos in the specified statuses, then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"read the code\", \"status\": \"in_progress\"}, {\"content\": \"write the fix\", \"status\": \"pending\"}, {\"content\": \"run the tests\", \"status\": \"pending\"}]}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":2913,"outputTokens":121,"cacheReadTokens":0,"reasoningTokens":31}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":96,"time":1783352059099,"data":{"turn":1,"step":1,"callId":"call_00_fjAnBThbDjxepBtp3hDt3264","name":"todo_write","arguments":"{\"todos\": [{\"content\": \"read the code\", \"status\": \"in_progress\"}, {\"content\": \"write the fix\", \"status\": \"pending\"}, {\"content\": \"run the tests\", \"status\": \"pending\"}]}"}}
|
||||
{"type":"todo/write","seq":97,"time":1783352059100,"data":{"todos":[{"content":"read the code","status":"in_progress"},{"content":"write the fix","status":"pending"},{"content":"run the tests","status":"pending"}]}}
|
||||
{"type":"tool/result","seq":98,"time":1783352059101,"data":{"turn":1,"step":1,"callId":"call_00_fjAnBThbDjxepBtp3hDt3264","content":[{"type":"text","text":"Updated todo list: 2 pending, 1 in progress, 0 completed."}],"isError":false},"sourceEventSeqs":[96],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":99,"time":1783352059101,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":100,"time":1783352059102,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":101,"time":1783352059732,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":102,"time0":1783352059733,"data":{"turn":1,"step":2,"index":0,"dt":[102,28,0,1,0,28,0,1,0,27,0,1,0,0,29,0,0,0,1,0],"texts":["The"," todos"," have"," been"," written"," successfully","."," Now"," I"," just"," need"," to"," reply"," with"," the"," single"," word"," \"","D","ONE","\"."]}}
|
||||
{"type":"assistant/chunk","seq":123,"time":1783352059979,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":124,"time":1783352059979,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}}
|
||||
{"type":"assistant/chunk","seq":125,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}}
|
||||
{"type":"assistant/chunk","seq":126,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The todos have been written successfully. Now I just need to reply with the single word \"DONE\"."}}}}
|
||||
{"type":"assistant/chunk","seq":127,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":128,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":237,"outputTokens":24,"cacheReadTokens":2816,"reasoningTokens":21}}}}
|
||||
{"type":"assistant/chunk","seq":129,"time":1783352059980,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":130,"time":1783352059981,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The todos have been written successfully. Now I just need to reply with the single word \"DONE\"."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":237,"outputTokens":24,"cacheReadTokens":2816,"reasoningTokens":21}},"sourceEventSeqs":[101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":131,"time":1783352059981,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":132,"time":1783352059981,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -1,65 +0,0 @@
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Use the todo_write tool to — DSH TUI snapshot"
|
||||
cursor hidden column=7 viewportRow=31 bufferRow=31
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Use the todo_write tool to"
|
||||
style 1-26 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
5| "Use the todo_write tool to record a plan with exactly three todos: \"read the code\" (in_progress), "
|
||||
6| "\"write the fix\" (pending), \"run the tests\" (pending). Send all three in one todo_write call. Then "
|
||||
7| "reply with the single word DONE and stop. "
|
||||
8| <blank>
|
||||
9| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
10| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
11| "The user wants me to use the todo_write tool to record a plan with exactly three todos in the "
|
||||
style 0-99 dim italic
|
||||
12| "specified statuses, then reply with \"DONE\". "
|
||||
style 0-42 dim italic
|
||||
13| <blank>
|
||||
14| "● Tool / todo_write"
|
||||
style 0-18 fg=green
|
||||
15| "Update todo list "
|
||||
style 0-99 dim
|
||||
16| "Updated todo list: 2 pending, 1 in progress, 0 completed. "
|
||||
style 0-99 dim
|
||||
17| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
18| <blank>
|
||||
19| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
20| "Reasoning "
|
||||
style 0-8 dim italic
|
||||
21| "The todos have been written successfully. Now I just need to reply with the single word \"DONE\". "
|
||||
style 0-94 dim italic
|
||||
22| "DONE "
|
||||
23| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
|
||||
style 0-46 dim
|
||||
24-25| <blank>
|
||||
26| "Plan"
|
||||
style 0-3 fg=bright-magenta bold
|
||||
27| " ● read the code"
|
||||
style 2-2 fg=yellow
|
||||
28| " ○ write the fix"
|
||||
style 2-2 dim
|
||||
29| " ○ run the tests"
|
||||
style 2-2 dim
|
||||
30| "/workspace/project deepseek-v4-flash ↑3.1k ↓145 cache 47% 3% context"
|
||||
style 0-37 fg=bright-magenta bold
|
||||
style 40-56 dim
|
||||
style 59-79 dim
|
||||
style 82-91 dim
|
||||
31| " dsh ◍ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
32-35| <blank>
|
||||
@@ -5,8 +5,8 @@ import { describe, expect, it } from 'vitest'
|
||||
/**
|
||||
* Keyless smoke for the SOURCE `dsh` launcher: run `apps/cli/src/bin.ts`
|
||||
* with the exact production launch vector (`node --import tsx/esm`, the same
|
||||
* shape as `bin/dsh` and the root `dsh`/`demo:tui`/`demo:web` scripts) and
|
||||
* assert the piped-stdio TTY refusal. The Node compatibility matrix runs this
|
||||
* shape as `bin/dsh` and the root `dsh`/`demo:web` scripts) and assert the
|
||||
* required-config diagnostic. The Node compatibility matrix runs this
|
||||
* WHOLE file, so a Node release changing module hooks or TypeScript handling
|
||||
* breaks this gate instead of every developer's `pnpm dsh`; the built-bin
|
||||
* suite covers the published `lib/` entry, not this source chain.
|
||||
@@ -16,7 +16,7 @@ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const dshSourceBin = 'apps/cli/src/bin.ts'
|
||||
|
||||
describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
|
||||
it('boots the source entry and refuses pipes LOUD (non-zero exit + stderr)', async () => {
|
||||
it('boots the source entry and requires the raw config overlay', async () => {
|
||||
const result = await execa(process.execPath, ['--import', 'tsx/esm', dshSourceBin], {
|
||||
cwd: repoRoot,
|
||||
input: '',
|
||||
@@ -28,9 +28,7 @@ describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
|
||||
throw new Error(`dsh source launch did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
|
||||
}
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.stderr).toContain('requires stdin and stdout to be interactive TTYs')
|
||||
expect(result.stderr).toContain('dsh -p')
|
||||
// The refusal happens before any plugin mounts: stdout stays silent.
|
||||
expect(result.stderr).toContain('--config <path> is required')
|
||||
expect(result.stdout).toBe('')
|
||||
}, 30_000)
|
||||
})
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
overlay 120x30 rows=20
|
||||
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮"
|
||||
style 0-119 dim
|
||||
1| "│ ▄ DeepSeek Harness │"
|
||||
style 0-0 dim
|
||||
style 10-37 fg=blue
|
||||
style 75-90 fg=blue bold
|
||||
style 119-119 dim
|
||||
2| "│ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │"
|
||||
style 0-0 dim
|
||||
style 9-38 fg=blue
|
||||
style 119-119 dim
|
||||
3| "│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能 │"
|
||||
style 0-0 dim
|
||||
style 4-43 fg=blue
|
||||
style 119-119 dim
|
||||
4| "│ ▄███████████████████▄ ████████████▀ 仍待完善,体验难免有些粗糙。 │"
|
||||
style 0-0 dim
|
||||
style 4-43 fg=blue
|
||||
style 119-119 dim
|
||||
5| "│ ▄██████████████████████▄ ▀█████████▀ │"
|
||||
style 0-0 dim
|
||||
style 4-42 fg=blue
|
||||
style 119-119 dim
|
||||
6| "│ ▄███▀█████████████████████▄ ████▀▀ “如切如磋,如琢如磨。” │"
|
||||
style 0-0 dim
|
||||
style 6-41 fg=blue
|
||||
style 49-70 bold
|
||||
style 119-119 dim
|
||||
7| "│ ███ ▀▀█████████▀▀▀█████████▀ │"
|
||||
style 0-0 dim
|
||||
style 7-40 fg=blue
|
||||
style 119-119 dim
|
||||
8| "│ ███ ▀███████▀█ ▀███████ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的 │"
|
||||
style 0-0 dim
|
||||
style 7-39 fg=blue
|
||||
style 119-119 dim
|
||||
9| "│ ███▄ ▀███████▄ ▀█████▀ 问题,也可能促使我们重新审视,甚至推翻已有的设计。 │"
|
||||
style 0-0 dim
|
||||
style 7-39 fg=blue
|
||||
style 119-119 dim
|
||||
10| "│ ▀███ ▀██████████████ │"
|
||||
style 0-0 dim
|
||||
style 8-39 fg=blue
|
||||
style 119-119 dim
|
||||
11| "│ ▀███▄ ▀███████████▀ 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 │"
|
||||
style 0-0 dim
|
||||
style 8-38 fg=blue
|
||||
style 119-119 dim
|
||||
12| "│ ▀███▄ ▄▄▄ ▀████████▀ Session Log;如需关闭,请设置环境变量 DSH_TELEMETRY_DISABLED=1。另外 │"
|
||||
style 0-0 dim
|
||||
style 9-38 fg=blue
|
||||
style 119-119 dim
|
||||
13| "│ █████▄ ███▄▄ ▀█████▄▄ ,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈, │"
|
||||
style 0-0 dim
|
||||
style 9-38 fg=blue
|
||||
style 119-119 dim
|
||||
14| "│ ▀█████████████▄▄▄▄█▀█████▀ 都会帮助我们把它打磨得更好。 │"
|
||||
style 0-0 dim
|
||||
style 8-39 fg=blue
|
||||
style 119-119 dim
|
||||
15| "│ ▀▀███████████▀▀ │"
|
||||
style 0-0 dim
|
||||
style 12-34 fg=blue
|
||||
style 119-119 dim
|
||||
16| "├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤"
|
||||
style 0-119 dim
|
||||
17| "│ Enter 继续 │"
|
||||
style 0-0 dim
|
||||
style 54-64 fg=bright-magenta bold
|
||||
style 119-119 dim
|
||||
18| "│ │"
|
||||
style 0-0 dim
|
||||
style 119-119 dim
|
||||
19| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
style 0-119 dim
|
||||
@@ -1,76 +0,0 @@
|
||||
overlay 160x30 rows=20
|
||||
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮"
|
||||
style 0-159 dim
|
||||
1| "│ ▄ DeepSeek Harness │"
|
||||
style 0-0 dim
|
||||
style 10-37 fg=blue
|
||||
style 95-110 fg=blue bold
|
||||
style 159-159 dim
|
||||
2| "│ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │"
|
||||
style 0-0 dim
|
||||
style 9-38 fg=blue
|
||||
style 159-159 dim
|
||||
3| "│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完善,体验难免有些粗糙。 │"
|
||||
style 0-0 dim
|
||||
style 4-43 fg=blue
|
||||
style 159-159 dim
|
||||
4| "│ ▄███████████████████▄ ████████████▀ │"
|
||||
style 0-0 dim
|
||||
style 4-43 fg=blue
|
||||
style 159-159 dim
|
||||
5| "│ ▄██████████████████████▄ ▀█████████▀ “如切如磋,如琢如磨。” │"
|
||||
style 0-0 dim
|
||||
style 4-42 fg=blue
|
||||
style 49-70 bold
|
||||
style 159-159 dim
|
||||
6| "│ ▄███▀█████████████████████▄ ████▀▀ │"
|
||||
style 0-0 dim
|
||||
style 6-41 fg=blue
|
||||
style 159-159 dim
|
||||
7| "│ ███ ▀▀█████████▀▀▀█████████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已 │"
|
||||
style 0-0 dim
|
||||
style 7-40 fg=blue
|
||||
style 159-159 dim
|
||||
8| "│ ███ ▀███████▀█ ▀███████ 有的设计。 │"
|
||||
style 0-0 dim
|
||||
style 7-39 fg=blue
|
||||
style 159-159 dim
|
||||
9| "│ ███▄ ▀███████▄ ▀█████▀ │"
|
||||
style 0-0 dim
|
||||
style 7-39 fg=blue
|
||||
style 159-159 dim
|
||||
10| "│ ▀███ ▀██████████████ 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session Log;如需关闭,请设置环境变量 │"
|
||||
style 0-0 dim
|
||||
style 8-39 fg=blue
|
||||
style 159-159 dim
|
||||
11| "│ ▀███▄ ▀███████████▀ DSH_TELEMETRY_DISABLED=1。另外,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我 │"
|
||||
style 0-0 dim
|
||||
style 8-38 fg=blue
|
||||
style 159-159 dim
|
||||
12| "│ ▀███▄ ▄▄▄ ▀████████▀ 们把它打磨得更好。 │"
|
||||
style 0-0 dim
|
||||
style 9-38 fg=blue
|
||||
style 159-159 dim
|
||||
13| "│ █████▄ ███▄▄ ▀█████▄▄ │"
|
||||
style 0-0 dim
|
||||
style 9-38 fg=blue
|
||||
style 159-159 dim
|
||||
14| "│ ▀█████████████▄▄▄▄█▀█████▀ │"
|
||||
style 0-0 dim
|
||||
style 8-39 fg=blue
|
||||
style 159-159 dim
|
||||
15| "│ ▀▀███████████▀▀ │"
|
||||
style 0-0 dim
|
||||
style 12-34 fg=blue
|
||||
style 159-159 dim
|
||||
16| "├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤"
|
||||
style 0-159 dim
|
||||
17| "│ Enter 继续 │"
|
||||
style 0-0 dim
|
||||
style 74-84 fg=bright-magenta bold
|
||||
style 159-159 dim
|
||||
18| "│ │"
|
||||
style 0-0 dim
|
||||
style 159-159 dim
|
||||
19| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
style 0-159 dim
|
||||
@@ -1,31 +0,0 @@
|
||||
overlay 60x12 rows=10
|
||||
0| "╭──────────────────────────────────────────────────────────╮"
|
||||
style 0-59 dim
|
||||
1| "│ DeepSeek Harness │"
|
||||
style 0-0 dim
|
||||
style 22-37 fg=blue bold
|
||||
style 59-59 dim
|
||||
2| "│ │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
3| "│ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
4| "│ 测试阶段,功能仍待完善,体验难免有些粗糙。 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
5| "│ │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
6| "├──────────────────────────────────────────────────────────┤"
|
||||
style 0-59 dim
|
||||
7| "│ Enter 继续 │"
|
||||
style 0-0 dim
|
||||
style 24-34 fg=bright-magenta bold
|
||||
style 59-59 dim
|
||||
8| "│ ↑/↓ 滚动 ↓ │"
|
||||
style 0-0 dim
|
||||
style 24-35 dim
|
||||
style 59-59 dim
|
||||
9| "╰──────────────────────────────────────────────────────────╯"
|
||||
style 0-59 dim
|
||||
@@ -1,64 +0,0 @@
|
||||
overlay 60x30 rows=21
|
||||
0| "╭──────────────────────────────────────────────────────────╮"
|
||||
style 0-59 dim
|
||||
1| "│ DeepSeek Harness │"
|
||||
style 0-0 dim
|
||||
style 22-37 fg=blue bold
|
||||
style 59-59 dim
|
||||
2| "│ │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
3| "│ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
4| "│ 测试阶段,功能仍待完善,体验难免有些粗糙。 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
5| "│ │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
6| "│ “如切如磋,如琢如磨。” │"
|
||||
style 0-0 dim
|
||||
style 2-23 bold
|
||||
style 59-59 dim
|
||||
7| "│ │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
8| "│ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
9| "│ 使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
10| "│ 设计。 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
11| "│ │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
12| "│ 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
13| "│ 会上传所有 Session Log;如需关闭,请设置环境变量 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
14| "│ DSH_TELEMETRY_DISABLED=1。另外,如果您有任何反馈与建议, │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
15| "│ 请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
16| "│ 它打磨得更好。 │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
17| "├──────────────────────────────────────────────────────────┤"
|
||||
style 0-59 dim
|
||||
18| "│ Enter 继续 │"
|
||||
style 0-0 dim
|
||||
style 24-34 fg=bright-magenta bold
|
||||
style 59-59 dim
|
||||
19| "│ │"
|
||||
style 0-0 dim
|
||||
style 59-59 dim
|
||||
20| "╰──────────────────────────────────────────────────────────╯"
|
||||
style 0-59 dim
|
||||
@@ -1,89 +0,0 @@
|
||||
overlay 80x30 rows=27
|
||||
0| "╭──────────────────────────────────────────────────────────────────────────────╮"
|
||||
style 0-79 dim
|
||||
1| "│ ▄▄▄▄▄▄ ▄▄ │"
|
||||
style 0-0 dim
|
||||
style 33-46 fg=blue
|
||||
style 79-79 dim
|
||||
2| "│ ▄████████▄ ▀████▀ │"
|
||||
style 0-0 dim
|
||||
style 31-48 fg=blue
|
||||
style 79-79 dim
|
||||
3| "│ █▀▀▀▀███████▄██▀ │"
|
||||
style 0-0 dim
|
||||
style 32-47 fg=blue
|
||||
style 79-79 dim
|
||||
4| "│ █▄ ▀███ ▀███ │"
|
||||
style 0-0 dim
|
||||
style 32-46 fg=blue
|
||||
style 79-79 dim
|
||||
5| "│ ▀█▄ ▀█████ │"
|
||||
style 0-0 dim
|
||||
style 33-46 fg=blue
|
||||
style 79-79 dim
|
||||
6| "│ ▀█▄▄ █▄▄▀███▄ │"
|
||||
style 0-0 dim
|
||||
style 33-46 fg=blue
|
||||
style 79-79 dim
|
||||
7| "│ ▀▀▀▀▀▀ │"
|
||||
style 0-0 dim
|
||||
style 35-44 fg=blue
|
||||
style 79-79 dim
|
||||
8| "│ │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
9| "│ DeepSeek Harness │"
|
||||
style 0-0 dim
|
||||
style 32-47 fg=blue bold
|
||||
style 79-79 dim
|
||||
10| "│ │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
11| "│ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完 │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
12| "│ 善,体验难免有些粗糙。 │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
13| "│ │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
14| "│ “如切如磋,如琢如磨。” │"
|
||||
style 0-0 dim
|
||||
style 2-23 bold
|
||||
style 79-79 dim
|
||||
15| "│ │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
16| "│ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也 │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
17| "│ 可能促使我们重新审视,甚至推翻已有的设计。 │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
18| "│ │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
19| "│ 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
20| "│ Log;如需关闭,请设置环境变量 DSH_TELEMETRY_DISABLED=1。另外,如果您有任何反 │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
21| "│ 馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把它打磨得更 │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
22| "│ 好。 │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
23| "├──────────────────────────────────────────────────────────────────────────────┤"
|
||||
style 0-79 dim
|
||||
24| "│ Enter 继续 │"
|
||||
style 0-0 dim
|
||||
style 34-44 fg=bright-magenta bold
|
||||
style 79-79 dim
|
||||
25| "│ │"
|
||||
style 0-0 dim
|
||||
style 79-79 dim
|
||||
26| "╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
style 0-79 dim
|
||||
@@ -1,346 +0,0 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Context } from 'cordis'
|
||||
import { visibleWidth } from '@earendil-works/pi-tui'
|
||||
import {
|
||||
type TuiOverlayHost,
|
||||
type TuiOverlayRequest,
|
||||
type TuiTheme,
|
||||
} from '@deepseek-ai/dsh-tui'
|
||||
import {
|
||||
acknowledgeTuiFirstRunWelcome,
|
||||
apply,
|
||||
hasTuiFirstRunWelcomeAcknowledgement,
|
||||
needsTuiFirstRunWelcomeAsciiArt,
|
||||
TuiFirstRunWelcomeComponent,
|
||||
tuiFirstRunWelcomeAcknowledgementPath,
|
||||
tuiFirstRunWelcomeArtTier,
|
||||
} from '../src/tui-onboarding/tui-first-run-welcome.ts'
|
||||
import {
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_COPY,
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE,
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
|
||||
} from '../src/tui-onboarding/tui-first-run-welcome-copy.ts'
|
||||
import { TUI_FIRST_RUN_WELCOME_WHALE } from '../src/tui-onboarding/tui-first-run-welcome-art.ts'
|
||||
|
||||
const mockDisposeRootAndExit = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@deepseek-ai/dsh-tui', async importOriginal => ({
|
||||
...await importOriginal<typeof import('@deepseek-ai/dsh-tui')>(),
|
||||
disposeRootAndExit: mockDisposeRootAndExit,
|
||||
}))
|
||||
|
||||
const identityTheme: TuiTheme = Object.freeze({
|
||||
text: (value: string) => value,
|
||||
brand: (value: string) => value,
|
||||
dim: (value: string) => value,
|
||||
accent: (value: string) => value,
|
||||
success: (value: string) => value,
|
||||
warning: (value: string) => value,
|
||||
error: (value: string) => value,
|
||||
bold: (value: string) => value,
|
||||
})
|
||||
|
||||
function hostFixture(rows: number): {
|
||||
host: TuiOverlayHost
|
||||
closed: () => boolean
|
||||
invalidations: () => number
|
||||
} {
|
||||
let closed = false
|
||||
let invalidations = 0
|
||||
const controller = new AbortController()
|
||||
return {
|
||||
host: Object.freeze({
|
||||
signal: controller.signal,
|
||||
viewport: Object.freeze({ columns: 160, rows }),
|
||||
theme: identityTheme,
|
||||
display: (value: string) => value,
|
||||
invalidate: () => { invalidations += 1 },
|
||||
close: () => { closed = true },
|
||||
}),
|
||||
closed: () => closed,
|
||||
invalidations: () => invalidations,
|
||||
}
|
||||
}
|
||||
|
||||
const copy = TUI_FIRST_RUN_WELCOME_NOTICE_COPY[TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE]
|
||||
const openingSentence = `${copy.paragraphs[0]!.split('。', 1)[0]}。`
|
||||
const temporaryHomes: string[] = []
|
||||
|
||||
function artAnchor(tier: keyof typeof TUI_FIRST_RUN_WELCOME_WHALE): string {
|
||||
return TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[tier === 'full' ? 2 : 0]!.trim()
|
||||
}
|
||||
|
||||
function withoutWhitespace(value: string): string {
|
||||
return value.replace(/\s/gu, '')
|
||||
}
|
||||
|
||||
async function temporaryHome(prefix: string): Promise<string> {
|
||||
const home = await mkdtemp(join(tmpdir(), prefix))
|
||||
temporaryHomes.push(home)
|
||||
return home
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
mockDisposeRootAndExit.mockClear()
|
||||
await Promise.all(temporaryHomes.splice(0).map(home => rm(home, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
describe('TUI first-run welcome acknowledgement', () => {
|
||||
it('publishes one immutable per-version marker safely across concurrent acknowledgements', async () => {
|
||||
const home = await temporaryHome('dsh-tui-welcome-ack-')
|
||||
expect(await hasTuiFirstRunWelcomeAcknowledgement(home)).toBe(false)
|
||||
|
||||
await Promise.all(Array.from({ length: 8 }, () => acknowledgeTuiFirstRunWelcome(home)))
|
||||
|
||||
expect(await hasTuiFirstRunWelcomeAcknowledgement(home)).toBe(true)
|
||||
const info = await stat(tuiFirstRunWelcomeAcknowledgementPath(home, TUI_FIRST_RUN_WELCOME_NOTICE_VERSION))
|
||||
expect(info.isFile()).toBe(true)
|
||||
if (process.platform !== 'win32') expect(info.mode & 0o777).toBe(0o600)
|
||||
})
|
||||
|
||||
it('treats a notice-version bump as a new one-time acknowledgement', async () => {
|
||||
const home = await temporaryHome('dsh-tui-welcome-version-')
|
||||
await acknowledgeTuiFirstRunWelcome(home)
|
||||
const nextVersion = TUI_FIRST_RUN_WELCOME_NOTICE_VERSION + 1
|
||||
|
||||
expect(await hasTuiFirstRunWelcomeAcknowledgement(home, nextVersion)).toBe(false)
|
||||
await acknowledgeTuiFirstRunWelcome(home, nextVersion)
|
||||
expect(await hasTuiFirstRunWelcomeAcknowledgement(home, nextVersion)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a malformed marker instead of silently acknowledging it', async () => {
|
||||
const home = await temporaryHome('dsh-tui-welcome-malformed-')
|
||||
await mkdir(tuiFirstRunWelcomeAcknowledgementPath(home, TUI_FIRST_RUN_WELCOME_NOTICE_VERSION), {
|
||||
recursive: true,
|
||||
})
|
||||
await expect(hasTuiFirstRunWelcomeAcknowledgement(home)).rejects.toThrow('is not a file')
|
||||
await expect(acknowledgeTuiFirstRunWelcome(home)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('detects only explicit ASCII-only terminal environments', () => {
|
||||
expect(needsTuiFirstRunWelcomeAsciiArt({ TERM: 'dumb' })).toBe(true)
|
||||
expect(needsTuiFirstRunWelcomeAsciiArt({ LC_ALL: 'C' })).toBe(true)
|
||||
expect(needsTuiFirstRunWelcomeAsciiArt({ LC_CTYPE: 'POSIX' })).toBe(true)
|
||||
expect(needsTuiFirstRunWelcomeAsciiArt({ LANG: 'C' })).toBe(true)
|
||||
expect(needsTuiFirstRunWelcomeAsciiArt({ LANG: 'en_US.UTF-8' })).toBe(false)
|
||||
expect(typeof needsTuiFirstRunWelcomeAsciiArt()).toBe('boolean')
|
||||
})
|
||||
})
|
||||
|
||||
describe('TUI first-run welcome composition', () => {
|
||||
it('pins the supplied official icon and exact Chinese copy at their owner boundaries', async () => {
|
||||
const icon = (await readFile(new URL('../assets/deepseek-color.svg', import.meta.url), 'utf8')).trimEnd()
|
||||
expect(createHash('sha256').update(icon).digest('hex'))
|
||||
.toBe('deba5f98a5c1796e20fcac3149bcd7eb8a32f0bdd04d048819400b1f28bd1439')
|
||||
expect(createHash('sha256').update(copy.paragraphs.join('\n')).digest('hex'))
|
||||
.toBe('99f9a828b4f083b28de21bf5e03f939c00238531e765db78911957c44c6e98da')
|
||||
expect(TUI_FIRST_RUN_WELCOME_NOTICE_COPY.en).toBe(copy)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ columns: 60, inner: 50, rows: 30, tier: undefined },
|
||||
{ columns: 80, inner: 68, rows: 30, tier: 'minimal' },
|
||||
{ columns: 100, inner: 84, rows: 34, tier: 'compact' },
|
||||
{ columns: 120, inner: 104, rows: 30, tier: 'full' },
|
||||
{ columns: 160, inner: 140, rows: 30, tier: 'full' },
|
||||
] as const)('renders the responsive composition at $columns columns without overdraw', ({ inner, rows, tier }) => {
|
||||
const fixture = hostFixture(rows)
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {}, () => {})
|
||||
const renderWidth = inner + 4
|
||||
const lines = component.render(renderWidth)
|
||||
|
||||
expect(tuiFirstRunWelcomeArtTier(inner, rows)).toBe(tier)
|
||||
expect(lines.every(line => visibleWidth(line) <= renderWidth)).toBe(true)
|
||||
if (tier === undefined) {
|
||||
expect(lines.join('\n')).not.toMatch(/[▀▄█]/u)
|
||||
} else {
|
||||
expect(lines.join('\n')).toContain(artAnchor(tier))
|
||||
}
|
||||
const rendered = lines.join('\n')
|
||||
const optOut = copy.paragraphs.at(-1)!.match(/[A-Z_]+=1/u)![0]
|
||||
expect(rendered).not.toContain(copy.scrollHint)
|
||||
expect(rendered).toContain(copy.paragraphs.at(-1)!.match(/[A-Za-z]+ [A-Za-z]+/u)![0])
|
||||
expect(rendered).toContain(optOut)
|
||||
expect(lines.join('\n')).toContain(`Enter ${copy.continueLabel}`)
|
||||
expect(lines.length).toBeLessThanOrEqual(Math.floor(rows * 0.9))
|
||||
expect(lines.length).toBeGreaterThan(5)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ inner: 68, rows: 14, tier: undefined },
|
||||
{ inner: 68, rows: 17, tier: undefined },
|
||||
{ inner: 68, rows: 18, tier: 'minimal' },
|
||||
{ inner: 84, rows: 21, tier: 'minimal' },
|
||||
{ inner: 84, rows: 22, tier: 'compact' },
|
||||
] as const)('degrades art to preserve the action at $rows rows', ({ inner, rows, tier }) => {
|
||||
const fixture = hostFixture(rows)
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {}, () => {})
|
||||
const lines = component.render(inner + 4)
|
||||
expect(tuiFirstRunWelcomeArtTier(inner, rows)).toBe(tier)
|
||||
expect(lines.length).toBeLessThanOrEqual(Math.floor(rows * 0.9))
|
||||
expect(lines.join('\n')).toContain(`Enter ${copy.continueLabel}`)
|
||||
})
|
||||
|
||||
it('drops the whale at low height while keeping prose, scrolling, and Enter reachable', () => {
|
||||
const fixture = hostFixture(10)
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {}, () => {})
|
||||
const initial = component.render(54).join('\n')
|
||||
expect(tuiFirstRunWelcomeArtTier(50, 10)).toBeUndefined()
|
||||
expect(initial).toContain(openingSentence)
|
||||
expect(initial).toContain(`Enter ${copy.continueLabel}`)
|
||||
|
||||
component.handleInput('\x1b[F')
|
||||
const end = component.render(54).join('\n')
|
||||
expect(withoutWhitespace(end)).toContain(withoutWhitespace(copy.paragraphs.at(-1)!.slice(-7)))
|
||||
expect(end).toContain(`Enter ${copy.continueLabel}`)
|
||||
|
||||
for (const key of ['\x1b[A', '\x1b[B', '\x1b[5~', '\x1b[6~', '\x1b[H', 'x']) {
|
||||
component.handleInput(key)
|
||||
}
|
||||
component.invalidate()
|
||||
})
|
||||
|
||||
it('renders a tiny viewport and a quotation-only paragraph without overdraw', () => {
|
||||
const fixture = hostFixture(5)
|
||||
const quoteOnly = { ...copy, paragraphs: ['“如切如磋,如琢如磨。”'] }
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, quoteOnly, async () => {}, () => {})
|
||||
const lines = component.render(2)
|
||||
expect(lines.every(line => visibleWidth(line) <= 6)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the side-by-side composition aligned when prose outgrows the full raster', () => {
|
||||
const fixture = hostFixture(40)
|
||||
const longCopy = { ...copy, paragraphs: [copy.paragraphs.join(' ').repeat(4)] }
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, longCopy, async () => {}, () => {})
|
||||
const lines = component.render(100)
|
||||
expect(lines.length).toBeGreaterThan(TUI_FIRST_RUN_WELCOME_WHALE.full.unicode.length)
|
||||
expect(lines.every(line => visibleWidth(line) <= 100)).toBe(true)
|
||||
component.handleInput('\x1b[F')
|
||||
expect(component.render(100).join('\n')).toContain(copy.title)
|
||||
})
|
||||
|
||||
it('renders the bit-equivalent ASCII icon fallback for an explicitly non-Unicode terminal', () => {
|
||||
const fixture = hostFixture(30)
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {}, () => {}, true)
|
||||
const rendered = component.render(72).join('\n')
|
||||
expect(rendered).toContain(TUI_FIRST_RUN_WELCOME_WHALE.minimal.ascii[0]!.trim())
|
||||
expect(rendered).not.toMatch(/[▀▄█]/u)
|
||||
})
|
||||
|
||||
it.each(['full', 'compact', 'minimal'] as const)('keeps the $tier ASCII raster bit-equivalent', (tier) => {
|
||||
const mapped = TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode.map(line => Array.from(line).map((cell) => {
|
||||
if (cell === '▀') return "'"
|
||||
if (cell === '▄') return '_'
|
||||
if (cell === '█') return '#'
|
||||
return cell
|
||||
}).join(''))
|
||||
expect(mapped).toEqual(TUI_FIRST_RUN_WELCOME_WHALE[tier].ascii)
|
||||
})
|
||||
|
||||
it('ignores Escape and acknowledges only Enter before closing', async () => {
|
||||
const fixture = hostFixture(30)
|
||||
const acknowledge = vi.fn(async () => {})
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, acknowledge, () => {})
|
||||
component.render(72)
|
||||
|
||||
component.handleInput('\x1b')
|
||||
await Promise.resolve()
|
||||
expect(acknowledge).not.toHaveBeenCalled()
|
||||
expect(fixture.closed()).toBe(false)
|
||||
|
||||
component.handleInput('\r')
|
||||
await vi.waitFor(() => { expect(fixture.closed()).toBe(true) })
|
||||
expect(acknowledge).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps the notice eligible when Ctrl+C or Ctrl+D requests a normal exit', async () => {
|
||||
const fixture = hostFixture(30)
|
||||
const acknowledge = vi.fn(async () => {})
|
||||
const exit = vi.fn()
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, acknowledge, exit)
|
||||
component.handleInput('\x03')
|
||||
component.handleInput('\x04')
|
||||
expect(exit).toHaveBeenCalledTimes(2)
|
||||
expect(acknowledge).not.toHaveBeenCalled()
|
||||
expect(fixture.closed()).toBe(false)
|
||||
})
|
||||
|
||||
it('does not start a second acknowledgement while the first Enter is pending', async () => {
|
||||
const fixture = hostFixture(30)
|
||||
const pending = Promise.withResolvers<undefined>()
|
||||
const acknowledge = vi.fn(async () => pending.promise)
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, acknowledge, () => {})
|
||||
component.render(72)
|
||||
|
||||
component.handleInput('\r')
|
||||
component.handleInput('\r')
|
||||
component.handleInput('\x1b[B')
|
||||
expect(component.render(72).join('\n')).toContain(copy.saving)
|
||||
expect(acknowledge).toHaveBeenCalledOnce()
|
||||
|
||||
pending.resolve(undefined)
|
||||
await vi.waitFor(() => { expect(fixture.closed()).toBe(true) })
|
||||
})
|
||||
|
||||
it('keeps the overlay open after a persistence failure and lets Enter retry', async () => {
|
||||
const fixture = hostFixture(30)
|
||||
let attempts = 0
|
||||
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {
|
||||
attempts += 1
|
||||
if (attempts === 1) throw new Error('disk unavailable')
|
||||
}, () => {})
|
||||
component.render(72)
|
||||
|
||||
component.handleInput('\r')
|
||||
await vi.waitFor(() => {
|
||||
expect(component.render(72).join('\n')).toContain(copy.saveError)
|
||||
})
|
||||
expect(fixture.closed()).toBe(false)
|
||||
|
||||
component.handleInput('\r')
|
||||
await vi.waitFor(() => { expect(fixture.closed()).toBe(true) })
|
||||
expect(attempts).toBe(2)
|
||||
expect(fixture.invalidations()).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it('opens through the TUI extension and uses the launcher-owned acknowledgement closure', async () => {
|
||||
const home = await temporaryHome('dsh-tui-welcome-apply-')
|
||||
let request: TuiOverlayRequest | undefined
|
||||
let disposePending: (() => Promise<void>) | undefined
|
||||
const ctx = {
|
||||
effect(register: () => () => Promise<void>) {
|
||||
disposePending = register()
|
||||
return () => {}
|
||||
},
|
||||
tui: {
|
||||
openOverlay(value: TuiOverlayRequest) {
|
||||
request = value
|
||||
return {} as never
|
||||
},
|
||||
},
|
||||
} as unknown as Context
|
||||
apply(ctx, { dshHome: home })
|
||||
expect(request?.options).toEqual({
|
||||
width: '100%',
|
||||
maxHeight: '90%',
|
||||
anchor: 'center',
|
||||
margin: 0,
|
||||
})
|
||||
|
||||
const fixture = hostFixture(30)
|
||||
const component = request?.create(fixture.host)
|
||||
expect(component).toBeInstanceOf(TuiFirstRunWelcomeComponent)
|
||||
component?.handleInput?.('\x03')
|
||||
expect(mockDisposeRootAndExit).toHaveBeenCalledWith(ctx, 0)
|
||||
component?.handleInput?.('\r')
|
||||
await disposePending?.()
|
||||
expect(await hasTuiFirstRunWelcomeAcknowledgement(home)).toBe(true)
|
||||
|
||||
apply(ctx, { dshHome: home, asciiArt: true })
|
||||
expect(request?.create(fixture.host).render(72).join('\n'))
|
||||
.toContain(TUI_FIRST_RUN_WELCOME_WHALE.minimal.ascii[0]!.trim())
|
||||
})
|
||||
})
|
||||
@@ -1,879 +0,0 @@
|
||||
import { createUserMessage, createMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS } from '@deepseek-ai/dsh-loader-smoke'
|
||||
import { PREPARED_ENTRY_FILENAME, prepareDshPlugin } from '@deepseek-ai/dsh-repository-plugin'
|
||||
import { packChunkRuns, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import { logPath, toHeaderLine } from '../../../packages/session-persistence/session-persistence-jsonl/src/format.ts'
|
||||
import { runTuiPtySmoke, type TuiPtySmokeOptions } from './pty-harness.ts'
|
||||
import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-terminal.ts'
|
||||
import {
|
||||
acknowledgeTuiFirstRunWelcome,
|
||||
hasTuiFirstRunWelcomeAcknowledgement,
|
||||
} from '../src/tui-onboarding/tui-first-run-welcome.ts'
|
||||
import {
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_COPY,
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE,
|
||||
} from '../src/tui-onboarding/tui-first-run-welcome-copy.ts'
|
||||
import { TUI_FIRST_RUN_WELCOME_WHALE } from '../src/tui-onboarding/tui-first-run-welcome-art.ts'
|
||||
|
||||
const dshBinScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url))
|
||||
// `--config` layers an overlay over the shared base, so the default surface
|
||||
// needs no config argument at all; these are the overlays under test.
|
||||
const scriptedConfigPath = fileURLToPath(new URL('./fixtures/tui-scripted.cordis.yml', import.meta.url))
|
||||
// An overlay whose `llm-pi-ai` config fails validation, so an entry rejects
|
||||
// while the TUI already holds the terminal.
|
||||
const invalidProviderConfigPath = fileURLToPath(new URL('./fixtures/tui-invalid-provider.cordis.yml', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const firstRunSnapshots = fileURLToPath(new URL('./tui-first-run-snapshots/', import.meta.url))
|
||||
const synchronizedFrameEnd = '\x1b[?2026l'
|
||||
// Artifact mode gives the inner PTY driver 60 seconds and its execa owner a
|
||||
// five-second backstop. Keep Vitest outside both deadlines so the harness can
|
||||
// report its own marker, exit, and cleanup failure instead of being cut off.
|
||||
const PTY_SMOKE_TEST_TIMEOUT_MS = process.env.DSH_EXAMPLE_MODE === 'lib'
|
||||
? 75_000
|
||||
: LOADER_SMOKE_TEST_TIMEOUT_MS
|
||||
|
||||
/**
|
||||
* Seed the isolated process workspace: ordinary files land in `cwd`, personal
|
||||
* files in the Harness home (`.dsh`), and skill bundles under the agents
|
||||
* home's `skills/` root — the same trees `$DSH_HOME` /
|
||||
* `$DSH_AGENTS_HOME` point the child at.
|
||||
*/
|
||||
function seedWorkspace(
|
||||
files: {
|
||||
workspace?: Record<string, string>
|
||||
personal?: Record<string, string>
|
||||
skills?: Record<string, string>
|
||||
},
|
||||
): (cwd: string) => Promise<void> {
|
||||
return async (cwd) => {
|
||||
for (const [name, content] of Object.entries(files.workspace ?? {})) {
|
||||
const file = join(cwd, name)
|
||||
await mkdir(dirname(file), { recursive: true })
|
||||
await writeFile(file, content)
|
||||
}
|
||||
for (const [name, content] of Object.entries(files.personal ?? {})) {
|
||||
const file = join(cwd, '.dsh', name)
|
||||
await mkdir(dirname(file), { recursive: true })
|
||||
await writeFile(file, content)
|
||||
}
|
||||
for (const [name, content] of Object.entries(files.skills ?? {})) {
|
||||
const file = join(cwd, '.agents', 'skills', name)
|
||||
await mkdir(dirname(file), { recursive: true })
|
||||
await writeFile(file, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the real `prepareDshPlugin` over an equivalent one-skill `.dsh-plugin`
|
||||
* package and return the generated wrapper text, so the smoke's cache-seeded
|
||||
* wrapper can never drift from the generator's template.
|
||||
*/
|
||||
async function generatePreparedWrapper(pluginName: string): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-smoke-wrapper-'))
|
||||
try {
|
||||
const plugin = join(root, '.dsh-plugin')
|
||||
await mkdir(join(root, 'skills', 'config-only-repository'), { recursive: true })
|
||||
await writeFile(join(root, 'skills', 'config-only-repository', 'SKILL.md'), [
|
||||
'---',
|
||||
'name: config-only-repository',
|
||||
'description: Generator input; the seeded cache copy owns the visible text.',
|
||||
'---',
|
||||
'',
|
||||
'Repository instructions.',
|
||||
'',
|
||||
].join('\n'))
|
||||
await mkdir(plugin, { recursive: true })
|
||||
await writeFile(join(plugin, 'package.json'), `${JSON.stringify({
|
||||
name: pluginName,
|
||||
version: '0.0.0',
|
||||
dsh: { skills: ['../skills'] },
|
||||
}, undefined, 2)}\n`)
|
||||
await prepareDshPlugin(plugin)
|
||||
return await readFile(join(plugin, PREPARED_ENTRY_FILENAME), 'utf8')
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
/** Seed one real plaintext JSONL session for the `/resume` selector and host handoff smoke. */
|
||||
async function seedResumeSession(cwd: string): Promise<void> {
|
||||
const sessionCwd = realpathSync.native(cwd)
|
||||
const id = SessionId('resume-target')
|
||||
const meta: SessionHeader = { version: 0, id, createdAt: 1_700_000_000_000, cwd: sessionCwd }
|
||||
const events: SessionEvent[] = [
|
||||
{ type: 'turn/start', seq: 0, time: 1_700_000_000_001, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
|
||||
{ type: 'user/message', seq: 1, time: 1_700_000_000_002, data: createUserMessage({
|
||||
content: [{ type: 'text', text: 'persisted prompt' }], source: { kind: 'user' },
|
||||
}), surfaceOp: 'append' },
|
||||
{ type: 'step/start', seq: 2, time: 1_700_000_000_003, data: { turn: 1, step: 1 } },
|
||||
{ type: 'request/header', seq: 3, time: 1_700_000_000_004, data: { header: { config: { provider: 'tui-scripted', model: 'tui-scripted-model' } }, reason: 'initial' } },
|
||||
{ type: 'assistant/message', seq: 4, time: 1_700_000_000_005, data: {
|
||||
turn: 1, step: 1,
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'persisted answer' }],
|
||||
source: {
|
||||
kind: 'model',
|
||||
...{ provider: 'tui-scripted', model: 'tui-scripted-model' },
|
||||
},
|
||||
}),
|
||||
}, surfaceOp: 'append' },
|
||||
{ type: 'step/end', seq: 5, time: 1_700_000_000_006, data: { turn: 1, step: 1 } },
|
||||
{ type: 'session/title', seq: 6, time: 1_700_000_000_007, data: { title: 'Resume selector design', messageSeqs: [1], source: { kind: 'fallback' } } },
|
||||
{ type: 'todo/write', seq: 7, time: 1_700_000_000_008, data: { todos: [{ content: 'Preserve restored state', status: 'in_progress' }] } },
|
||||
{ type: 'turn/end', seq: 8, time: 1_700_000_000_009, data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
]
|
||||
const file = logPath(join(cwd, '.sessions'), sessionCwd, id, 'none')
|
||||
await mkdir(dirname(file), { recursive: true })
|
||||
await writeFile(file, [
|
||||
JSON.stringify(toHeaderLine(meta)),
|
||||
...packChunkRuns(events).map(record => JSON.stringify(record)),
|
||||
'',
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
/** Model-visible startup context from the first request in the workspace's persisted session log. */
|
||||
interface LoggedRequestContext {
|
||||
/** The system prompt string the launcher sends. */
|
||||
system: string
|
||||
/** The durable skill-catalog message serialized to text. */
|
||||
skillCatalog: string
|
||||
}
|
||||
|
||||
async function readLoggedRequestContext(cwd: string): Promise<LoggedRequestContext> {
|
||||
const sessionsDir = join(cwd, '.sessions')
|
||||
const entries = await readdir(sessionsDir, { recursive: true })
|
||||
// A single keyless run writes one session log; the source section is global, so any log carries it.
|
||||
const logRelPath = entries.find(name => name.endsWith('.jsonl'))
|
||||
if (logRelPath === undefined) throw new Error(`no session log written under ${sessionsDir}`)
|
||||
const lines = (await readFile(join(sessionsDir, logRelPath), 'utf8')).split('\n').filter(Boolean)
|
||||
let skillCatalog = ''
|
||||
for (const line of lines) {
|
||||
const event = JSON.parse(line) as SessionEvent
|
||||
if (
|
||||
event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === 'dsh-tool-skill'
|
||||
) {
|
||||
skillCatalog = JSON.stringify(event.data.content)
|
||||
}
|
||||
if (event.type === 'request/header') {
|
||||
return {
|
||||
system: event.data.header.system ?? '',
|
||||
skillCatalog,
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`session log ${logRelPath} has no request/header event`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared defaults: the keyless key and the dsh bin. Each case supplies either
|
||||
* `configArgs: []` (boot the shipped composition, `base.cordis.yml` +
|
||||
* `tui.cordis.yml`, with no flags) or `configPath` (an overlay layered over that
|
||||
* same base through `--config`).
|
||||
*/
|
||||
function smoke(overrides: Partial<TuiPtySmokeOptions> & {
|
||||
label: string
|
||||
showFirstRunWelcome?: boolean
|
||||
}): Promise<string> {
|
||||
const { showFirstRunWelcome = false, prepare, ...options } = overrides
|
||||
return runTuiPtySmoke({
|
||||
tempDirPrefix: 'dsh-tui-smoke-',
|
||||
binScript: dshBinScript,
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DEEPSEEK_API_KEY: 'keyless-tui-no-call',
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
LANG: 'en_US.UTF-8',
|
||||
LC_ALL: 'en_US.UTF-8',
|
||||
LC_CTYPE: 'en_US.UTF-8',
|
||||
TERM: 'xterm-256color',
|
||||
},
|
||||
// Artifact CI builds and smokes concurrently on a contended runner.
|
||||
...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}),
|
||||
...options,
|
||||
prepare: async (cwd) => {
|
||||
if (!showFirstRunWelcome) await acknowledgeTuiFirstRunWelcome(join(cwd, '.dsh'))
|
||||
await prepare?.(cwd)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const firstRunCopy = TUI_FIRST_RUN_WELCOME_NOTICE_COPY[TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE]
|
||||
const firstRunOpeningSentence = `${firstRunCopy.paragraphs[0]!.split('。', 1)[0]}。`
|
||||
|
||||
function firstRunArtAnchor(tier: keyof typeof TUI_FIRST_RUN_WELCOME_WHALE): string {
|
||||
return TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[tier === 'full' ? 2 : 0]!.trim()
|
||||
}
|
||||
|
||||
/** Keep only the overlay rows, excluding platform-specific scrollback and the underlying TUI. */
|
||||
function overlaySnapshot(snapshot: string, columns: number, rows: number): string {
|
||||
const blocks: string[][] = []
|
||||
for (const line of snapshot.split('\n')) {
|
||||
if (/^\d+(?:-\d+)?~?\| /u.test(line)) blocks.push([line])
|
||||
else if (line.startsWith(' style ') && blocks.length > 0) blocks.at(-1)?.push(line)
|
||||
}
|
||||
const first = blocks.findIndex(block => block[0]?.includes('╭') === true)
|
||||
const last = blocks.findIndex((block, index) => index >= first && block[0]?.includes('╰') === true)
|
||||
if (first < 0 || last < first) throw new Error('first-run PTY snapshot has no complete overlay frame')
|
||||
const overlay = blocks.slice(first, last + 1).flatMap((block, index) => [
|
||||
block[0]!.replace(/^\d+(?:-\d+)?(~)?\|/u, `${String(index)}$1|`),
|
||||
...block.slice(1),
|
||||
])
|
||||
return [`overlay ${String(columns)}x${String(rows)} rows=${String(last - first + 1)}`, ...overlay, ''].join('\n')
|
||||
}
|
||||
|
||||
/** Project the first synchronized PTY frame containing `marker` into an overlay-only snapshot. */
|
||||
async function firstRunFrameSnapshot(
|
||||
output: string,
|
||||
marker: string,
|
||||
columns: number,
|
||||
rows: number,
|
||||
): Promise<string> {
|
||||
const markerIndex = output.indexOf(marker)
|
||||
if (markerIndex < 0) throw new Error(`first-run PTY output has no marker ${JSON.stringify(marker)}`)
|
||||
const frameEnd = output.indexOf(synchronizedFrameEnd, markerIndex)
|
||||
if (frameEnd < 0) throw new Error(`first-run PTY output has no complete frame after ${JSON.stringify(marker)}`)
|
||||
const terminal = new HeadlessTerminal(columns, rows)
|
||||
try {
|
||||
terminal.write(output.slice(0, frameEnd + synchronizedFrameEnd.length))
|
||||
return overlaySnapshot(await terminal.snapshot(), columns, rows)
|
||||
} finally {
|
||||
await terminal.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
// The scripted conversation switches to the pro model first: the scripted
|
||||
// adapter proves routing + prompt variables by rejecting tool-ful calls on any
|
||||
// other route (see fixtures/tui-scripted-llm.ts).
|
||||
const SELECT_PRO_MODEL = [
|
||||
{ waitFor: 'scripted TUI ready.', send: '/model\r' },
|
||||
{ waitFor: 'Select model', send: '\x1b[B\x1b[Z\r' },
|
||||
] as const
|
||||
const ANSWER_MULTI_WITH_CUSTOM = ' \tRelease notes\r'
|
||||
|
||||
describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
|
||||
it.each([
|
||||
{ columns: 60, tier: undefined },
|
||||
{ columns: 80, tier: 'minimal' },
|
||||
{ columns: 120, tier: 'full' },
|
||||
{ columns: 160, tier: 'full' },
|
||||
] as const)('renders and acknowledges the responsive first-run composition at $columns columns', async ({ columns, tier }) => {
|
||||
const output = await smoke({
|
||||
label: `dsh first-run welcome ${String(columns)} columns`,
|
||||
tempDirPrefix: `dsh-tui-welcome-${String(columns)}-`,
|
||||
configPath: scriptedConfigPath,
|
||||
showFirstRunWelcome: true,
|
||||
expectedExitCode: 0,
|
||||
columns,
|
||||
rows: 30,
|
||||
actions: [
|
||||
{
|
||||
waitFor: `Enter ${firstRunCopy.continueLabel}`,
|
||||
send: '\r\x03',
|
||||
},
|
||||
],
|
||||
inspect: async (cwd) => {
|
||||
expect(await hasTuiFirstRunWelcomeAcknowledgement(join(cwd, '.dsh'))).toBe(true)
|
||||
const entries = await readdir(join(cwd, '.sessions'), { recursive: true })
|
||||
const logs = entries.filter(name => name.endsWith('.jsonl'))
|
||||
for (const log of logs) {
|
||||
const stored = await readFile(join(cwd, '.sessions', log), 'utf8')
|
||||
expect(stored).not.toContain(firstRunCopy.paragraphs[0])
|
||||
}
|
||||
},
|
||||
})
|
||||
await expect(await firstRunFrameSnapshot(output, firstRunOpeningSentence, columns, 30))
|
||||
.toMatchFileSnapshot(join(firstRunSnapshots, `${String(columns)}-columns.expected.txt`))
|
||||
if (tier === undefined) {
|
||||
expect(output).not.toContain(TUI_FIRST_RUN_WELCOME_WHALE.minimal.unicode[0]!.trim())
|
||||
} else {
|
||||
expect(output).toContain(firstRunArtAnchor(tier))
|
||||
}
|
||||
expect(output).toContain(`Enter ${firstRunCopy.continueLabel}`)
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('keeps prose and Enter reachable in a low-height real PTY after dropping the whale', async () => {
|
||||
const output = await smoke({
|
||||
label: 'dsh low-height first-run welcome',
|
||||
tempDirPrefix: 'dsh-tui-welcome-low-',
|
||||
configPath: scriptedConfigPath,
|
||||
showFirstRunWelcome: true,
|
||||
expectedExitCode: 0,
|
||||
columns: 60,
|
||||
rows: 12,
|
||||
actions: [
|
||||
{ waitFor: firstRunOpeningSentence, send: '\x1b[F' },
|
||||
{
|
||||
waitFor: `Enter ${firstRunCopy.continueLabel}`,
|
||||
occurrence: 2,
|
||||
send: '\r\x03',
|
||||
},
|
||||
],
|
||||
})
|
||||
await expect(await firstRunFrameSnapshot(output, firstRunOpeningSentence, 60, 12))
|
||||
.toMatchFileSnapshot(join(firstRunSnapshots, '60-columns-low-height.expected.txt'))
|
||||
expect(output).toContain(firstRunCopy.title)
|
||||
expect(output).toContain(firstRunOpeningSentence)
|
||||
expect(output).toContain('企业微信群')
|
||||
expect(output).toContain(`Enter ${firstRunCopy.continueLabel}`)
|
||||
expect(output).not.toContain(TUI_FIRST_RUN_WELCOME_WHALE.minimal.unicode[0]!.trim())
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('shows once and skips the second launch under the same DSH_HOME', async () => {
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'dsh-tui-welcome-twice-'))
|
||||
try {
|
||||
const first = await smoke({
|
||||
label: 'dsh first welcome launch',
|
||||
tempDirPrefix: 'unused-',
|
||||
cwd,
|
||||
configPath: scriptedConfigPath,
|
||||
showFirstRunWelcome: true,
|
||||
expectedExitCode: 0,
|
||||
actions: [
|
||||
{ waitFor: `Enter ${firstRunCopy.continueLabel}`, send: '\r\x03' },
|
||||
],
|
||||
})
|
||||
expect(first).toContain(firstRunCopy.title)
|
||||
|
||||
const second = await smoke({
|
||||
label: 'dsh second welcome launch',
|
||||
tempDirPrefix: 'unused-',
|
||||
cwd,
|
||||
configPath: scriptedConfigPath,
|
||||
showFirstRunWelcome: true,
|
||||
expectedExitCode: process.platform === 'win32' ? 0 : -15,
|
||||
actions: [{ waitFor: 'main-session-', signal: 'SIGTERM' }],
|
||||
})
|
||||
expect(second).not.toContain(firstRunOpeningSentence)
|
||||
expect(second).not.toContain(`Enter ${firstRunCopy.continueLabel}`)
|
||||
} finally {
|
||||
await rm(cwd, { recursive: true, force: true })
|
||||
}
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it.skipIf(process.platform === 'win32')('keeps the notice eligible when the process exits before Enter', async () => {
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'dsh-tui-welcome-abort-'))
|
||||
try {
|
||||
await smoke({
|
||||
label: 'dsh aborted welcome launch',
|
||||
tempDirPrefix: 'unused-',
|
||||
cwd,
|
||||
configPath: scriptedConfigPath,
|
||||
showFirstRunWelcome: true,
|
||||
expectedExitCode: -15,
|
||||
actions: [{ waitFor: firstRunOpeningSentence, signal: 'SIGTERM' }],
|
||||
inspect: async (workspace) => {
|
||||
expect(await hasTuiFirstRunWelcomeAcknowledgement(join(workspace, '.dsh'))).toBe(false)
|
||||
},
|
||||
})
|
||||
|
||||
const next = await smoke({
|
||||
label: 'dsh welcome after aborted launch',
|
||||
tempDirPrefix: 'unused-',
|
||||
cwd,
|
||||
configPath: scriptedConfigPath,
|
||||
showFirstRunWelcome: true,
|
||||
expectedExitCode: 0,
|
||||
actions: [
|
||||
{ waitFor: `Enter ${firstRunCopy.continueLabel}`, send: '\r\x03' },
|
||||
],
|
||||
})
|
||||
expect(next).toContain(firstRunOpeningSentence)
|
||||
} finally {
|
||||
await rm(cwd, { recursive: true, force: true })
|
||||
}
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('boots pi-tui, sweeps the borderless banner in, enters plan mode, and restores the terminal', async () => {
|
||||
// With no configured welcome the borderless banner sweeps in left-to-right;
|
||||
// the detail line's session id (`main-session-<uuid>`) renders only once
|
||||
// the sweep reaches it, so it marks a settled banner.
|
||||
const output = await smoke({
|
||||
label: 'dsh boot',
|
||||
configArgs: [],
|
||||
actions: [
|
||||
{ waitFor: 'main-session-', send: '/plan' },
|
||||
{ waitFor: '[off|message] — Enter or leave plan mode', send: '\r' },
|
||||
{ waitFor: 'Plan mode on. Use /plan off to leave.', send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).toContain('DEEPSEEK')
|
||||
expect(output).toContain('HARNESS')
|
||||
expect(output).toContain('main-session-')
|
||||
expect(output).toContain('[off|message] — Enter or leave plan mode')
|
||||
expect(output).toContain('Plan mode on. Use /plan off to leave.')
|
||||
// Borderless: no box-drawing frame around the banner.
|
||||
expect(output).not.toContain('╭')
|
||||
expect(output).not.toContain('╮')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
// The Loader mounts entries concurrently, so `ui-tui` can already own the
|
||||
// terminal when a sibling entry rejects on its config. Exiting without the
|
||||
// tree's own teardown left raw mode and bracketed paste set on the user's
|
||||
// shell, and the pending Device Attributes reply landed there as literal
|
||||
// text. The transactional mount must settle (an HMR initial-scan refresh
|
||||
// once deadlocked its rollback into a silent exit 13) so `boot` disposes
|
||||
// the tree — reaching the TUI's own shutdown — and rejects with the
|
||||
// labelled diagnostic.
|
||||
it('restores the terminal when a sibling entry fails to validate during boot', async () => {
|
||||
const output = await smoke({
|
||||
label: 'dsh invalid provider config',
|
||||
tempDirPrefix: 'dsh-tui-invalid-config-',
|
||||
configPath: invalidProviderConfigPath,
|
||||
expectedExitCode: 1,
|
||||
})
|
||||
expect(output).toContain('dsh: plugin tree failed to load:')
|
||||
expect(output).toContain('$.providers')
|
||||
// Bracketed paste is disabled again, which only `ProcessTerminal.stop()`
|
||||
// writes — proof the tree was disposed rather than exited out from under.
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('switches models, streams a response, answers a user-question dialog, and exits cleanly', async () => {
|
||||
const output = await smoke({
|
||||
label: 'dsh conversation',
|
||||
tempDirPrefix: 'dsh-tui-conversation-',
|
||||
configPath: scriptedConfigPath,
|
||||
actions: [
|
||||
...SELECT_PRO_MODEL,
|
||||
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: '/plan exercise the TUI\r' },
|
||||
// The question text first appears in the streamed tool-call card. Wait
|
||||
// for the dialog's input legend so Enter cannot arrive before it owns
|
||||
// terminal input when pre-dispatch policy yields.
|
||||
{
|
||||
waitFor: 'Tab custom answer • ↑/↓ navigate • Space toggle • Enter submit • Esc interrupt',
|
||||
send: ANSWER_MULTI_WITH_CUSTOM,
|
||||
},
|
||||
{ waitFor: 'Decision received. Scripted TUI run complete.', send: '' },
|
||||
// Session title: the first user message drives the first-message-llm
|
||||
// provider's tool-less title call; the scripted adapter answers it, the
|
||||
// accepted title lands in the log, and the TUI renders the terminal
|
||||
// window title as `<session title> — <configured title>` via OSC 0.
|
||||
// Gating /status on it keeps the assertion race-free; the diagnostics
|
||||
// card is then exercised through the same real Loader/PTY composition.
|
||||
{ waitFor: 'scripted session title — DeepSeek Harness', send: '/plan off\r' },
|
||||
{ waitFor: 'Plan mode off.', send: 'Confirm the scripted run left plan mode.\r' },
|
||||
{ waitFor: 'Default mode confirmed.', send: '/status\r' },
|
||||
{ waitFor: 'Session status', send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).toContain('I need one decision before I continue.')
|
||||
expect(output).toContain('Reasoning effort: Max.')
|
||||
expect(output).toContain('Plan mode on. Use /plan off to leave.')
|
||||
expect(output).toContain('Plan mode off.')
|
||||
expect(output).toContain('Default mode confirmed.')
|
||||
expect(output).toContain(String.raw`\x1b]2;MODEL_CONTROLLED\x07`)
|
||||
expect(output).toContain(String.raw`\x1b[999CMODEL_CURSOR`)
|
||||
expect(output).toContain(String.raw`\x9b31mMODEL_C1`)
|
||||
expect(output).not.toContain('\u001B]2;MODEL_CONTROLLED\u0007')
|
||||
expect(output).not.toContain('\u001B[999CMODEL_CURSOR')
|
||||
expect(output).not.toContain('\u009B31mMODEL_C1')
|
||||
expect(output).toContain('Safe')
|
||||
expect(output).toContain('Release notes')
|
||||
expect(output).toContain('\u001B]0;scripted session title — DeepSeek Harness\u0007')
|
||||
expect(output).toContain('Session status')
|
||||
expect(output).toContain('Title')
|
||||
expect(output).toContain('scripted session title')
|
||||
expect(output).toContain('Model')
|
||||
expect(output).toContain('tui-scripted/tui-scripted-model-pro')
|
||||
expect(output).toContain('KV cache')
|
||||
expect(output).toContain('Context')
|
||||
expect(output).toContain('128,000')
|
||||
expect(output).toContain('System prompt')
|
||||
expect(output).toContain('You are an AI agent powered by the DeepSeek Harness SDK.')
|
||||
expect(output).toContain('Registered tools')
|
||||
expect(output).toContain('ask_user_question')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('loads a local skill via /skill: and delivers its body to the model as a user turn', async () => {
|
||||
// The whole user-only invocation path in one keyless boot: `ctx.get('skills')`
|
||||
// resolves in the shipped tree, the client-side `/skill:` command parses,
|
||||
// and the local provider admits a model-disabled skill by the omitted
|
||||
// `user-invocable` default. The rendered `<skill name="…">` block reaches
|
||||
// the model — proven by the scripted adapter echoing the fixture's body
|
||||
// marker only when it arrives.
|
||||
const output = await smoke({
|
||||
label: 'dsh skill',
|
||||
tempDirPrefix: 'dsh-tui-skill-',
|
||||
configPath: scriptedConfigPath,
|
||||
prepare: seedWorkspace({
|
||||
skills: {
|
||||
'scripted-skill/SKILL.md': [
|
||||
'---',
|
||||
'name: scripted-skill',
|
||||
'description: Keyless PTY proof that the skill command loads a local skill into the conversation.',
|
||||
'disable-model-invocation: true',
|
||||
'---',
|
||||
'',
|
||||
'SCRIPTED SKILL BODY MARKER',
|
||||
'',
|
||||
].join('\n'),
|
||||
},
|
||||
}),
|
||||
actions: [
|
||||
...SELECT_PRO_MODEL,
|
||||
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: '/skill:scripted-skill\r' },
|
||||
{ waitFor: 'Scripted skill body received.', send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).not.toContain('[instructions]')
|
||||
expect(output).toContain('Scripted skill body received.')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('adds a watched local skill to live /skill: autocomplete without restarting', async () => {
|
||||
const skill = [
|
||||
'---',
|
||||
'name: hot-added-skill',
|
||||
'description: HOT_ADDED_COMPLETION_MARKER',
|
||||
'---',
|
||||
'',
|
||||
'Hot-added body.',
|
||||
'',
|
||||
].join('\n')
|
||||
const output = await smoke({
|
||||
label: 'tui-agent hot-added skill autocomplete',
|
||||
tempDirPrefix: 'tui-agent-hot-skill-',
|
||||
configPath: scriptedConfigPath,
|
||||
actions: [
|
||||
{
|
||||
waitFor: 'scripted TUI ready.',
|
||||
writeFile: {
|
||||
path: '.agents/skills/hot-added-skill/SKILL.md',
|
||||
content: skill,
|
||||
},
|
||||
send: '/skill:hot',
|
||||
},
|
||||
{ waitFor: 'HOT_ADDED_COMPLETION_MARKER', send: '\x03/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).toContain('HOT_ADDED_COMPLETION_MARKER')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it.skipIf(process.env.DSH_EXAMPLE_MODE === 'lib')('fuzzy-completes an @file path without reading or submitting the file', async () => {
|
||||
const output = await smoke({
|
||||
label: 'dsh file autocomplete',
|
||||
tempDirPrefix: 'dsh-tui-file-autocomplete-',
|
||||
// Source-plane PTY coverage complements the deterministic package-level
|
||||
// autocomplete tests. Artifact CI omits this timing-sensitive terminal
|
||||
// rendering assertion; built boot is covered by the neighboring cases.
|
||||
configArgs: [],
|
||||
prepare: seedWorkspace({
|
||||
workspace: {
|
||||
'src/terminal-special-case.ts': 'export const marker = true\n',
|
||||
'src/other.ts': 'export const other = true\n',
|
||||
},
|
||||
}),
|
||||
actions: [
|
||||
{ waitFor: 'main-session-', send: '@tsc' },
|
||||
{ waitFor: 'File · terminal-special-case.t', send: '\t' },
|
||||
{ waitFor: '@src/terminal-special-case.ts', send: '\x03/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).toContain('File · terminal-special-case.t')
|
||||
expect(output).toContain('@src/terminal-special-case.ts')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
})
|
||||
|
||||
describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
|
||||
it('shows the terminal-local notice over a resumed session without changing its log', async () => {
|
||||
let originalLineCount = 0
|
||||
const output = await smoke({
|
||||
label: 'dsh first-run notice on resume',
|
||||
tempDirPrefix: 'dsh-tui-welcome-resume-',
|
||||
binScript: dshBinScript,
|
||||
configArgs: ['--resume', 'resume-target', '--config', scriptedConfigPath],
|
||||
showFirstRunWelcome: true,
|
||||
expectedExitCode: 0,
|
||||
prepare: async (cwd) => {
|
||||
await seedResumeSession(cwd)
|
||||
const before = await readFile(logPath(
|
||||
join(cwd, '.sessions'),
|
||||
realpathSync.native(cwd),
|
||||
SessionId('resume-target'),
|
||||
'none',
|
||||
), 'utf8')
|
||||
originalLineCount = before.split('\n').filter(Boolean).length
|
||||
},
|
||||
actions: [
|
||||
{ waitFor: `Enter ${firstRunCopy.continueLabel}`, send: '\r\x03' },
|
||||
],
|
||||
inspect: async (cwd) => {
|
||||
const after = await readFile(logPath(
|
||||
join(cwd, '.sessions'),
|
||||
realpathSync.native(cwd),
|
||||
SessionId('resume-target'),
|
||||
'none',
|
||||
), 'utf8')
|
||||
expect(after).not.toContain(firstRunCopy.paragraphs[0])
|
||||
const appended = after.split('\n').filter(Boolean).slice(originalLineCount)
|
||||
.map(line => JSON.parse(line) as SessionEvent)
|
||||
expect(appended).not.toContainEqual(expect.objectContaining({ type: 'user/message' }))
|
||||
expect(appended).not.toContainEqual(expect.objectContaining({ type: 'turn/start' }))
|
||||
},
|
||||
})
|
||||
expect(output).toContain(firstRunOpeningSentence)
|
||||
expect(output).toContain('Resume selector design — DeepSeek Harness')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('exec-replaces the TUI for /resume and restores the same session state', async () => {
|
||||
const output = await smoke({
|
||||
label: 'dsh in-place resume',
|
||||
tempDirPrefix: 'dsh-in-place-resume-',
|
||||
binScript: dshBinScript,
|
||||
configPath: scriptedConfigPath,
|
||||
prepare: seedResumeSession,
|
||||
actions: [
|
||||
{ waitFor: 'scripted TUI ready.', send: '/resume\r' },
|
||||
{ waitFor: 'Resume selector design', send: 'Resume selector design' },
|
||||
{ waitFor: '⌕ Resume selector design', send: '\r' },
|
||||
{ waitFor: 'Preserve restored state', send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
const released = output.indexOf('\u001B[?2004l')
|
||||
const restored = output.indexOf('Resume selector design — DeepSeek Harness')
|
||||
expect(released).toBeGreaterThanOrEqual(0)
|
||||
expect(restored).toBeGreaterThan(released)
|
||||
expect(output).toContain('Preserve restored state')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('boots the shipped default config with no arguments and no personal overlay', async () => {
|
||||
const output = await smoke({
|
||||
label: 'dsh default boot',
|
||||
tempDirPrefix: 'dsh-default-boot-',
|
||||
binScript: dshBinScript,
|
||||
configArgs: [],
|
||||
actions: [{ waitFor: 'main-session-', send: '/exit\r' }],
|
||||
})
|
||||
expect(output).toContain('DEEPSEEK')
|
||||
expect(output).toContain('main-session-')
|
||||
expect(output).not.toContain('╭')
|
||||
expect(output).not.toContain('╮')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('applies the personal overlay: config.yaml patches an overlay-inserted row, the invoking directory\'s .env feeds its !!js, and the home .env stays out of the environment', async () => {
|
||||
// The whole personal-config chain in one boot, plus the environment layer
|
||||
// it deliberately excludes. config.yaml patches the `tui` row — a row the
|
||||
// SURFACE OVERLAY inserted, not one the base declares — proving a later
|
||||
// patch list reaches a row an earlier one inserted. The single `!!js`
|
||||
// expression prefers the PERSONAL variable, so the welcome can only render
|
||||
// the project value while the harness home's .env — the credential store
|
||||
// of `dsh-credentials-local` — is NOT hoisted into `process.env`; hoisting
|
||||
// it would make every stored key read as a read-only launch override on
|
||||
// the next run and hand it to every subprocess the agent starts.
|
||||
const output = await smoke({
|
||||
label: 'dsh personal overlay',
|
||||
tempDirPrefix: 'dsh-personal-overlay-',
|
||||
binScript: dshBinScript,
|
||||
configArgs: [],
|
||||
prepare: seedWorkspace({
|
||||
workspace: { '.env': 'DSH_PROJECT_WELCOME=PROJECT OVERLAY READY.\n' },
|
||||
personal: {
|
||||
'.env': 'DSH_PERSONAL_WELCOME=HOME ENV LEAKED.\n',
|
||||
'config.yaml': [
|
||||
'- id: workspace-context',
|
||||
' disabled: true',
|
||||
'- id: tui',
|
||||
' config:',
|
||||
" sessionId: !!js configuredAgentIdentities?.main?.id ?? 'main'",
|
||||
' welcome: !!js process.env.DSH_PERSONAL_WELCOME ?? process.env.DSH_PROJECT_WELCOME',
|
||||
'',
|
||||
].join('\n'),
|
||||
},
|
||||
}),
|
||||
actions: [{ waitFor: 'PROJECT OVERLAY READY.', send: '/exit\r' }],
|
||||
})
|
||||
expect(output).toContain('PROJECT OVERLAY READY.')
|
||||
expect(output).not.toContain('HOME ENV LEAKED.')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('loads a cached repository Plugin from personal config alone', async () => {
|
||||
const source = 'github:fixture/repository#fixed-ref'
|
||||
const specifier = `${source}&path:/.dsh-plugin`
|
||||
const key = createHash('sha256').update(specifier).digest('hex')
|
||||
const packageRoot = `cache/repository-plugins/${key}/node_modules/repository`
|
||||
// Produced by the real generator (prepareDshPlugin over an equivalent
|
||||
// .dsh-plugin package) rather than hand-written, so a wrapper-template
|
||||
// change cannot leave this smoke exercising a stale shape. The cache
|
||||
// LAYOUT below (sha256 key, marker, node_modules/repository) remains a
|
||||
// deliberate external pin of the durable on-disk format.
|
||||
const wrapper = await generatePreparedWrapper('config-only-fixture')
|
||||
const output = await smoke({
|
||||
label: 'dsh personal repository Plugin',
|
||||
tempDirPrefix: 'dsh-personal-repository-plugin-',
|
||||
binScript: dshBinScript,
|
||||
configArgs: [],
|
||||
prepare: seedWorkspace({
|
||||
personal: {
|
||||
'config.yaml': [
|
||||
'- id: repository-plugins',
|
||||
" name: '@deepseek-ai/dsh-repository-plugin'",
|
||||
' config:',
|
||||
' repositories:',
|
||||
` - '${source}'`,
|
||||
'',
|
||||
].join('\n'),
|
||||
[`cache/repository-plugins/${key}/.repository-cache.json`]: `${JSON.stringify({ specifier })}\n`,
|
||||
[`${packageRoot}/dsh-plugin.mjs`]: wrapper,
|
||||
[`${packageRoot}/dsh-plugin-assets/skills/0/config-only-repository/SKILL.md`]: [
|
||||
'---',
|
||||
'name: config-only-repository',
|
||||
'description: CONFIG_ONLY_REPOSITORY_SKILL',
|
||||
'---',
|
||||
'',
|
||||
'Repository instructions.',
|
||||
'',
|
||||
].join('\n'),
|
||||
},
|
||||
}),
|
||||
actions: [
|
||||
{ waitFor: 'main-session-', send: '/skill:config-only' },
|
||||
{ waitFor: 'CONFIG_ONLY_REPOSITORY_SKILL', send: '\x03/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).toContain('CONFIG_ONLY_REPOSITORY_SKILL')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('fails loud instead of booting when the personal config.yaml is invalid', async () => {
|
||||
const output = await smoke({
|
||||
label: 'dsh invalid personal config',
|
||||
tempDirPrefix: 'dsh-invalid-personal-',
|
||||
binScript: dshBinScript,
|
||||
configArgs: [],
|
||||
prepare: seedWorkspace({ personal: { 'config.yaml': 'id: not-a-list\n' } }),
|
||||
expectedExitCode: 1,
|
||||
})
|
||||
expect(output).toContain('must be a top-level YAML array of loader patch entries')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('routes the --resume flag into the launcher session-identity slot, failing loud on a missing id', async () => {
|
||||
// The flag path end to end: apps/cli parses `--resume missing-session`,
|
||||
// provides it as the launcher-owned identity on the boot context, and the
|
||||
// resume fails loud — proving the printed hint reaches the app's resume
|
||||
// intake with no config key and no environment variable.
|
||||
const output = await smoke({
|
||||
label: 'dsh resume flag failure',
|
||||
tempDirPrefix: 'dsh-resume-flag-',
|
||||
binScript: dshBinScript,
|
||||
configArgs: ['--resume', 'missing-session'],
|
||||
expectedExitCode: 1,
|
||||
})
|
||||
expect(output).toContain('ui-tui: session "missing-session" failed to start:')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('prints the launcher-owned resume command on exit, naming the booted config', async () => {
|
||||
// The exit line is built by apps/cli from this invocation, so it must carry
|
||||
// `--config`: a hint that omitted it would resume into the default tree.
|
||||
const output = await smoke({
|
||||
label: 'dsh goodbye message',
|
||||
tempDirPrefix: 'dsh-goodbye-',
|
||||
binScript: dshBinScript,
|
||||
configPath: scriptedConfigPath,
|
||||
actions: [{ waitFor: 'scripted TUI ready.', send: '/exit\r' }],
|
||||
})
|
||||
expect(output).toMatch(/To resume this session: dsh --resume=main-session-[0-9a-f-]{36} --config/)
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('keeps resume working when the personal overlay replaces the whole agent-loop config', async () => {
|
||||
// Loader patches replace a targeted `config` key wholesale, so a personal
|
||||
// overlay repointing the model route drops every identity key the shipped
|
||||
// row declared. Launcher-owned identity makes that unreachable: agent-loop
|
||||
// applies the launcher's id over whatever route survives.
|
||||
const output = await smoke({
|
||||
label: 'dsh overlay keeps resume',
|
||||
tempDirPrefix: 'dsh-overlay-resume-',
|
||||
binScript: dshBinScript,
|
||||
configArgs: [],
|
||||
prepare: seedWorkspace({
|
||||
personal: {
|
||||
'config.yaml': [
|
||||
'- id: workspace-context',
|
||||
' disabled: true',
|
||||
'- id: agent-loop',
|
||||
' config:',
|
||||
' agents:',
|
||||
' - id: main',
|
||||
' provider: deepseek-official',
|
||||
' model: deepseek-v4-flash',
|
||||
' cwd: !!js process.cwd()',
|
||||
'- id: tui',
|
||||
' config:',
|
||||
" sessionId: !!js configuredAgentIdentities?.main?.id ?? 'main'",
|
||||
' welcome: OVERLAY REPLACED THE CONFIG.',
|
||||
'',
|
||||
].join('\n'),
|
||||
},
|
||||
}),
|
||||
actions: [{ waitFor: 'OVERLAY REPLACED THE CONFIG.', send: '/exit\r' }],
|
||||
})
|
||||
expect(output).toMatch(/To resume this session: dsh --resume=main-session-[0-9a-f-]{36}/)
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('reports a failing bash command exactly once, as the terminal card exit pill', async () => {
|
||||
// The model-facing result ends in `[exit code: 3]`, which the terminal card
|
||||
// consumes into its own `[exit 3]` pill. Rendering both would report the same
|
||||
// exit twice, so the marker must not survive into the card body.
|
||||
const output = await smoke({
|
||||
label: 'dsh bash exit pill',
|
||||
tempDirPrefix: 'dsh-bash-exit-pill-',
|
||||
configPath: scriptedConfigPath,
|
||||
actions: [
|
||||
...SELECT_PRO_MODEL,
|
||||
{
|
||||
waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.',
|
||||
send: 'Run the failing scripted command.\r',
|
||||
},
|
||||
{ waitFor: 'Scripted bash failure observed.', send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
// The command really ran: its stdout is in the card body.
|
||||
expect(output).toContain('SCRIPTED_BASH_FAILED')
|
||||
expect(output).toContain('[exit 3]')
|
||||
expect(output).not.toContain('[exit code: 3]')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('distinguishes its source path from the current workdir and offers the bundled maintenance skills', async () => {
|
||||
// The launcher resolves the checkout root three hops up from apps/cli/{src,lib};
|
||||
// this test file sits an equal depth under the same root, so the same hop applies.
|
||||
// The source-path line explicitly distinguishes that checkout from the current workdir;
|
||||
// bundled skills reach the model through a durable user message, so each assertion
|
||||
// targets its own field.
|
||||
const sourceRoot = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
let context: LoggedRequestContext = { system: '', skillCatalog: '' }
|
||||
await smoke({
|
||||
label: 'dsh source-path prompt',
|
||||
tempDirPrefix: 'dsh-source-path-',
|
||||
binScript: dshBinScript,
|
||||
configPath: scriptedConfigPath,
|
||||
actions: [
|
||||
...SELECT_PRO_MODEL,
|
||||
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: 'exercise the TUI\r' },
|
||||
{ waitFor: 'How should the scripted run proceed?', send: ANSWER_MULTI_WITH_CUSTOM },
|
||||
{ waitFor: 'Decision received. Scripted TUI run complete.', send: '/exit\r' },
|
||||
],
|
||||
inspect: async (cwd) => { context = await readLoggedRequestContext(cwd) },
|
||||
})
|
||||
expect(context.system).toContain(`The DeepSeek Harness implementation checkout is at ${sourceRoot}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.`)
|
||||
expect(context.skillCatalog).toContain("- `dsh-customize`: Customize or maintain any dsh source checkout — the one powering the current DSH process, the installed `dsh` command, or a sibling dsh/deepseek-harness clone. Use before any requested action that alters such a checkout's files or git state. Read-only questions that only inspect the checkout do not trigger this. Do not edit the personal staging checkout directly.")
|
||||
expect(context.skillCatalog).toContain('- `dsh-upgrade`: Upgrades a source-installed, personally customized DSH checkout to upstream master while preserving local changes and an unchanged rollback worktree. Use when the user asks to update or upgrade DSH.')
|
||||
expect(context.skillCatalog).toContain('- `dsh-upstream-customization`: Classifies personal DSH customizations for upstream contribution and, after explicit per-feature approval, rebuilds one on upstream master and opens a draft pull request. Use when the user asks to contribute, publish, or upstream a local DSH change, or asks whether one is worth proposing.')
|
||||
}, PTY_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
@@ -1,852 +0,0 @@
|
||||
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { scrubRequestHeaders, tokenizeSessionFixtureCwd } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import * as AgentCore from '@deepseek-ai/dsh-agent-spine-demo'
|
||||
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
|
||||
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import WorkerCodeRuntime from '@deepseek-ai/dsh-code-runtime-worker'
|
||||
import CommandService from '@deepseek-ai/dsh-commands'
|
||||
import * as CommandCompact from '@deepseek-ai/dsh-command-compact'
|
||||
import { BasicCompactService } from '@deepseek-ai/dsh-compact-basic'
|
||||
import type { SummarizationInput } from '@deepseek-ai/dsh-compact-basic/src/summarizer.ts'
|
||||
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { installLlmReplay, parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
||||
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import { packChunkRuns, SessionId, type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SubagentService from '@deepseek-ai/dsh-subagent'
|
||||
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
|
||||
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
|
||||
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
|
||||
import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph'
|
||||
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
|
||||
import { createTuiChat, FILE_REFERENCE_PROMPT, TuiPromptService } from '@deepseek-ai/dsh-tui'
|
||||
import LocalSpillStore from '@deepseek-ai/dsh-spill-local'
|
||||
import * as SpillPolicy from '@deepseek-ai/dsh-spill-policy'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
|
||||
import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-terminal.ts'
|
||||
|
||||
const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
|
||||
// Keep pre-normalization layout widths identical across macOS and Linux.
|
||||
const SNAPSHOT_TMP_ROOT = process.platform === 'win32' ? tmpdir() : '/tmp'
|
||||
const PROVIDERS = [{ id: 'deepseek-official', models: [{ id: 'deepseek-v4-flash', contextWindow: 128_000 }] }]
|
||||
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi
|
||||
|
||||
type SnapshotMode = 'replay' | 'record' | 'refresh'
|
||||
type Composition = 'native' | 'code' | 'advanced'
|
||||
type ScenarioInteraction = 'skill-invocation-policy'
|
||||
|
||||
interface Scenario {
|
||||
name: string
|
||||
/** Replay fixture owned by an earlier scenario, for a derived presentation case. */
|
||||
fixture?: string
|
||||
composition: Composition
|
||||
expectedTools: string[]
|
||||
expectedEventCounts?: Record<string, number>
|
||||
childSessions?: number
|
||||
enterPlanMode?: boolean
|
||||
leavePlanModeAfterFirstTurn?: boolean
|
||||
recorded: boolean
|
||||
seedWorkspace?: boolean
|
||||
/** Add the launcher's model-visible DSH source checkout at this fixed path. */
|
||||
harnessSourceRoot?: string
|
||||
/** Replace the real `pwd` result with a portable fixed-length workspace path. */
|
||||
normalizePwdResult?: boolean
|
||||
/**
|
||||
* Load the opt-in `todo_write` tool for this scenario. The shipped TUI
|
||||
* config omits it, so only the todo-plan scenario (the enabled-path proof)
|
||||
* mounts it; the rest cover the default, todo-free composition.
|
||||
*/
|
||||
enableTodo?: boolean
|
||||
/**
|
||||
* Mount the spill stack (local backend + policy) with this inline cap, as the
|
||||
* shipped configs do. The dispatch-spill scenario proves the durable
|
||||
* `tool/code-dispatch` copy of an oversized sub-result is bounded to a
|
||||
* preview + locator while the program value stays whole.
|
||||
*/
|
||||
spillMaxInlineBytes?: number
|
||||
/** Run scenario-specific terminal input instead of replaying recorded user prompts. */
|
||||
interaction?: ScenarioInteraction
|
||||
/**
|
||||
* Mount a deterministic compaction backend plus `/compact`, then run the
|
||||
* human command with a held summary while a prompt and injected context
|
||||
* arrive. Proves queued input waits for the standalone bracket's durability
|
||||
* checkpoint instead of racing the replacement.
|
||||
*/
|
||||
manualCompact?: boolean
|
||||
}
|
||||
|
||||
const SCENARIOS: Scenario[] = [
|
||||
{
|
||||
name: 'multi-turn-conversation',
|
||||
composition: 'native',
|
||||
expectedTools: [],
|
||||
expectedEventCounts: { 'plan/mode': 2 },
|
||||
enterPlanMode: true,
|
||||
leavePlanModeAfterFirstTurn: true,
|
||||
recorded: true,
|
||||
},
|
||||
{
|
||||
name: 'queued-manual-compact',
|
||||
fixture: 'multi-turn-conversation',
|
||||
composition: 'native',
|
||||
expectedTools: [],
|
||||
recorded: false,
|
||||
manualCompact: true,
|
||||
},
|
||||
{
|
||||
name: 'todo-plan',
|
||||
composition: 'native',
|
||||
expectedTools: ['todo_write'],
|
||||
expectedEventCounts: { 'todo/write': 1 },
|
||||
recorded: true,
|
||||
enableTodo: true,
|
||||
},
|
||||
{
|
||||
name: 'bash-terminal-card',
|
||||
composition: 'native',
|
||||
expectedTools: ['bash'],
|
||||
recorded: true,
|
||||
},
|
||||
{
|
||||
name: 'source-checkout-workdir',
|
||||
composition: 'native',
|
||||
expectedTools: ['bash'],
|
||||
recorded: true,
|
||||
harnessSourceRoot: '/opt/dsh-source',
|
||||
normalizePwdResult: true,
|
||||
},
|
||||
{
|
||||
name: 'parallel-file-reads',
|
||||
composition: 'native',
|
||||
expectedTools: ['read', 'read'],
|
||||
recorded: true,
|
||||
seedWorkspace: true,
|
||||
},
|
||||
{
|
||||
name: 'skill-invocation-policy',
|
||||
composition: 'native',
|
||||
expectedTools: [],
|
||||
recorded: false,
|
||||
seedWorkspace: true,
|
||||
interaction: 'skill-invocation-policy',
|
||||
},
|
||||
{
|
||||
name: 'code-mode',
|
||||
composition: 'code',
|
||||
expectedTools: ['run_code'],
|
||||
expectedEventCounts: { 'tool/code-dispatch': 2 },
|
||||
recorded: true,
|
||||
},
|
||||
{
|
||||
name: 'code-mode-dispatch-spill',
|
||||
composition: 'code',
|
||||
expectedTools: ['run_code'],
|
||||
expectedEventCounts: { 'tool/code-dispatch-start': 1, 'tool/code-dispatch': 1 },
|
||||
recorded: true,
|
||||
spillMaxInlineBytes: 600,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-workflow',
|
||||
composition: 'native',
|
||||
expectedTools: ['workflow'],
|
||||
childSessions: 1,
|
||||
recorded: true,
|
||||
},
|
||||
{
|
||||
name: 'cordis-dynamic-toolchain',
|
||||
composition: 'advanced',
|
||||
expectedTools: ['cordis_mount', 'run_code', 'subagent', 'workflow', 'cordis_unmount'],
|
||||
expectedEventCounts: { 'tool/code-dispatch': 1 },
|
||||
childSessions: 2,
|
||||
recorded: false,
|
||||
},
|
||||
]
|
||||
|
||||
function snapshotModeFromEnv(value: string | undefined): SnapshotMode {
|
||||
if (value === undefined || value === '' || value === 'replay') return 'replay'
|
||||
if (value === 'record' || value === 'refresh') return value
|
||||
throw new Error(`DSH_SNAPSHOT must be replay, record, or refresh; got ${JSON.stringify(value)}`)
|
||||
}
|
||||
|
||||
const MODE = snapshotModeFromEnv(process.env.DSH_SNAPSHOT)
|
||||
const observedScenarios = new Set<string>()
|
||||
const workerState = Reflect.get(globalThis, '__vitest_worker__') as
|
||||
| { readonly config?: { readonly testNamePattern?: RegExp } }
|
||||
| undefined
|
||||
// Worker argv omits the parent CLI's `-t`; the serialized runner config is the
|
||||
// authoritative distinction between a focused replay and the full suite.
|
||||
const TEST_NAME_FILTERED = workerState?.config?.testNamePattern !== undefined
|
||||
|
||||
/**
|
||||
* Deterministic keyless summary that pauses so the scenario can submit a real
|
||||
* prompt and inject context while manual compaction holds turn admission.
|
||||
*/
|
||||
class DeferredSnapshotCompactService extends BasicCompactService {
|
||||
readonly summaryStarted = Promise.withResolvers<undefined>()
|
||||
readonly releaseSummary = Promise.withResolvers<undefined>()
|
||||
|
||||
override async summarize(
|
||||
_input: SummarizationInput,
|
||||
_agent: Agent,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ summary: [{ type: 'text'; text: string }]; provider: string; model: string }> {
|
||||
this.summaryStarted.resolve(undefined)
|
||||
await this.releaseSummary.promise
|
||||
signal?.throwIfAborted()
|
||||
return {
|
||||
summary: [{ type: 'text', text: 'Keyless manual compaction checkpoint.' }],
|
||||
provider: 'snapshot',
|
||||
model: 'snapshot-compactor',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Seed between-turn model-visible history without inventing a loop execution. */
|
||||
function seedCompactableHistory(agent: Agent): void {
|
||||
agent.inject(createUserMessage({
|
||||
content: [{ type: 'text', text: 'Older snapshot context. '.repeat(60) }],
|
||||
source: { kind: 'plugin', plugin: 'snapshot-seed' },
|
||||
}))
|
||||
}
|
||||
|
||||
function snapshotDisplayPath(displayPath: string, cwd: string, displayCwd: string): string {
|
||||
const rel = relative(cwd, displayPath)
|
||||
if (rel === '') return displayCwd
|
||||
if (isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`)) return displayPath
|
||||
return `${displayCwd}/${rel.split(sep).join('/')}`
|
||||
}
|
||||
|
||||
function scenarioDir(scenario: Scenario): string {
|
||||
return join(SNAPSHOTS_DIR, scenario.name)
|
||||
}
|
||||
|
||||
/** Directory owning the replay fixture: the scenario's own, or the one it derives from. */
|
||||
function fixtureDir(scenario: Scenario): string {
|
||||
return join(SNAPSHOTS_DIR, scenario.fixture ?? scenario.name)
|
||||
}
|
||||
|
||||
function childFixturePaths(scenario: Scenario): string[] {
|
||||
return Array.from(
|
||||
{ length: scenario.childSessions ?? 0 },
|
||||
(_, index) => join(fixtureDir(scenario), `session.${index + 1}.jsonl`),
|
||||
)
|
||||
}
|
||||
|
||||
function userPrompts(rawLog: string): string[] {
|
||||
return parseSessionLog(rawLog).flatMap((event) => {
|
||||
if (event.type !== 'user/message' || event.data.source.kind !== 'user') return []
|
||||
const text = event.data.content
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join('')
|
||||
return text.length > 0 ? [text] : []
|
||||
})
|
||||
}
|
||||
|
||||
function rawSessionLog(session: Session): string {
|
||||
return [
|
||||
JSON.stringify({ type: 'session', ...session.header }),
|
||||
...packChunkRuns(session.events).map(record => JSON.stringify(record)),
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
async function materializeFixtureCwd(fixtureFile: string, cwd: string, replayRoot: string): Promise<string> {
|
||||
const realized = join(replayRoot, basename(fixtureFile))
|
||||
await writeFile(realized, (await readFile(fixtureFile, 'utf8')).split('{{cwd}}').join(cwd))
|
||||
return realized
|
||||
}
|
||||
|
||||
function normalizeTerminalSnapshot(snapshot: string, cwd: string, displayCwd: string): string {
|
||||
return snapshot
|
||||
.split(`/private${cwd}`).join('/workspace/project')
|
||||
.split(displayCwd).join('/workspace/project')
|
||||
.split(cwd).join('/workspace/project')
|
||||
.replace(UUID_RE, '{{uuid}}')
|
||||
}
|
||||
|
||||
async function settleTerminal(terminal: HeadlessTerminal): Promise<void> {
|
||||
let stable = 0
|
||||
for (let attempt = 0; attempt < 20 && stable < 3; attempt++) {
|
||||
const before = terminal.frames
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
await terminal.flush()
|
||||
stable = terminal.frames === before ? stable + 1 : 0
|
||||
}
|
||||
if (stable < 3) throw new Error('TUI frames did not quiesce within 200ms')
|
||||
}
|
||||
|
||||
/** Bound deterministic in-process coordination waits with actionable state. */
|
||||
async function snapshotDeadline<T>(
|
||||
operation: Promise<T>,
|
||||
detail: () => string,
|
||||
): Promise<T> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
return await Promise.race([
|
||||
operation,
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
timer = setTimeout(() => { reject(new Error(detail())) }, 5_000)
|
||||
}),
|
||||
])
|
||||
} finally {
|
||||
if (timer !== undefined) clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
async function mountScenarioContext(
|
||||
scenario: Scenario,
|
||||
cwd: string,
|
||||
displayCwd: string,
|
||||
fixtureFile: string,
|
||||
childFiles: string[],
|
||||
replayRoot: string | undefined,
|
||||
): Promise<Context> {
|
||||
class SnapshotLocalFileSystem extends LocalFileSystem {
|
||||
override async resolve(
|
||||
path: string,
|
||||
opts?: { cwd?: string; signal?: AbortSignal },
|
||||
): Promise<Awaited<ReturnType<LocalFileSystem['resolve']>>> {
|
||||
const target = await super.resolve(path, opts)
|
||||
return { ...target, displayPath: snapshotDisplayPath(target.displayPath, cwd, displayCwd) }
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentCore, {
|
||||
agents: [],
|
||||
dshHome: join(cwd, '.dsh'),
|
||||
workspaceContext: false,
|
||||
tools: { mode: scenario.composition === 'code' ? 'code' : scenario.composition === 'advanced' ? 'both' : 'native' },
|
||||
skills: { local: { agentsHome: join(cwd, '.agents') } },
|
||||
})
|
||||
if (scenario.harnessSourceRoot !== undefined) addHarnessSourceSection(ctx, scenario.harnessSourceRoot)
|
||||
await ctx.plugin(TokenMeterService)
|
||||
if (scenario.manualCompact === true) {
|
||||
await ctx.plugin(DeferredSnapshotCompactService, { auto: false })
|
||||
}
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
await ctx.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 })
|
||||
await ctx.plugin(SnapshotLocalFileSystem, { cwd: '/' })
|
||||
await ctx.plugin(FsPolicy)
|
||||
await ctx.plugin(ToolFs)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
await ctx.plugin(TuiPromptService)
|
||||
// todo_write is opt-in: only the todo-plan scenario mounts it, matching the shipped
|
||||
// config that omits it. The other scenarios prove the default todo-free composition.
|
||||
if (scenario.enableTodo === true) await ctx.plugin(ToolTodo)
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
|
||||
await ctx.plugin(ToolSubagent, { provider: 'spawn', toolName: 'subagent', enableRunInBackground: false })
|
||||
await ctx.plugin(WorkerWorkflowEngine, { provider: 'spawn' })
|
||||
await ctx.plugin(ToolWorkflow)
|
||||
await ctx.plugin(ToolRalph)
|
||||
await ctx.plugin(CommandService)
|
||||
if (scenario.manualCompact === true) await ctx.plugin(CommandCompact)
|
||||
if (scenario.enterPlanMode === true) {
|
||||
await ctx.plugin(PlanModeService, { section: 'Snapshot plan mode instructions.' })
|
||||
}
|
||||
if (scenario.composition === 'code' || scenario.composition === 'advanced') {
|
||||
await ctx.plugin(WorkerCodeRuntime, {})
|
||||
}
|
||||
if (scenario.spillMaxInlineBytes !== undefined) {
|
||||
await ctx.plugin(LocalSpillStore, { root: join(cwd, '.spill') })
|
||||
await ctx.plugin(SpillPolicy, { maxInlineBytes: scenario.spillMaxInlineBytes })
|
||||
}
|
||||
if (scenario.composition === 'advanced') await ctx.plugin(ToolCordis, { vmTimeoutMs: 5_000 })
|
||||
if (MODE === 'record' && scenario.recorded) {
|
||||
await ctx.plugin(LlmDeepSeek)
|
||||
} else {
|
||||
if (replayRoot === undefined) throw new Error('replay mode requires an isolated fixture directory')
|
||||
// Recorded model text may name the generated cwd. Realize the portable token
|
||||
// outside that cwd so tools see only the scenario workspace during replay.
|
||||
const replayFile = await materializeFixtureCwd(fixtureFile, cwd, replayRoot)
|
||||
const replayChildFiles = await Promise.all(childFiles.map(file => materializeFixtureCwd(file, cwd, replayRoot)))
|
||||
installLlmReplay(ctx, { file: replayFile, childFiles: replayChildFiles, providers: PROVIDERS })
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
interface ScenarioResult {
|
||||
terminal: string
|
||||
parent: Session
|
||||
children: Session[]
|
||||
workflowEvents: string[]
|
||||
}
|
||||
|
||||
async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
|
||||
const snapshotTime = new Date(2026, 6, 21, 12, 0, 0).getTime()
|
||||
const clock = vi.spyOn(Date, 'now').mockReturnValue(snapshotTime)
|
||||
const fixtureFile = join(fixtureDir(scenario), 'session.jsonl')
|
||||
const childFiles = childFixturePaths(scenario)
|
||||
const prompts = userPrompts(await readFile(fixtureFile, 'utf8'))
|
||||
if (scenario.interaction === undefined) {
|
||||
expect(prompts.length, `${scenario.name} must carry at least one recorded user prompt`).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
const cwd = await mkdtemp(join(SNAPSHOT_TMP_ROOT, `dsh-tui-snapshot-${scenario.name}-`))
|
||||
const displayCwd = `/tmp/${basename(cwd)}`
|
||||
let replayRoot: string | undefined
|
||||
let ctx: Context | undefined
|
||||
let controller: ReturnType<typeof createTuiChat> | undefined
|
||||
const terminal = new HeadlessTerminal(100, 36)
|
||||
try {
|
||||
if (!(MODE === 'record' && scenario.recorded)) {
|
||||
replayRoot = await mkdtemp(join(SNAPSHOT_TMP_ROOT, `dsh-tui-replay-${scenario.name}-`))
|
||||
}
|
||||
if (scenario.seedWorkspace === true) {
|
||||
const source = join(fixtureDir(scenario), 'workspace')
|
||||
await cp(source, cwd, { recursive: true })
|
||||
}
|
||||
ctx = await mountScenarioContext(scenario, cwd, displayCwd, fixtureFile, childFiles, replayRoot)
|
||||
if (scenario.normalizePwdResult === true) {
|
||||
ctx.on('tools/post-execute', async (exec, result, next) => {
|
||||
const args = exec.arguments as { command?: unknown }
|
||||
return exec.name === 'bash' && args.command === 'pwd' && !result.isError
|
||||
? { kind: 'accept', content: [{ type: 'text', text: '/workspace/project\n' }] }
|
||||
: next()
|
||||
})
|
||||
}
|
||||
const disposedSessions: Session[] = []
|
||||
ctx.on('session/disposed', (session) => { disposedSessions.push(session) })
|
||||
const workflowEvents: string[] = []
|
||||
for (const name of ['workflow/start', 'workflow/phase', 'workflow/agent-start', 'workflow/agent-end', 'workflow/end'] as const) {
|
||||
ctx.on(name, () => { workflowEvents.push(name) })
|
||||
}
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId('main-session'),
|
||||
meta: { cwd },
|
||||
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
const agent: Agent = handle.agent
|
||||
if (scenario.manualCompact === true) seedCompactableHistory(agent)
|
||||
controller = createTuiChat(ctx, {
|
||||
sessionId: 'main-session',
|
||||
theme: { color: true },
|
||||
showReasoning: true,
|
||||
title: 'DSH TUI snapshot',
|
||||
welcome: `Recorded replay: ${scenario.name}`,
|
||||
maxToolOutputLines: 8,
|
||||
}, {
|
||||
terminal,
|
||||
exit: () => {},
|
||||
formatCwd: () => displayCwd,
|
||||
})
|
||||
await settleTerminal(terminal)
|
||||
|
||||
let interactionSnapshot: string | undefined
|
||||
if (scenario.interaction === 'skill-invocation-policy') {
|
||||
terminal.send('/skill')
|
||||
await settleTerminal(terminal)
|
||||
const discovery = normalizeTerminalSnapshot(
|
||||
await terminal.snapshot({ includeScrollback: true }),
|
||||
cwd,
|
||||
displayCwd,
|
||||
)
|
||||
expect(discovery).toContain('user-only-skill')
|
||||
expect(discovery).not.toContain('model-only-skill')
|
||||
|
||||
terminal.send('\x03')
|
||||
await settleTerminal(terminal)
|
||||
const skillContext = ctx
|
||||
const skillTurnEnded = new Promise<void>((resolve) => {
|
||||
const detach = skillContext.on('session/event', (session, event) => {
|
||||
if (session !== agent.session || event.type !== 'turn/end') return
|
||||
detach()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
terminal.send('/skill:user-only-skill')
|
||||
terminal.send('\r')
|
||||
await skillTurnEnded
|
||||
await agent.whenIdle()
|
||||
await settleTerminal(terminal)
|
||||
const loaded = normalizeTerminalSnapshot(
|
||||
await terminal.snapshot({ includeScrollback: true }),
|
||||
cwd,
|
||||
displayCwd,
|
||||
)
|
||||
expect(loaded).toContain('USER-ONLY SKILL LOADED')
|
||||
|
||||
terminal.send('/skill:model-only-skill')
|
||||
terminal.send('\r')
|
||||
await settleTerminal(terminal)
|
||||
const denied = normalizeTerminalSnapshot(
|
||||
await terminal.snapshot({ includeScrollback: true }),
|
||||
cwd,
|
||||
displayCwd,
|
||||
)
|
||||
expect(denied).toContain('model-only-skill')
|
||||
expect(denied).toContain('not available for user invocation.')
|
||||
expect(denied).not.toContain('MODEL-ONLY BODY MUST NOT LOAD')
|
||||
interactionSnapshot = [
|
||||
'=== skill autocomplete ===',
|
||||
discovery,
|
||||
'',
|
||||
'=== loaded exact invocation ===',
|
||||
loaded,
|
||||
'',
|
||||
'=== denied exact invocation ===',
|
||||
denied,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
let remainingPrompts = prompts
|
||||
let queuedPrompt: string | undefined
|
||||
let manualOrder: string[] | undefined
|
||||
let manualCommandId: string | undefined
|
||||
if (scenario.manualCompact === true) {
|
||||
expect(prompts.length, 'queued manual compaction needs a second replayed prompt').toBeGreaterThanOrEqual(2)
|
||||
queuedPrompt = prompts.at(-1)
|
||||
remainingPrompts = prompts.slice(0, -1)
|
||||
}
|
||||
if (scenario.enterPlanMode === true) {
|
||||
const firstPrompt = prompts[0]!
|
||||
terminal.send(`/plan ${firstPrompt}`)
|
||||
terminal.send('\r')
|
||||
await agent.whenIdle()
|
||||
await settleTerminal(terminal)
|
||||
remainingPrompts = prompts.slice(1)
|
||||
}
|
||||
|
||||
if (scenario.leavePlanModeAfterFirstTurn === true) {
|
||||
terminal.send('/plan off')
|
||||
terminal.send('\r')
|
||||
await settleTerminal(terminal)
|
||||
}
|
||||
|
||||
for (const prompt of remainingPrompts) {
|
||||
const admitted = agent.session.events.filter(event =>
|
||||
event.type === 'user/message' && event.data.source.kind === 'user').length
|
||||
terminal.send(prompt)
|
||||
terminal.send('\r')
|
||||
await terminal.flush()
|
||||
await expect.poll(() => agent.session.events.filter(event =>
|
||||
event.type === 'user/message' && event.data.source.kind === 'user').length).toBe(admitted + 1)
|
||||
await agent.whenIdle()
|
||||
await settleTerminal(terminal)
|
||||
}
|
||||
|
||||
if (scenario.manualCompact === true && queuedPrompt !== undefined) {
|
||||
terminal.send('/help')
|
||||
terminal.send('\r')
|
||||
await settleTerminal(terminal)
|
||||
expect(await terminal.snapshot({ includeScrollback: true }))
|
||||
.toContain('/compact — Compact older conversation history')
|
||||
|
||||
const compact = ctx.compact as DeferredSnapshotCompactService
|
||||
const inbox: string[] = []
|
||||
manualOrder = []
|
||||
ctx.on('agent/inbox/enqueue', (subject, item) => {
|
||||
if (subject === agent) inbox.push(`enqueue:${item.placement}:${item.id}`)
|
||||
})
|
||||
ctx.on('agent/inbox/dequeue', (subject, message) => {
|
||||
if (subject === agent) inbox.push(`dequeue:${message.id}`)
|
||||
})
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (session !== agent.session) return
|
||||
if (event.type === 'command/run' && event.data.name === 'compact') {
|
||||
manualCommandId = event.data.commandId
|
||||
manualOrder?.push('command/run')
|
||||
}
|
||||
if (event.type === 'command/done' && event.data.commandId === manualCommandId) {
|
||||
manualOrder?.push('command/done')
|
||||
}
|
||||
if (event.type.startsWith('compact/')) manualOrder?.push(event.type)
|
||||
if (event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === 'compact') manualOrder?.push('checkpoint')
|
||||
if (event.type === 'turn/start') manualOrder?.push(`turn/start:${event.data.trigger.kind}`)
|
||||
})
|
||||
ctx.on('session/flush', (session) => {
|
||||
if (session === agent.session) manualOrder?.push('flush')
|
||||
})
|
||||
|
||||
terminal.send('/compact')
|
||||
terminal.send('\r')
|
||||
await terminal.flush()
|
||||
await snapshotDeadline(compact.summaryStarted.promise, () =>
|
||||
`manual summary did not start; status=${agent.status}; tail=${
|
||||
agent.session.events.slice(-8).map(event => event.type).join(',')
|
||||
}`)
|
||||
clock.mockReturnValue(snapshotTime + 1_000)
|
||||
await settleTerminal(terminal)
|
||||
await expect.poll(() => terminal.snapshot()).toContain('dsh ⊙')
|
||||
await expect.poll(() => terminal.snapshot()).toContain('Context being compacted 1.0s')
|
||||
const liveCompaction = await terminal.snapshot()
|
||||
expect(liveCompaction.indexOf('Context being compacted 1.0s')).toBeLessThan(liveCompaction.indexOf('dsh ⊙'))
|
||||
clock.mockReturnValue(snapshotTime)
|
||||
|
||||
// Real keystrokes: the prompt keeps its ordinary queue identity while
|
||||
// admission is reserved, and an injection appends immediately.
|
||||
terminal.send(queuedPrompt)
|
||||
terminal.send('\r')
|
||||
await terminal.flush()
|
||||
await expect.poll(() => inbox.length).toBe(1)
|
||||
agent.inject(createUserMessage({
|
||||
content: [{ type: 'text', text: 'Injected while compaction was running.' }],
|
||||
source: { kind: 'plugin', plugin: 'snapshot-injector' },
|
||||
}))
|
||||
expect(inbox[0]).toMatch(/^enqueue:queued:/u)
|
||||
expect(agent.status).toBe('idle')
|
||||
expect(agent.session.events.some(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'user'
|
||||
&& event.data.content.some(block => block.type === 'text' && block.text === queuedPrompt))).toBe(false)
|
||||
|
||||
const idle = agent.whenIdle()
|
||||
compact.releaseSummary.resolve(undefined)
|
||||
await snapshotDeadline(idle, () =>
|
||||
`manual compaction did not reach idle; status=${agent.status}; order=${manualOrder?.join(',') ?? ''}; tail=${
|
||||
agent.session.events.slice(-12).map(event => event.type).join(',')
|
||||
}`)
|
||||
await settleTerminal(terminal)
|
||||
expect(inbox).toEqual([inbox[0], `dequeue:${inbox[0]?.slice('enqueue:queued:'.length) ?? ''}`])
|
||||
}
|
||||
|
||||
const events: SessionEvent[] = [...agent.session.events]
|
||||
const firstHeader = events.find(event => event.type === 'request/header')
|
||||
expect(firstHeader?.type === 'request/header' && firstHeader.data.header.system)
|
||||
.toContain(FILE_REFERENCE_PROMPT)
|
||||
if (scenario.harnessSourceRoot !== undefined) {
|
||||
expect(firstHeader?.type === 'request/header' && firstHeader.data.header.system)
|
||||
.toContain(`The DeepSeek Harness implementation checkout is at ${scenario.harnessSourceRoot}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.`)
|
||||
}
|
||||
expect(events.filter(event => event.type === 'tool/call').map(event => event.data.name)).toEqual(scenario.expectedTools)
|
||||
for (const [type, count] of Object.entries(scenario.expectedEventCounts ?? {})) {
|
||||
expect(events.filter(event => event.type === type), `${scenario.name} must emit ${type}`).toHaveLength(count)
|
||||
}
|
||||
if (scenario.enterPlanMode === true) {
|
||||
expect(ctx.planMode.get(agent)).toEqual({
|
||||
active: scenario.leavePlanModeAfterFirstTurn !== true,
|
||||
})
|
||||
const planMode = events.find(event => event.type === 'plan/mode')
|
||||
if (planMode === undefined || firstHeader === undefined) {
|
||||
throw new Error('plan-mode command snapshot needs plan/mode before its first request/header')
|
||||
}
|
||||
expect(planMode.seq).toBeLessThan(firstHeader.seq)
|
||||
expect(firstHeader.data.header.system).toContain('Snapshot plan mode instructions.')
|
||||
const firstMessage = events.find(event => event.type === 'user/message')
|
||||
expect(firstMessage?.data.content).toEqual([{ type: 'text', text: prompts[0] }])
|
||||
}
|
||||
if (scenario.leavePlanModeAfterFirstTurn === true) {
|
||||
const planModes = events.filter(event => event.type === 'plan/mode')
|
||||
expect(planModes.map(event => event.data.active)).toEqual([true, false])
|
||||
const headers = events.filter(event => event.type === 'request/header')
|
||||
const exit = planModes[1]
|
||||
const afterExit = headers[1]
|
||||
if (exit === undefined || afterExit === undefined) {
|
||||
throw new Error('active plan exit snapshot needs a committed exit and changed request header')
|
||||
}
|
||||
expect(exit.seq).toBeLessThan(afterExit.seq)
|
||||
expect(afterExit.data.header.system).not.toContain('Snapshot plan mode instructions.')
|
||||
expect(events.filter(event => event.type === 'user/message' && event.data.source.kind === 'plugin').map(event => (event.data as { content: unknown }).content))
|
||||
.toContainEqual([{ type: 'text', text: 'The user switched this session back to the default mode.' }])
|
||||
}
|
||||
if (scenario.manualCompact === true) {
|
||||
const compactStart = events.find(event => event.type === 'compact/start')
|
||||
const compactSummary = events.find(event => event.type === 'compact/summary')
|
||||
const compactCheckpoint = events.find(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin' && event.data.source.plugin === 'compact')
|
||||
const injectedEvent = events.find(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin' && event.data.source.plugin === 'snapshot-injector')
|
||||
const compactEnd = events.find(event => event.type === 'compact/end')
|
||||
expect(compactStart?.data.turn).toBeNull()
|
||||
expect(compactEnd?.data.turn).toBeNull()
|
||||
expect(events.filter(event => event.type === 'compact/summary')).toHaveLength(1)
|
||||
if (compactStart === undefined || compactSummary === undefined
|
||||
|| compactCheckpoint === undefined || injectedEvent === undefined
|
||||
|| compactEnd === undefined) {
|
||||
throw new Error('manual compaction snapshot is missing its durable marker, summary, checkpoint, or injection')
|
||||
}
|
||||
// The markers are time points, not an exclusive container: unrelated
|
||||
// idle injection is allowed between them while the selected span stays stable.
|
||||
expect(compactStart.seq).toBeLessThan(injectedEvent.seq)
|
||||
expect(injectedEvent.seq).toBeLessThan(compactSummary.seq)
|
||||
expect(compactSummary.seq).toBeLessThan(compactCheckpoint.seq)
|
||||
expect(compactCheckpoint.seq).toBeLessThan(compactEnd.seq)
|
||||
|
||||
const manualTimeline = manualOrder ?? []
|
||||
const commandRunIndex = manualTimeline.indexOf('command/run')
|
||||
const compactStartIndex = manualTimeline.indexOf('compact/start')
|
||||
const compactEndIndex = manualTimeline.indexOf('compact/end')
|
||||
const firstFlushIndex = manualTimeline.indexOf('flush')
|
||||
const queuedTurnIndex = manualTimeline.indexOf('turn/start:message')
|
||||
const commandDoneIndex = manualTimeline.indexOf('command/done')
|
||||
expect(manualTimeline.filter(item => item === 'command/run')).toHaveLength(1)
|
||||
expect(manualTimeline.filter(item => item === 'command/done')).toHaveLength(1)
|
||||
expect(compactStartIndex).toBeGreaterThan(commandRunIndex)
|
||||
expect(compactEndIndex).toBeGreaterThan(compactStartIndex)
|
||||
expect(firstFlushIndex).toBeGreaterThan(compactEndIndex)
|
||||
expect(queuedTurnIndex).toBeGreaterThan(firstFlushIndex)
|
||||
expect(commandDoneIndex).toBeGreaterThan(firstFlushIndex)
|
||||
|
||||
const commandRun = events.find(event => event.type === 'command/run'
|
||||
&& event.data.name === 'compact')
|
||||
const commandRunId = commandRun?.type === 'command/run'
|
||||
? commandRun.data.commandId
|
||||
: undefined
|
||||
const commandDone = events.find(event => event.type === 'command/done'
|
||||
&& event.data.commandId === commandRunId)
|
||||
expect(commandRun?.type === 'command/run' && commandRun.data).toEqual({
|
||||
commandId: commandRunId,
|
||||
name: 'compact',
|
||||
args: '',
|
||||
source: { kind: 'user' },
|
||||
})
|
||||
expect(commandDone?.type === 'command/done' && commandDone.data).toEqual({
|
||||
commandId: commandRunId,
|
||||
kind: 'success',
|
||||
text: 'Compacted 2 history items (~387 tokens).',
|
||||
})
|
||||
expect(commandRun !== undefined && commandRun.seq < compactStart.seq).toBe(true)
|
||||
expect(commandDone !== undefined && commandDone.seq > compactEnd.seq).toBe(true)
|
||||
expect(agent.session.surface.nodes).not.toContain(commandRun?.seq)
|
||||
expect(agent.session.surface.nodes).not.toContain(commandDone?.seq)
|
||||
|
||||
// The command line itself never becomes a prompt.
|
||||
expect(events.some(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'user'
|
||||
&& event.data.content.some(block => block.type === 'text' && block.text.trim() === '/compact'))).toBe(false)
|
||||
const derived = agent.session.deriveMessages().map(message => message.content
|
||||
.map(block => block.type === 'text' ? block.text : '')
|
||||
.join(''))
|
||||
const checkpoint = derived.findIndex(text => text.includes('Keyless manual compaction checkpoint.'))
|
||||
const injected = derived.findIndex(text => text.includes('Injected while compaction was running.'))
|
||||
const queued = derived.findIndex(text => text === queuedPrompt)
|
||||
expect(checkpoint).toBe(0)
|
||||
expect(injected).toBeGreaterThan(checkpoint)
|
||||
expect(queued).toBeGreaterThan(injected)
|
||||
expect(derived).not.toContain('/compact')
|
||||
expect(derived).not.toContain('Compacted 2 history items (~387 tokens).')
|
||||
expect(derived.filter(text => text.includes('Injected while compaction was running.'))).toHaveLength(1)
|
||||
expect(compactSummary.data.shadowedSeqs).not.toContain(injectedEvent.seq)
|
||||
const queuedTurn = events.findLast(event => event.type === 'turn/start')
|
||||
expect(queuedTurn !== undefined && compactEnd.seq < queuedTurn.seq).toBe(true)
|
||||
}
|
||||
if (scenario.spillMaxInlineBytes !== undefined) {
|
||||
// The REAL pipeline ran (tools execute on replay too): the durable
|
||||
// dispatch copy is bounded to a preview + locator under the run cwd,
|
||||
// while the outer result still carries the program's whole value.
|
||||
const dispatch = events.find(event => (event.type as string) === 'tool/code-dispatch')
|
||||
const content = (dispatch?.data as { content: { type: string; text?: string }[] }).content
|
||||
const text = content.filter(block => block.type === 'text').map(block => block.text ?? '').join('')
|
||||
expect(Buffer.byteLength(text, 'utf8')).toBeLessThanOrEqual(scenario.spillMaxInlineBytes)
|
||||
expect(text).toContain('Full formatted result stored at:')
|
||||
expect(text).toContain('.spill')
|
||||
}
|
||||
expect(events.filter(event => event.type === 'tool/result').every(event => !event.data.message.content[0].isError)).toBe(true)
|
||||
expect(events.filter(event => event.type === 'turn/end').every(event => event.data.reason.kind !== 'error')).toBe(true)
|
||||
if (scenario.name === 'dynamic-workflow' || scenario.name === 'cordis-dynamic-toolchain') {
|
||||
expect(workflowEvents).toEqual([
|
||||
'workflow/start',
|
||||
'workflow/phase',
|
||||
'workflow/agent-start',
|
||||
'workflow/agent-end',
|
||||
'workflow/end',
|
||||
])
|
||||
}
|
||||
|
||||
expect(terminal.themeViolations(), `${scenario.name} must remain theme-agnostic`).toEqual([])
|
||||
const snapshot = interactionSnapshot ?? normalizeTerminalSnapshot(
|
||||
await terminal.snapshot({ includeScrollback: true }),
|
||||
cwd,
|
||||
displayCwd,
|
||||
)
|
||||
await handle.dispose()
|
||||
const children = disposedSessions
|
||||
.filter(session => session !== agent.session)
|
||||
.sort((a, b) => a.header.createdAt - b.header.createdAt)
|
||||
expect(children).toHaveLength(scenario.childSessions ?? 0)
|
||||
return { terminal: snapshot, parent: agent.session, children, workflowEvents }
|
||||
} finally {
|
||||
await controller?.dispose()
|
||||
await ctx?.fiber.dispose()
|
||||
await terminal.dispose()
|
||||
await rm(cwd, { recursive: true, force: true })
|
||||
if (replayRoot !== undefined) await rm(replayRoot, { recursive: true, force: true })
|
||||
clock.mockRestore()
|
||||
}
|
||||
}
|
||||
|
||||
async function writeRecording(scenario: Scenario, result: ScenarioResult): Promise<void> {
|
||||
const dir = scenarioDir(scenario)
|
||||
await mkdir(dir, { recursive: true })
|
||||
await writeFile(
|
||||
join(dir, 'session.jsonl'),
|
||||
scrubRequestHeaders(tokenizeSessionFixtureCwd(rawSessionLog(result.parent))),
|
||||
)
|
||||
expect(result.children).toHaveLength(scenario.childSessions ?? 0)
|
||||
for (const [index, child] of result.children.entries()) {
|
||||
await writeFile(
|
||||
join(dir, `session.${index + 1}.jsonl`),
|
||||
scrubRequestHeaders(tokenizeSessionFixtureCwd(rawSessionLog(child))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('TUI recorded-session terminal snapshots', () => {
|
||||
for (const scenario of SCENARIOS) {
|
||||
it(scenario.name, async () => {
|
||||
observedScenarios.add(scenario.name)
|
||||
const result = await runScenario(scenario)
|
||||
const terminalFile = join(scenarioDir(scenario), 'terminal.expected.txt')
|
||||
if (MODE === 'record' || MODE === 'refresh') {
|
||||
await mkdir(scenarioDir(scenario), { recursive: true })
|
||||
await writeFile(terminalFile, result.terminal)
|
||||
}
|
||||
if (MODE === 'record' && scenario.recorded) await writeRecording(scenario, result)
|
||||
await expect(result.terminal).toMatchFileSnapshot(terminalFile)
|
||||
}, 120_000)
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const scenarioNames = SCENARIOS.map(scenario => scenario.name).sort()
|
||||
const observedNames = [...observedScenarios].sort()
|
||||
if (TEST_NAME_FILTERED) {
|
||||
expect(observedNames).not.toHaveLength(0)
|
||||
expect(scenarioNames).toEqual(expect.arrayContaining(observedNames))
|
||||
} else {
|
||||
expect(observedNames).toEqual(scenarioNames)
|
||||
}
|
||||
for (const [index, scenario] of SCENARIOS.entries()) {
|
||||
if (scenario.fixture === undefined) continue
|
||||
const sourceIndex = SCENARIOS.findIndex(candidate => candidate.name === scenario.fixture)
|
||||
expect(sourceIndex, `${scenario.name} fixture source ${scenario.fixture} must exist`).toBeGreaterThanOrEqual(0)
|
||||
expect(sourceIndex, `${scenario.name} fixture source must precede it`).toBeLessThan(index)
|
||||
const source = SCENARIOS[sourceIndex]
|
||||
expect(source?.fixture, `${scenario.name} fixture source must own its replay files`).toBeUndefined()
|
||||
expect(source?.recorded, `${scenario.name} fixture source must be recordable`).toBe(true)
|
||||
}
|
||||
const directories = (await readdir(SNAPSHOTS_DIR, { withFileTypes: true }))
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => entry.name)
|
||||
.sort()
|
||||
expect(directories).toEqual(SCENARIOS.map(scenario => scenario.name).sort())
|
||||
for (const scenario of SCENARIOS) {
|
||||
const expected = [
|
||||
...scenario.fixture === undefined ? ['session.jsonl'] : [],
|
||||
'terminal.expected.txt',
|
||||
...scenario.seedWorkspace === true && scenario.fixture === undefined ? ['workspace'] : [],
|
||||
...Array.from({ length: scenario.childSessions ?? 0 }, (_, index) => `session.${index + 1}.jsonl`),
|
||||
].sort()
|
||||
expect((await readdir(scenarioDir(scenario))).sort()).toEqual(expected)
|
||||
for (const fixture of ['session.jsonl', ...childFixturePaths(scenario).map(path => basename(path))]) {
|
||||
const content = await readFile(join(fixtureDir(scenario), fixture), 'utf8')
|
||||
expect(scrubRequestHeaders(content), `${scenario.name}/${fixture} carries request-header bulk`).toBe(content)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -26,9 +26,6 @@
|
||||
{
|
||||
"path": "../../packages/bash/tool-bash"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/ui/tui"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/util/paths"
|
||||
},
|
||||
|
||||
@@ -3,8 +3,8 @@ import { defineConfig } from 'tsdown'
|
||||
/**
|
||||
* The dsh CLI ships one entry: the `bin` referenced by package.json `bin`.
|
||||
* The root tsdown builds only `lib/types/index.js`, so this override points at
|
||||
* `lib/types/bin.js` instead; the statically imported surface modules bundle
|
||||
* into it. Declarations come from `tsc -b` (dts: false), matching every package.
|
||||
* `lib/types/bin.js` instead; its reachable mode modules bundle with it.
|
||||
* Declarations come from `tsc -b` (dts: false), matching every package.
|
||||
*/
|
||||
export default defineConfig({
|
||||
entry: ['lib/types/bin.js'],
|
||||
|
||||
Reference in New Issue
Block a user