Merge remote-tracking branch 'origin/master' into codex/responsive-queue-panel
# Conflicts: # packages/client/ui-conversation/src/client/queue/QueueDock.module.css # packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css # packages/client/ui-conversation/src/client/skeleton/InputBar.module.css # packages/client/ui-conversation/src/client/skeleton/TodoPanel.module.css
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: 93c36d18abd06bbd7a80c918f520b92489180395
|
||||
README.zh.md: 85f4624a592eaf2ae44dc31fb4e18fb5657e62fd
|
||||
README.md: e4b34c11d5deb722caed199d6350f7931092a636
|
||||
README.zh.md: 5701bc8b6d99f00e68db572a58a0b6d520d67f08
|
||||
|
||||
@@ -2,24 +2,30 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The `dsh` command-line entry follows the `apps/` assembly tier: `apps/*` are product assemblies over `packages/*` libraries. Plain `dsh` boots the interactive TUI coding agent, `dsh -p "task"` runs one headless turn, and `dsh web` serves the browser UI.
|
||||
|
||||
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`) and whose `web` subcommand is the browser UI. `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. `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 `cordis.yml` 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.
|
||||
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`), whose `meta` subcommand is the same TUI over this checkout, whose `upgrade` subcommand is an option-less guided-session entry, and whose `web` subcommand is the browser UI. `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` 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.
|
||||
|
||||
The TUI surface:
|
||||
|
||||
- boots the shipped default config (`examples/tui-agent/cordis.yml`), or the tree named by `--config <path>` (the demo/test escape for booting an alternate example tree), through [`dsh-app-boot`](../../packages/ui/app-boot/README.md);
|
||||
- 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 `dsh --resume <id>`; runtimes without process replacement keep the displayed command fallback. The flag provides the id on the boot context under `RESUME_SESSION_ID_KEY` (no environment variable), which the shipped config reads through `!!js`, 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;
|
||||
- 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)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree.
|
||||
- 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 Web and headless surfaces boot one shared composition (`cordis.yml`): both 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, and opt into first-message model titles. 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`).
|
||||
`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. Both take no options — `--config`, `-p`, and `--resume` fail loud — and seed only on this first launch, so a later `dsh --resume <id>` of the session is an ordinary TUI session with no re-injection.
|
||||
|
||||
|
||||
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 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, and mount a disposable in-memory SQLite content-index service. That 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 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.
|
||||
|
||||
`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 (its config tree pins its own mode).
|
||||
|
||||
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).
|
||||
|
||||
## Install (developer machine)
|
||||
|
||||
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:
|
||||
|
||||
@@ -2,24 +2,30 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`dsh` 命令行入口遵循 `apps/` 组装层:`apps/*` 是位于 `packages/*` 库之上的产品组装。直接运行 `dsh` 会启动交互式 TUI 编码 agent(智能体),`dsh -p "task"` 运行一个无头轮次,`dsh web` 则提供浏览器 UI。
|
||||
|
||||
Argv 只会通过 [Commander](https://github.com/tj/commander.js) 适配器([`src/args.ts`](src/args.ts))解析一次:同一个程序的默认形式(无子命令)是 TUI/无头界面(`--config`、`-p`/`--prompt`、`--resume`),`web` 子命令则是浏览器 UI。`src/bin.ts` 按解析后的 mode 分支,仅动态导入该 mode 的模块。`dsh --help` 列出所有 mode,`dsh web --help` 渲染 Web 用法,`dsh --version` 打印此应用的版本;未知选项或拼错的 `--resume` 会明确报错(stderr,退出码 1),而不会被错路由。`dsh web` 的 `--host`/`--port` 是未验证的直通覆盖:`dsh-host-webserver` schema 是默认值(标志缺失时使用已交付的 `cordis.yml` 值)和有效性的唯一真源,并在启动时拒绝错误值。`--trusted-host` 为 /api 浏览器信任栅栏追加具名权威;全接口绑定还会自行推导本机的 LAN IP 字面量([`src/app-cli-entry.ts`](src/app-cli-entry.ts)),因此打印出的 LAN URL 无需任何标志即可使用。
|
||||
Argv 只会通过 [Commander](https://github.com/tj/commander.js) 适配器([`src/args.ts`](src/args.ts))解析一次:同一个程序的默认形式(无子命令)是 TUI/无头界面(`--config`、`-p`/`--prompt`、`--resume`),`meta` 子命令是以本 checkout 为 workspace 的同一个 TUI,`upgrade` 子命令是无选项的引导会话入口,`web` 子命令则是浏览器 UI。`src/bin.ts` 按解析后的 mode 分支,仅动态导入该 mode 的模块。`dsh --help` 列出所有 mode,`dsh web --help` 渲染 Web 用法,`dsh --version` 打印此应用的版本;未知选项或拼错的 `--resume` 会明确报错(stderr,退出码 1),而不会被错路由。凡与默认界面不共享任何选项的子命令(`upgrade`、`web`、`meta`)都会拒绝泄漏进来的 `--config`/`-p`/`--resume`,而不会照常运行并丢弃它。`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 界面:
|
||||
|
||||
- 启动已交付的默认配置(`examples/tui-agent/cordis.yml`),或由 `--config <path>` 指定的树(演示/测试用于启动其他示例树的逃生口),并通过 [`dsh-app-boot`](../../packages/ui/app-boot/README.md) 完成启动;
|
||||
- 使用 `dsh --resume <session-id>` 恢复已持久化会话。当 Node 宿主公开 `process.execve` 时,还会提供 TUI 的原地移交宿主:选择器预检并刷新当前会话后,宿主会释放应用,并以规范化的 `dsh --resume <id>` 替换进程;不支持进程替换的运行时保留屏幕上显示的命令回退。该标志通过 `RESUME_SESSION_ID_KEY` 在启动上下文中提供 id(不使用环境变量),已交付的配置通过 `!!js` 读取它;缺失或无法读取的 id 会明确报错,而不会创建新会话;
|
||||
- 将 **调用目录** 视为 workspace:会话、相对路径和 workspace 指令都从 cwd 解析;
|
||||
- 通过 [`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)):`.env` 填补环境缺口(环境中已有的值 > 项目 `.env` > 个人 `.env`),`config.yaml` 则修补已启动的树。
|
||||
- 应用 `~/.dsh` 中的个人覆盖(参见 [app-boot 的个人配置](../../packages/ui/app-boot/README.md#personal-config)):`config.yaml` 修补已启动的树,而那里的 `.env` 是凭据 provider 自己的存储(绝不会被提升进环境,因此密钥始终可轮换)。环境优先级为环境中已有的值 > 项目 `.env`。
|
||||
|
||||
Web 和无头界面启动同一个共享组合(`cordis.yml`):两者都将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root <path>` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。
|
||||
`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 会话,不会重复注入。
|
||||
|
||||
|
||||
Web 和无头界面启动 `base.cordis.yml` 与 `web.cordis.yml`,随后应用 `$DSH_HOME/config.yaml`;显式的 `--config <path>` 会替代该个人覆盖。除此之外,两者共享同一套组合:两者都将调用目录视为默认项目和 Workspace 根目录,除非通过 `--workspace-root <path>` 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 `AGENTS.md`/`CLAUDE.md` 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题,且挂载一个可丢弃的内存 SQLite 内容索引服务。该服务在启动时处于 ACTIVE 状态,但其 `node:sqlite` 模块与数据库句柄分别要到首次内容搜索才会导入和打开。这样可使 Node 22 在尚未使用搜索时的启动输出不出现 SQLite 实验性警告;首次实际搜索仍可能发出运行时警告。每个服务实例独占自己的数据库,因此并行调用既不会共享不受支持的 SQLite 状态,也不会留下派生索引文件,首次搜索还会惰性对账实时日志与持久化日志。无头界面唯一的差异是监听操作系统分配的端口(并行 `dsh -p` 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(`pnpm run build && pnpm run build:web`)。
|
||||
|
||||
已交付的 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`。
|
||||
|
||||
`DSH_TOOLS_MODE` 为整个 Web/无头进程选择工具呈现模式:可选值为 `native`(未设置时的 schema 默认值)、`code`(仅含 `run_code` 的 Code Mode 协议接口)或 `both`;任何其他值都会经由 `dsh-tools` 配置 schema 在启动时明确报错。它是一个临时 seam:Loader 组合是静态的,因此该设置作用于整个进程;待 Web UI 负责逐会话工具模式选择后便会移除。TUI 界面会忽略该变量(其配置树固定了自身模式)。
|
||||
|
||||
每个 `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)。
|
||||
|
||||
## 安装(开发机)
|
||||
|
||||
将从源码运行的启动器符号链接到 PATH 上;它通过自身真实路径解析 checkout,因此代码更改会在下次启动时生效,无需构建:
|
||||
|
||||
165
apps/cli/composition.md
Normal file
165
apps/cli/composition.md
Normal file
@@ -0,0 +1,165 @@
|
||||
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
|
||||
Run `pnpm run gen-doc-graphs` to regenerate. -->
|
||||
|
||||
# TUI Agent App Composition
|
||||
|
||||
The TUI surface combines the shared CLI base with its surface overlay and full-screen terminal package.
|
||||
|
||||
```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_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_bash_local["bash-local<br/>@deepseek-ai/dsh-bash-local"]
|
||||
cfg --> plugin_tui_bash_local
|
||||
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_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["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_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_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_local["fs-local<br/>@deepseek-ai/dsh-fs-local"]
|
||||
cfg --> plugin_tui_fs_local
|
||||
plugin_tui_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
|
||||
cfg --> plugin_tui_llm_deepseek
|
||||
```
|
||||
|
||||
| Plugin id | Package / module |
|
||||
| --- | --- |
|
||||
| `timer` | `@cordisjs/plugin-timer` |
|
||||
| `hmr` | `@cordisjs/plugin-hmr` |
|
||||
| `llm` | `@deepseek-ai/dsh-llm` |
|
||||
| `session` | `@deepseek-ai/dsh-session` |
|
||||
| `session-title` | `@deepseek-ai/dsh-session-title` |
|
||||
| `session-title-llm` | `@deepseek-ai/dsh-session-title-first-message-llm` |
|
||||
| `user-interaction` | `@deepseek-ai/dsh-user-interaction` |
|
||||
| `agent` | `@deepseek-ai/dsh-agent` |
|
||||
| `tasks` | `@deepseek-ai/dsh-tasks-local` |
|
||||
| `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
|
||||
| `settings` | `@deepseek-ai/dsh-settings-local` |
|
||||
| `credentials` | `@deepseek-ai/dsh-credentials-local` |
|
||||
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
|
||||
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
|
||||
| `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` |
|
||||
| `telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` |
|
||||
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
|
||||
| `bash-local` | `@deepseek-ai/dsh-bash-local` |
|
||||
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
|
||||
| `tool-tasks` | `@deepseek-ai/dsh-tool-tasks` |
|
||||
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
|
||||
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |
|
||||
| `tool-fs-search` | `@deepseek-ai/dsh-tool-fs-search` |
|
||||
| `workspace-context` | `@deepseek-ai/dsh-workspace-context` |
|
||||
| `skill` | `@deepseek-ai/dsh-skill` |
|
||||
| `skill-local` | `@deepseek-ai/dsh-skill-local` |
|
||||
| `tool-skill` | `@deepseek-ai/dsh-tool-skill` |
|
||||
| `commands` | `@deepseek-ai/dsh-commands` |
|
||||
| `goal` | `@deepseek-ai/dsh-goal` |
|
||||
| `goal-session` | `@deepseek-ai/dsh-goal-session` |
|
||||
| `command-goal` | `@deepseek-ai/dsh-command-goal` |
|
||||
| `plan-mode` | `@deepseek-ai/dsh-plan-mode` |
|
||||
| `token-meter` | `@deepseek-ai/dsh-token-meter` |
|
||||
| `compact-basic` | `@deepseek-ai/dsh-compact-basic` |
|
||||
| `subagent` | `@deepseek-ai/dsh-subagent` |
|
||||
| `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` |
|
||||
| `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` |
|
||||
| `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` |
|
||||
| `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` |
|
||||
| `workflow-workerthread` | `@deepseek-ai/dsh-workflow-workerthread` |
|
||||
| `tool-workflow` | `@deepseek-ai/dsh-tool-workflow` |
|
||||
| `timeout-policy` | `@deepseek-ai/dsh-timeout-policy` |
|
||||
| `spill-local` | `@deepseek-ai/dsh-spill-local` |
|
||||
| `spill-policy` | `@deepseek-ai/dsh-spill-policy` |
|
||||
| `tools` | `@deepseek-ai/dsh-tools` |
|
||||
| `system-prompt` | `@deepseek-ai/dsh-system-prompt` |
|
||||
| `agent-loop` | `@deepseek-ai/dsh-agent-loop` |
|
||||
| `fs-local` | `@deepseek-ai/dsh-fs-local` |
|
||||
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
|
||||
|
||||
Source config: [`apps/cli/config/base.cordis.yml`](config/base.cordis.yml).
|
||||
|
||||
Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
|
||||
278
apps/cli/config/base.cordis.yml
Normal file
278
apps/cli/config/base.cordis.yml
Normal file
@@ -0,0 +1,278 @@
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
# 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.
|
||||
#
|
||||
# Row order carries no load semantics (activation is service-availability
|
||||
# driven); the grouping is for readers.
|
||||
|
||||
- id: timer
|
||||
name: '@cordisjs/plugin-timer'
|
||||
|
||||
- id: hmr
|
||||
name: '@cordisjs/plugin-hmr'
|
||||
config:
|
||||
root: ['.']
|
||||
|
||||
- id: llm
|
||||
name: '@deepseek-ai/dsh-llm'
|
||||
|
||||
- id: session
|
||||
name: '@deepseek-ai/dsh-session'
|
||||
|
||||
- id: session-title
|
||||
name: '@deepseek-ai/dsh-session-title'
|
||||
config:
|
||||
fallbackMaxWords: 5
|
||||
fallbackMaxBytes: 40
|
||||
maxTitleBytes: 80
|
||||
|
||||
- id: session-title-llm
|
||||
name: '@deepseek-ai/dsh-session-title-first-message-llm'
|
||||
config:
|
||||
targetWords: 5
|
||||
targetCjkCharacters: 10
|
||||
maxInputBytes: 4096
|
||||
maxOutputTokens: 64
|
||||
timeoutMs: 60000
|
||||
|
||||
- id: user-interaction
|
||||
name: '@deepseek-ai/dsh-user-interaction'
|
||||
|
||||
- id: agent
|
||||
name: '@deepseek-ai/dsh-agent'
|
||||
|
||||
- id: tasks
|
||||
name: '@deepseek-ai/dsh-tasks-local'
|
||||
|
||||
- id: llm-retry
|
||||
name: '@deepseek-ai/dsh-llm-retry'
|
||||
|
||||
# User-settings document (`$DSH_HOME/settings.yaml`, hot-reloaded): a
|
||||
# `llm-deepseek:` or `llm-pi-ai:` section there overrides the adapter entries
|
||||
# below without a restart, and is what the web Models page writes.
|
||||
- id: settings
|
||||
name: '@deepseek-ai/dsh-settings-local'
|
||||
|
||||
# Credential store: the live process environment over `$DSH_HOME/.env`
|
||||
# (owner-only file, hot-reloaded). Adapters resolve their key references
|
||||
# through it at each request, so no key is inlined in this file. The web
|
||||
# Models page's key inputs write it through `credentials.set`; nothing hoists
|
||||
# the document into the process environment, which would make every stored key
|
||||
# read as an unrotatable ambient override.
|
||||
- id: credentials
|
||||
name: '@deepseek-ai/dsh-credentials-local'
|
||||
|
||||
# The pi-ai multi-provider twin, mounted dormant: zero routes (and no extra
|
||||
# models in the picker) until a `llm-pi-ai:` settings section supplies provider
|
||||
# profiles — then those routes register live, keys resolving per request
|
||||
# through their apiKeyEnv references, and drop again when the section empties.
|
||||
# Supplying those profiles is exactly what the web Models page does. Which
|
||||
# adapters exist is composition; which providers run is the user's settings
|
||||
# document.
|
||||
- id: llm-pi-ai
|
||||
name: '@deepseek-ai/dsh-llm-pi-ai'
|
||||
|
||||
- id: session-persistence-jsonl
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: !!js >-
|
||||
(() => { const path = process.getBuiltinModule('node:path'); const home = process.getBuiltinModule('node:os').homedir(); const configured = process.env.DSH_HOME; const selected = configured !== undefined && configured.trim().length > 0 ? configured : path.join(home, '.dsh'); const expanded = selected === '~' ? home : selected.startsWith('~/') || selected.startsWith('~\\') ? path.join(home, selected.slice(2)) : selected; return path.join(path.resolve(expanded), '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).
|
||||
- id: session-query-sqlite
|
||||
name: '@deepseek-ai/dsh-session-query-sqlite'
|
||||
config:
|
||||
path: !!js launcherSessionQueryPath ?? './.sessions/session-query.db'
|
||||
|
||||
# Session telemetry, on for every dsh surface: 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
|
||||
# interval. No telemetry/record redaction rule is mounted yet, so exports
|
||||
# are the raw captured copy; the deployment stance, env seams, and
|
||||
# follow-ups are pinned in the web-telemetry-default-mount Agent Note.
|
||||
# DSH_TELEMETRY_OTLP_URL overrides the production endpoint, and a non-empty
|
||||
# DSH_TELEMETRY_DISABLED — any value, including '0'/'false' — opts the
|
||||
# process out (the launchers patch the row disabled; config cannot disable
|
||||
# a row). The exporter/processor values bound the shutdown drain to ~1s
|
||||
# against an unreachable collector: exporter.timeoutMillis is both the
|
||||
# per-attempt socket timeout and the retry deadline (1s effectively
|
||||
# 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.
|
||||
- id: telemetry-otel
|
||||
name: '@deepseek-ai/dsh-session-telemetry-otel'
|
||||
config:
|
||||
exporter:
|
||||
url: !!js process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://harness-telemetry.deepseeksvc.com/v1/logs'
|
||||
compression: gzip
|
||||
timeoutMillis: 1000
|
||||
processor:
|
||||
scheduledDelayMillis: 10000
|
||||
maxQueueSize: 2048
|
||||
maxExportBatchSize: 2048
|
||||
exportTimeoutMillis: 1500
|
||||
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
- id: bash-local
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
|
||||
- id: tool-tasks
|
||||
name: '@deepseek-ai/dsh-tool-tasks'
|
||||
|
||||
- id: fs-policy
|
||||
name: '@deepseek-ai/dsh-fs-policy'
|
||||
|
||||
- id: tool-fs
|
||||
name: '@deepseek-ai/dsh-tool-fs'
|
||||
|
||||
- id: tool-fs-search
|
||||
name: '@deepseek-ai/dsh-tool-fs-search'
|
||||
config:
|
||||
sampleOverCapGlobResults: false
|
||||
|
||||
- id: workspace-context
|
||||
name: '@deepseek-ai/dsh-workspace-context'
|
||||
config:
|
||||
maxBytes: 65536
|
||||
|
||||
- id: skill
|
||||
name: '@deepseek-ai/dsh-skill'
|
||||
|
||||
- id: skill-local
|
||||
name: '@deepseek-ai/dsh-skill-local'
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
|
||||
- id: commands
|
||||
name: '@deepseek-ai/dsh-commands'
|
||||
|
||||
- id: goal
|
||||
name: '@deepseek-ai/dsh-goal'
|
||||
|
||||
- id: goal-session
|
||||
name: '@deepseek-ai/dsh-goal-session'
|
||||
|
||||
- id: command-goal
|
||||
name: '@deepseek-ai/dsh-command-goal'
|
||||
|
||||
- id: plan-mode
|
||||
name: '@deepseek-ai/dsh-plan-mode'
|
||||
config:
|
||||
section: |
|
||||
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
||||
|
||||
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
||||
|
||||
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed only to keep the request shape stable. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
||||
|
||||
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
||||
|
||||
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
||||
|
||||
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
||||
|
||||
- id: token-meter
|
||||
name: '@deepseek-ai/dsh-token-meter'
|
||||
|
||||
- id: compact-basic
|
||||
name: '@deepseek-ai/dsh-compact-basic'
|
||||
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
- id: subagent-spawn
|
||||
name: '@deepseek-ai/dsh-subagent-spawn'
|
||||
config:
|
||||
providerName: spawn
|
||||
|
||||
- id: subagent-fork
|
||||
name: '@deepseek-ai/dsh-subagent-fork'
|
||||
config:
|
||||
providerName: fork
|
||||
|
||||
- id: tool-subagent
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: spawn
|
||||
toolName: subagent
|
||||
|
||||
- id: tool-subagent-fork
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: fork
|
||||
toolName: subagent_fork
|
||||
|
||||
- id: workflow-workerthread
|
||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||
config:
|
||||
provider: spawn
|
||||
|
||||
- id: tool-workflow
|
||||
name: '@deepseek-ai/dsh-tool-workflow'
|
||||
|
||||
- id: timeout-policy
|
||||
name: '@deepseek-ai/dsh-timeout-policy'
|
||||
|
||||
- id: spill-local
|
||||
name: '@deepseek-ai/dsh-spill-local'
|
||||
|
||||
- id: spill-policy
|
||||
name: '@deepseek-ai/dsh-spill-policy'
|
||||
config:
|
||||
maxInlineBytes: 50000
|
||||
|
||||
# ── rows every surface mounts, whose values each overlay states ──────────────
|
||||
|
||||
# The tool registry. Presentation mode is a surface choice, so each overlay
|
||||
# states it; 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
|
||||
# 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.
|
||||
- id: agent-loop
|
||||
name: '@deepseek-ai/dsh-agent-loop'
|
||||
config:
|
||||
agents: []
|
||||
|
||||
# The filesystem provider. `cwd` defaults to the package's `process.cwd()`; the
|
||||
# TUI states it explicitly because that value is also the session workspace.
|
||||
- id: fs-local
|
||||
name: '@deepseek-ai/dsh-fs-local'
|
||||
|
||||
# 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
|
||||
# choice.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
113
apps/cli/config/tui.cordis.yml
Normal file
113
apps/cli/config/tui.cordis.yml
Normal file
@@ -0,0 +1,113 @@
|
||||
# `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-local
|
||||
config:
|
||||
cwd: !!js process.cwd()
|
||||
|
||||
# The shipped TUI presents the native tool registry.
|
||||
- id: tools
|
||||
config:
|
||||
mode: native
|
||||
|
||||
# ── TUI-only rows ───────────────────────────────────────────────────────────
|
||||
|
||||
- insert:
|
||||
# Relational runtime checks over the authoritative event streams; each
|
||||
# companion registers the assertions its own package owns.
|
||||
- id: invariants
|
||||
name: '@deepseek-ai/dsh-invariants'
|
||||
- id: session-invariant
|
||||
name: '@deepseek-ai/dsh-session/invariant'
|
||||
- id: agent-invariant
|
||||
name: '@deepseek-ai/dsh-agent/invariant'
|
||||
- id: scope-invariant
|
||||
name: '@deepseek-ai/dsh-scope/invariant'
|
||||
- id: agent-loop-invariant
|
||||
name: '@deepseek-ai/dsh-agent-loop/invariant'
|
||||
|
||||
- id: session-checkpoint-policy
|
||||
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
|
||||
# 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'
|
||||
|
||||
# Compacts oversized tool results before the broader conversation compactor
|
||||
# runs, preserving the model-visible result within the configured budget.
|
||||
- id: tool-result-prune
|
||||
name: '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
|
||||
# Persisted same-session goals reach the model and the slash menu here; the
|
||||
# domain, driver, and `/goal` command are in the base.
|
||||
- id: tool-goal
|
||||
name: '@deepseek-ai/dsh-tool-goal'
|
||||
|
||||
# A separate fixed consumer demonstrates fresh-agent Ralph iteration without
|
||||
# changing the workflow tool or same-session goal behavior.
|
||||
- id: tool-ralph
|
||||
name: '@deepseek-ai/dsh-tool-ralph'
|
||||
|
||||
# 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'
|
||||
223
apps/cli/config/web.cordis.yml
Normal file
223
apps/cli/config/web.cordis.yml
Normal file
@@ -0,0 +1,223 @@
|
||||
# `dsh web` — the browser surface, as a patch list over `base.cordis.yml`.
|
||||
# The launcher includes the base and applies this file, then any `--config`
|
||||
# overlay, then AppCLIEntry's profile-json and CLI-flag patches, 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. `--dev` appends the dsh-client-hmr row in code
|
||||
# (AppCLIEntry).
|
||||
|
||||
# ── surface-specific values the base deliberately omits ─────────────────────
|
||||
|
||||
# TODO: Re-enable shared HMR for Web after its reload lifecycle is tested.
|
||||
- id: hmr
|
||||
disabled: true
|
||||
|
||||
# Web content search runs on an ephemeral in-memory index. The service
|
||||
# activates at boot, while first-search defers the node:sqlite import and
|
||||
# in-memory handle so Node 22 startup stays quiet until content search
|
||||
# actually uses SQLite. That search then reconciles this boot's sources.
|
||||
- id: session-query-sqlite
|
||||
config:
|
||||
path: ':memory:'
|
||||
openAt: first-search
|
||||
|
||||
- id: tools
|
||||
config:
|
||||
# TEMPORARY workaround: DSH_TOOLS_MODE (native|code|both) opts a whole dsh
|
||||
# process into Code Mode while per-session tool-mode selection is being
|
||||
# designed; unset keeps the schema default (native). Remove the env seam
|
||||
# once the web UI owns the choice per session.
|
||||
mode: !!js process.env.DSH_TOOLS_MODE
|
||||
|
||||
- id: llm-deepseek
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||
|
||||
# The web surface replaces the unrestricted local executors with the shared
|
||||
# sandbox policy. Its default preserves the previous unrestricted behavior;
|
||||
# DSH_PERMISSION_MODE and the browser permission picker can confine a session.
|
||||
- insert:
|
||||
- id: sandbox
|
||||
name: '@deepseek-ai/dsh-sandbox-local'
|
||||
|
||||
- id: sandbox-policy
|
||||
name: '@deepseek-ai/dsh-sandbox-policy'
|
||||
config:
|
||||
mode: !!js process.env.DSH_PERMISSION_MODE ?? 'danger-full-access'
|
||||
workspaceRoot: !!js process.cwd()
|
||||
|
||||
- id: bash-sandbox
|
||||
name: '@deepseek-ai/dsh-bash-sandbox'
|
||||
|
||||
- id: approval
|
||||
name: '@deepseek-ai/dsh-user-approval'
|
||||
config:
|
||||
policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'danger-full-access') === 'danger-full-access' ? 'never' : 'ask'"
|
||||
|
||||
- id: permission
|
||||
name: '@deepseek-ai/dsh-permission'
|
||||
config:
|
||||
presets:
|
||||
read-only:
|
||||
sandbox: read-only
|
||||
approval: ask
|
||||
workspace-write:
|
||||
sandbox: workspace-write
|
||||
approval: ask
|
||||
danger-full-access:
|
||||
sandbox: danger-full-access
|
||||
approval: never
|
||||
|
||||
- id: fs-sandbox
|
||||
name: '@deepseek-ai/dsh-fs-sandbox'
|
||||
|
||||
- id: bash-local
|
||||
disabled: true
|
||||
|
||||
- id: fs-local
|
||||
disabled: true
|
||||
|
||||
# ── web-only host rows, the transport layer, and the browser roster ─────────
|
||||
|
||||
# `dshClient` rows are the browser roster the modules node half scans into
|
||||
# window.__DSH_BOOT__; the modules row is simultaneously a host row.
|
||||
- insert:
|
||||
- id: session-projection
|
||||
name: '@deepseek-ai/dsh-session-projection'
|
||||
|
||||
- id: code-runtime
|
||||
name: '@deepseek-ai/dsh-code-runtime-worker'
|
||||
|
||||
- id: storage
|
||||
name: '@deepseek-ai/dsh-storage'
|
||||
|
||||
- id: storage-json
|
||||
name: '@deepseek-ai/dsh-storage-json'
|
||||
config:
|
||||
root: './.storages'
|
||||
|
||||
- id: storage-domain
|
||||
name: '@deepseek-ai/dsh-storage-domain'
|
||||
config:
|
||||
backend: json
|
||||
|
||||
- id: workspace
|
||||
name: '@deepseek-ai/dsh-workspace'
|
||||
|
||||
- id: session-projection-cache
|
||||
name: '@deepseek-ai/dsh-session-projection-cache'
|
||||
config:
|
||||
writeEveryEvents: 200
|
||||
writeIntervalMs: 5000
|
||||
|
||||
- id: tool-todo
|
||||
name: '@deepseek-ai/dsh-tool-todo'
|
||||
|
||||
# Resolve bind host, SSH launch, and display once at boot, then mount the
|
||||
# matching dual-face directory picker. Mount -native or -browse directly in
|
||||
# an overlay to pin the interaction.
|
||||
- id: directory-picker
|
||||
name: '@deepseek-ai/dsh-host-directory-picker-auto'
|
||||
|
||||
# The API gateway: the transport-agnostic dispatch face every client shape
|
||||
# shares. provider/model are the host default routing — the profile json's
|
||||
# mapping target (user config overrides these engineering defaults).
|
||||
- id: api-gateway
|
||||
name: '@deepseek-ai/dsh-host-apiproxy'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
|
||||
# ── layer 2: transport/service ──────────────────────────────────────────────
|
||||
|
||||
# Plain route-registration carrier. distIndex is an assembly fact, not user
|
||||
# config — AppCLIEntry resolves the frontend dist and patches it in; host and
|
||||
# port arrive as CLI-flag patches over these defaults.
|
||||
- id: webserver
|
||||
name: '@deepseek-ai/dsh-host-webserver'
|
||||
config:
|
||||
host: 127.0.0.1
|
||||
port: 3080
|
||||
|
||||
# ── browser plugin roster (dshClient rows; node halves are layer-2 hosts) ──
|
||||
|
||||
# Dual-face: node half scans this very tree for dshClient rows, composes
|
||||
# window.__DSH_BOOT__, serves /plugins/<id>/client.js; browser half is the
|
||||
# module table the shell kernel constructs before cordis exists (§4.7 —
|
||||
# adopted as a plugin entry by the kernel, never fetched).
|
||||
- id: modules
|
||||
name: '@deepseek-ai/dsh-client-modules'
|
||||
|
||||
# Owns both ends of the web transport: node half binds the gateway to the
|
||||
# webserver under /api; browser half is the fetch/SSE client.
|
||||
- id: connection
|
||||
name: '@deepseek-ai/dsh-client-connection'
|
||||
|
||||
- id: client-runtime
|
||||
name: '@deepseek-ai/dsh-client-runtime'
|
||||
|
||||
- id: ui-theme
|
||||
name: '@deepseek-ai/dsh-client-ui-theme'
|
||||
|
||||
- id: locale
|
||||
name: '@deepseek-ai/dsh-client-locale'
|
||||
|
||||
- id: ui-layout
|
||||
name: '@deepseek-ai/dsh-client-ui-layout'
|
||||
|
||||
- id: ui-sidebar
|
||||
name: '@deepseek-ai/dsh-client-ui-sidebar'
|
||||
|
||||
- id: ui-settings
|
||||
name: '@deepseek-ai/dsh-client-ui-settings'
|
||||
|
||||
- id: ui-settings-general
|
||||
name: '@deepseek-ai/dsh-client-ui-settings-general'
|
||||
|
||||
- id: ui-models
|
||||
name: '@deepseek-ai/dsh-client-ui-models'
|
||||
|
||||
- id: ui-conversation
|
||||
name: '@deepseek-ai/dsh-client-ui-conversation'
|
||||
|
||||
|
||||
- id: ui-workspace
|
||||
name: '@deepseek-ai/dsh-client-ui-workspace'
|
||||
|
||||
# Input triggers: the '/' | '@' pipeline (ui-slash), the command surface over
|
||||
# it (ui-command), and the two reference sources (ui-skill / ui-subagent).
|
||||
- id: ui-slash
|
||||
name: '@deepseek-ai/dsh-client-ui-slash'
|
||||
|
||||
- id: ui-command
|
||||
name: '@deepseek-ai/dsh-client-ui-command'
|
||||
|
||||
- id: ui-skill
|
||||
name: '@deepseek-ai/dsh-client-ui-skill'
|
||||
|
||||
- id: ui-subagent
|
||||
name: '@deepseek-ai/dsh-client-ui-subagent'
|
||||
|
||||
# Goal surface: GoalBar in the input dock over the goal session projection.
|
||||
- id: ui-goal
|
||||
name: '@deepseek-ai/dsh-client-ui-goal'
|
||||
|
||||
# Model selection: the /model popupSelect + composer seat over session.models.
|
||||
- id: ui-model
|
||||
name: '@deepseek-ai/dsh-client-ui-model'
|
||||
|
||||
- id: ui-permission
|
||||
name: '@deepseek-ai/dsh-client-ui-permission'
|
||||
|
||||
# Plan control: the composer plan seat over the plan projection + /plan channel.
|
||||
- id: ui-plan
|
||||
name: '@deepseek-ai/dsh-client-ui-plan'
|
||||
|
||||
- id: ui-question
|
||||
name: '@deepseek-ai/dsh-client-ui-question'
|
||||
|
||||
- id: ui-trajectory
|
||||
name: '@deepseek-ai/dsh-client-ui-trajectory'
|
||||
@@ -1,420 +0,0 @@
|
||||
# dsh web — the full web-shape composition: host runtime (layer 1), the
|
||||
# transport/service layer (layer 2), and the browser plugin roster (dshClient
|
||||
# rows the modules node half scans into window.__DSH_BOOT__). Row order
|
||||
# carries no load semantics (activation is service-availability driven); the
|
||||
# grouping below is for readers. `--dev` appends the dsh-client-hmr row in
|
||||
# code (AppCLIEntry) — prod and dev differ by exactly that one row.
|
||||
# AppCLIEntry patches this tree before boot: profile json + CLI flags +
|
||||
# distIndex land as config patches over the rows below (yaml = engineering
|
||||
# defaults, json = user config, user wins per field).
|
||||
|
||||
# ── layer 1: runtime ────────────────────────────────────────────────────────
|
||||
|
||||
- id: timer
|
||||
name: '@cordisjs/plugin-timer'
|
||||
|
||||
- id: llm
|
||||
name: '@deepseek-ai/dsh-llm'
|
||||
|
||||
- id: session
|
||||
name: '@deepseek-ai/dsh-session'
|
||||
|
||||
# Projection registry: drives every registered domain unit over committed
|
||||
# session events and serves finished values (history-tail projections block +
|
||||
# session/projection frames). Without this row every domain's optional unit
|
||||
# injection stays silent — no block, no frames, no titles/todos on the web.
|
||||
- id: session-projection
|
||||
name: '@deepseek-ai/dsh-session-projection'
|
||||
|
||||
- id: session-title
|
||||
name: '@deepseek-ai/dsh-session-title'
|
||||
config:
|
||||
fallbackMaxWords: 5
|
||||
fallbackMaxBytes: 40
|
||||
maxTitleBytes: 80
|
||||
|
||||
# Model-made titles on the first-message cadence (the web sidebar renders
|
||||
# session/title). Same values as the TUI composition.
|
||||
- id: session-title-llm
|
||||
name: '@deepseek-ai/dsh-session-title-first-message-llm'
|
||||
config:
|
||||
targetWords: 5
|
||||
targetCjkCharacters: 10
|
||||
maxInputBytes: 4096
|
||||
maxOutputTokens: 64
|
||||
timeoutMs: 60000
|
||||
|
||||
- id: system-prompt
|
||||
name: '@deepseek-ai/dsh-system-prompt'
|
||||
config:
|
||||
persona: ''
|
||||
|
||||
- id: tools
|
||||
name: '@deepseek-ai/dsh-tools'
|
||||
config:
|
||||
# TEMPORARY workaround: DSH_TOOLS_MODE (native|code|both) opts a whole dsh
|
||||
# process into Code Mode while per-session tool-mode selection is being
|
||||
# designed; unset keeps the schema default (native). Remove the env seam
|
||||
# once the web UI owns the choice per session.
|
||||
mode: !!js process.env.DSH_TOOLS_MODE
|
||||
|
||||
# Code Mode substrate for the row above. Mounted unconditionally because
|
||||
# Loader metadata is static (no conditional rows): a native-mode boot only
|
||||
# registers the service — a worker thread spawns per run_code execution.
|
||||
- id: code-runtime
|
||||
name: '@deepseek-ai/dsh-code-runtime-worker'
|
||||
|
||||
- id: user-interaction
|
||||
name: '@deepseek-ai/dsh-user-interaction'
|
||||
|
||||
- id: agent
|
||||
name: '@deepseek-ai/dsh-agent'
|
||||
|
||||
- id: tasks
|
||||
name: '@deepseek-ai/dsh-tasks-local'
|
||||
|
||||
- id: agent-loop
|
||||
name: '@deepseek-ai/dsh-agent-loop'
|
||||
config:
|
||||
agents: []
|
||||
|
||||
# The native DeepSeek adapter; reads the key/base-url the boot's layered
|
||||
# .env loading (cwd then $DSH_HOME) left in the environment.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||
|
||||
# Common pi-ai provider routes read credentials and endpoint overrides from the
|
||||
# boot's layered environment.
|
||||
- id: llm-pi-ai
|
||||
name: '@deepseek-ai/dsh-llm-pi-ai'
|
||||
config:
|
||||
providers:
|
||||
- provider: openai
|
||||
apiKey: !!js process.env.OPENAI_API_KEY
|
||||
baseURL: !!js process.env.OPENAI_BASE_URL
|
||||
- provider: anthropic
|
||||
apiKey: !!js process.env.ANTHROPIC_API_KEY
|
||||
baseURL: !!js process.env.ANTHROPIC_BASE_URL
|
||||
|
||||
# Transient-failure recovery around the loop's model calls (same policy as
|
||||
# the TUI's agent-spine composition; defaults: 2 retries, 500ms→10s backoff).
|
||||
- id: llm-retry
|
||||
name: '@deepseek-ai/dsh-llm-retry'
|
||||
|
||||
# Session store root. AppCLIEntry resolves the engineering default to a
|
||||
# global dir under the Harness home ($DSH_HOME, else ~/.dsh): sessions live
|
||||
# in one place across every cwd, not a project-local ./.sessions. The
|
||||
# persistenceRoot profile key (user config) still overrides this per field.
|
||||
- id: session-persistence-jsonl
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
|
||||
- id: storage
|
||||
name: '@deepseek-ai/dsh-storage'
|
||||
|
||||
- id: storage-json
|
||||
name: '@deepseek-ai/dsh-storage-json'
|
||||
config:
|
||||
root: './.storages'
|
||||
|
||||
- id: storage-domain
|
||||
name: '@deepseek-ai/dsh-storage-domain'
|
||||
config:
|
||||
backend: json
|
||||
|
||||
- id: workspace
|
||||
name: '@deepseek-ai/dsh-workspace'
|
||||
|
||||
# Persisted projection cache: durable per-session checkpoints of every
|
||||
# registered projection unit (json backend → ./.storages/session_projcache.json,
|
||||
# beside workspace.json), throttled between the two mandatory points
|
||||
# (turn/end + detach), serving cold listings without full-log loads.
|
||||
- id: session-projection-cache
|
||||
name: '@deepseek-ai/dsh-session-projection-cache'
|
||||
config:
|
||||
writeEveryEvents: 200
|
||||
writeIntervalMs: 5000
|
||||
|
||||
# Managed child-process groups for the bash executor (spawn/kill/output plumbing).
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
# The sandboxed product path (the acp-agent composition): per-platform
|
||||
# runner provider, the shared policy home, the confined bash executor, and
|
||||
# the approval seam its escalation asks through. The web deployment default
|
||||
# is danger-full-access + never (same behavior as the former bash-local
|
||||
# rows); DSH_PERMISSION_MODE opts a process into a confined default, and
|
||||
# per-session switches ride the /permission command's knob events.
|
||||
- id: sandbox
|
||||
name: '@deepseek-ai/dsh-sandbox-local'
|
||||
|
||||
- id: sandbox-policy
|
||||
name: '@deepseek-ai/dsh-sandbox-policy'
|
||||
config:
|
||||
mode: !!js process.env.DSH_PERMISSION_MODE ?? 'danger-full-access'
|
||||
workspaceRoot: !!js process.cwd()
|
||||
|
||||
- id: bash-sandbox
|
||||
name: '@deepseek-ai/dsh-bash-sandbox'
|
||||
|
||||
- id: approval
|
||||
name: '@deepseek-ai/dsh-user-approval'
|
||||
config:
|
||||
policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'danger-full-access') === 'danger-full-access' ? 'never' : 'ask'"
|
||||
|
||||
# Presets over the two knobs (requires the confining executor + approval):
|
||||
# the web permission chip's table, served through the permissions projection
|
||||
# and switched through /permission.
|
||||
- id: permission
|
||||
name: '@deepseek-ai/dsh-permission'
|
||||
config:
|
||||
presets:
|
||||
read-only:
|
||||
sandbox: read-only
|
||||
approval: ask
|
||||
workspace-write:
|
||||
sandbox: workspace-write
|
||||
approval: ask
|
||||
danger-full-access:
|
||||
sandbox: danger-full-access
|
||||
approval: never
|
||||
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
|
||||
- id: tool-todo
|
||||
name: '@deepseek-ai/dsh-tool-todo'
|
||||
|
||||
- id: tool-tasks
|
||||
name: '@deepseek-ai/dsh-tool-tasks'
|
||||
|
||||
# fs cwd stays the package default (process.cwd()) — the same value the
|
||||
# gateway injects into session.cwd, so paths and sessions agree. The
|
||||
# sandboxed backend rides the SAME policy as bash: write/edit fence by the
|
||||
# effective mode, so read/write/edit stay available under every mode.
|
||||
- id: fs-sandbox
|
||||
name: '@deepseek-ai/dsh-fs-sandbox'
|
||||
|
||||
- id: fs-policy
|
||||
name: '@deepseek-ai/dsh-fs-policy'
|
||||
|
||||
- id: tool-fs
|
||||
name: '@deepseek-ai/dsh-tool-fs'
|
||||
|
||||
- id: tool-fs-search
|
||||
name: '@deepseek-ai/dsh-tool-fs-search'
|
||||
|
||||
- id: workspace-context
|
||||
name: '@deepseek-ai/dsh-workspace-context'
|
||||
config:
|
||||
maxBytes: 65536
|
||||
|
||||
- id: skill
|
||||
name: '@deepseek-ai/dsh-skill'
|
||||
|
||||
- id: skill-local
|
||||
name: '@deepseek-ai/dsh-skill-local'
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
|
||||
# Host command registry: the single source of truth behind command.list /
|
||||
# command.execute; the web '/' menu is a pure projection of this registry.
|
||||
- id: commands
|
||||
name: '@deepseek-ai/dsh-commands'
|
||||
|
||||
# Goal service + automatic same-session continuation + the /goal command.
|
||||
# The GoalService registers the 'goal' session projection unit; the web
|
||||
# GoalBar reads it through useProjection.
|
||||
- id: goal
|
||||
name: '@deepseek-ai/dsh-goal'
|
||||
|
||||
- id: goal-session
|
||||
name: '@deepseek-ai/dsh-goal-session'
|
||||
|
||||
- id: command-goal
|
||||
name: '@deepseek-ai/dsh-command-goal'
|
||||
|
||||
# Plan mode registers /plan (the first real command on the web surface).
|
||||
# Section text mirrors examples/tui-agent/cordis.yml (the reference
|
||||
# deployment); plan-mode throws at load on an empty section.
|
||||
- id: plan-mode
|
||||
name: '@deepseek-ai/dsh-plan-mode'
|
||||
config:
|
||||
section: |
|
||||
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
||||
|
||||
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
||||
|
||||
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed only to keep the request shape stable. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
||||
|
||||
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
||||
|
||||
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
||||
|
||||
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
||||
|
||||
# token-meter rejects unknown config keys — keep this row bare.
|
||||
- id: token-meter
|
||||
name: '@deepseek-ai/dsh-token-meter'
|
||||
|
||||
- id: compact-basic
|
||||
name: '@deepseek-ai/dsh-compact-basic'
|
||||
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
- id: subagent-spawn
|
||||
name: '@deepseek-ai/dsh-subagent-spawn'
|
||||
config:
|
||||
providerName: spawn
|
||||
|
||||
- id: subagent-fork
|
||||
name: '@deepseek-ai/dsh-subagent-fork'
|
||||
config:
|
||||
providerName: fork
|
||||
|
||||
- id: tool-subagent
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: spawn
|
||||
toolName: subagent
|
||||
|
||||
- id: tool-subagent-fork
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: fork
|
||||
toolName: subagent_fork
|
||||
|
||||
- id: workflow-workerthread
|
||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||
config:
|
||||
provider: spawn
|
||||
|
||||
- id: tool-workflow
|
||||
name: '@deepseek-ai/dsh-tool-workflow'
|
||||
|
||||
- id: timeout-policy
|
||||
name: '@deepseek-ai/dsh-timeout-policy'
|
||||
|
||||
- id: spill-local
|
||||
name: '@deepseek-ai/dsh-spill-local'
|
||||
|
||||
# Omitting maxInlineBytes makes the whole policy a silent no-op — always
|
||||
# state it explicitly.
|
||||
- id: spill-policy
|
||||
name: '@deepseek-ai/dsh-spill-policy'
|
||||
config:
|
||||
maxInlineBytes: 50000
|
||||
|
||||
# The API gateway: the transport-agnostic dispatch face every client shape
|
||||
# shares. provider/model are the host default routing — the profile json's
|
||||
# mapping target (user config overrides these engineering defaults).
|
||||
# Directory-picking package, dual-face: the node half serves the gateway's
|
||||
# host.* picker RPCs, the browser half fills ui-workspace's directory-flow
|
||||
# slots — one row composes the whole interaction. Swap point: mount
|
||||
# '-native' instead for the host-display OS chooser.
|
||||
- id: directory-picker
|
||||
name: '@deepseek-ai/dsh-host-directory-picker-browse'
|
||||
|
||||
- id: api-gateway
|
||||
name: '@deepseek-ai/dsh-host-apiproxy'
|
||||
config:
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
|
||||
# ── layer 2: transport/service ──────────────────────────────────────────────
|
||||
|
||||
# Plain route-registration carrier. distIndex is an assembly fact, not user
|
||||
# config — AppCLIEntry resolves the frontend dist and patches it in; host and
|
||||
# port arrive as CLI-flag patches over these defaults.
|
||||
- id: webserver
|
||||
name: '@deepseek-ai/dsh-host-webserver'
|
||||
config:
|
||||
host: 127.0.0.1
|
||||
port: 3080
|
||||
|
||||
# ── browser plugin roster (dshClient rows; node halves are layer-2 hosts) ──
|
||||
|
||||
# Dual-face: node half scans this very tree for dshClient rows, composes
|
||||
# window.__DSH_BOOT__, serves /plugins/<id>/client.js; browser half is the
|
||||
# module table the shell kernel constructs before cordis exists (§4.7 —
|
||||
# adopted as a plugin entry by the kernel, never fetched).
|
||||
- id: modules
|
||||
name: '@deepseek-ai/dsh-client-modules'
|
||||
|
||||
# Owns both ends of the web transport: node half binds the gateway to the
|
||||
# webserver under /api; browser half is the fetch/SSE client.
|
||||
- id: connection
|
||||
name: '@deepseek-ai/dsh-client-connection'
|
||||
|
||||
- id: client-runtime
|
||||
name: '@deepseek-ai/dsh-client-runtime'
|
||||
|
||||
- id: ui-theme
|
||||
name: '@deepseek-ai/dsh-client-ui-theme'
|
||||
|
||||
- id: locale
|
||||
name: '@deepseek-ai/dsh-client-locale'
|
||||
|
||||
- id: ui-layout
|
||||
name: '@deepseek-ai/dsh-client-ui-layout'
|
||||
|
||||
- id: ui-sidebar
|
||||
name: '@deepseek-ai/dsh-client-ui-sidebar'
|
||||
|
||||
- id: ui-settings
|
||||
name: '@deepseek-ai/dsh-client-ui-settings'
|
||||
|
||||
- id: ui-settings-general
|
||||
name: '@deepseek-ai/dsh-client-ui-settings-general'
|
||||
|
||||
- id: ui-models
|
||||
name: '@deepseek-ai/dsh-client-ui-models'
|
||||
|
||||
- id: ui-conversation
|
||||
name: '@deepseek-ai/dsh-client-ui-conversation'
|
||||
|
||||
|
||||
- id: ui-workspace
|
||||
name: '@deepseek-ai/dsh-client-ui-workspace'
|
||||
|
||||
# Input triggers: the '/' | '@' pipeline (ui-slash), the command surface over
|
||||
# it (ui-command), and the two reference sources (ui-skill / ui-subagent).
|
||||
- id: ui-slash
|
||||
name: '@deepseek-ai/dsh-client-ui-slash'
|
||||
|
||||
- id: ui-command
|
||||
name: '@deepseek-ai/dsh-client-ui-command'
|
||||
|
||||
- id: ui-skill
|
||||
name: '@deepseek-ai/dsh-client-ui-skill'
|
||||
|
||||
- id: ui-subagent
|
||||
name: '@deepseek-ai/dsh-client-ui-subagent'
|
||||
|
||||
# Goal surface: GoalBar in the input dock over the goal session projection.
|
||||
- id: ui-goal
|
||||
name: '@deepseek-ai/dsh-client-ui-goal'
|
||||
|
||||
# Model selection: the /model popupSelect + composer seat over session.models.
|
||||
- id: ui-model
|
||||
name: '@deepseek-ai/dsh-client-ui-model'
|
||||
|
||||
# The /permission popup picker (hostBacked over the host /permission command).
|
||||
- id: ui-permission
|
||||
name: '@deepseek-ai/dsh-client-ui-permission'
|
||||
|
||||
# Plan control: the composer plan seat over the plan projection + /plan channel.
|
||||
- id: ui-plan
|
||||
name: '@deepseek-ai/dsh-client-ui-plan'
|
||||
|
||||
- id: ui-question
|
||||
name: '@deepseek-ai/dsh-client-ui-question'
|
||||
|
||||
- id: ui-trajectory
|
||||
name: '@deepseek-ai/dsh-client-ui-trajectory'
|
||||
@@ -9,17 +9,19 @@
|
||||
},
|
||||
"files": [
|
||||
"lib/bin.js",
|
||||
"cordis.yml",
|
||||
"config",
|
||||
"src"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@cordisjs/plugin-hmr": "workspace:*",
|
||||
"@cordisjs/plugin-include": "workspace:*",
|
||||
"@cordisjs/plugin-loader": "workspace:*",
|
||||
"@cordisjs/plugin-timer": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-app-boot": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-hmr": "workspace:^",
|
||||
@@ -48,15 +50,20 @@
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-compact-basic": "workspace:^",
|
||||
"@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-frontend": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-directory-picker-auto": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-directory-picker-native": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
|
||||
@@ -66,12 +73,19 @@
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-scope": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:^",
|
||||
"@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:^",
|
||||
"@deepseek-ai/dsh-settings-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-spill-local": "workspace:^",
|
||||
@@ -87,9 +101,13 @@
|
||||
"@deepseek-ai/dsh-tasks-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-fs-search": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
|
||||
@@ -107,6 +125,9 @@
|
||||
"js-yaml": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9"
|
||||
"@deepseek-ai/dsh-llm-mock-server": "workspace:^",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"execa": "^10.0.0",
|
||||
"node-pty": "1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
/**
|
||||
* AppCLIEntry — the pre-cordis boot glue the config-tree dsh surfaces share
|
||||
* (`dsh web` and `dsh -p` boot the one composition; TUI migrates later).
|
||||
* Everything here is what must exist before the Loader runs: layered env,
|
||||
* the patch composition over the shipped cordis.yml (profile json + CLI
|
||||
* flags + the resolved frontend dist), and the fail-loud triple after the
|
||||
* tree settles.
|
||||
* for the Web/headless surface.
|
||||
* Everything here is what must exist before the Loader runs: the patch
|
||||
* composition over the shipped base and surface 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
|
||||
* provider and is never hoisted here.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { networkInterfaces } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { Context } from 'cordis'
|
||||
import type { FiberState } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include, { type PatchOptions } from '@cordisjs/plugin-include'
|
||||
import type { PatchOptions } from '@cordisjs/plugin-include'
|
||||
import yaml from 'js-yaml'
|
||||
import { assertEntriesLoaded, installFailLoud, loadEnv } from '@deepseek-ai/dsh-app-boot'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
import { boot, installFailLoud, loadOverlayPatches, loadPersonalPatches } from '@deepseek-ai/dsh-app-boot'
|
||||
// Empty type import carries the httpServer Context merge for the port read below.
|
||||
import type {} from '@deepseek-ai/dsh-host-webserver'
|
||||
|
||||
@@ -26,6 +24,9 @@ import type {} from '@deepseek-ai/dsh-host-webserver'
|
||||
const PROFILE_DIR = '.dsh-tmp-profile'
|
||||
const PROFILE_FILE = 'config.json'
|
||||
|
||||
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets (mounted in web.cordis.yml). */
|
||||
const TELEMETRY_ROW_ID = 'telemetry-otel'
|
||||
|
||||
/** The webserver schema's all-interfaces bind literal: gates LAN-authority derivation here and the printed LAN URL in web.ts. */
|
||||
const ALL_INTERFACES_HOST = '0.0.0.0'
|
||||
|
||||
@@ -61,6 +62,38 @@ export function resolveLanTrust(
|
||||
return { lanAddresses, trustedHosts: [...lanAddresses, ...extra] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the telemetry opt-out switch into its boot patch. ANY non-empty
|
||||
* value (including `'0'`/`'false'`) disables: a privacy switch prefers
|
||||
* off-by-mistake over on-by-mistake. Throws when the switch is set but the
|
||||
* row is absent — a silently no-op "disabled" privacy switch would keep
|
||||
* exporting while the user believes it is off.
|
||||
* @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset).
|
||||
* @param hasRow - whether the composition carries the {@link TELEMETRY_ROW_ID} row.
|
||||
* @returns the disable patch, or `undefined` when telemetry stays enabled.
|
||||
*/
|
||||
export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: boolean): PatchOptions | undefined {
|
||||
if ((disabledEnv ?? '') === '') return undefined
|
||||
if (!hasRow) {
|
||||
throw new Error(`dsh: DSH_TELEMETRY_DISABLED is set but row "${TELEMETRY_ROW_ID}" is not in this composition`)
|
||||
}
|
||||
return { id: TELEMETRY_ROW_ID, disabled: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* @param file - absolute path of the config or overlay file.
|
||||
* @returns true when a top-level (or inserted) row has the telemetry id.
|
||||
*/
|
||||
export function configHasTelemetryRow(file: string): boolean {
|
||||
const doc = yaml.load(readFileSync(file, 'utf8'), { schema: includeYamlSchema })
|
||||
if (!Array.isArray(doc)) throw new Error(`dsh: ${file} is not a top-level entry list`)
|
||||
return (doc as { id?: string; insert?: { id?: string }[] }[]).some(row =>
|
||||
row.id === TELEMETRY_ROW_ID || (row.insert ?? []).some(inserted => inserted.id === TELEMETRY_ROW_ID))
|
||||
}
|
||||
|
||||
/** One profile-json key mapped onto a yml row's config field. */
|
||||
interface ProfileMapping {
|
||||
jsonPath: string
|
||||
@@ -90,18 +123,23 @@ const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
})
|
||||
const includeYamlSchema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||
|
||||
/**
|
||||
* Value mirror of cordis's `FiberState` const enum members the sweep needs
|
||||
* (a const enum has no runtime object to import; same rationale as the
|
||||
* client-side mirror in dsh-client-web).
|
||||
*/
|
||||
const FIBER_ACTIVE = 2 as FiberState.ACTIVE
|
||||
const FIBER_PENDING = 0 as FiberState.PENDING
|
||||
|
||||
/** Constructor facts for one dsh invocation over the shared composition (argv already parsed by the surface bin). */
|
||||
export interface AppCLIEntryOptions {
|
||||
/** Absolute path of the shipped cordis.yml. */
|
||||
/** Absolute path of the shared base config the Loader includes. */
|
||||
configPath: string
|
||||
/**
|
||||
* Absolute path of this surface's overlay: a patch list applied over
|
||||
* {@link configPath} before this entry's own profile/flag patches. Its rows
|
||||
* are also merge inputs, so a flag override preserves the overlay's other
|
||||
* fields on the same row.
|
||||
*/
|
||||
overlayPath: string
|
||||
/**
|
||||
* Optional explicit overlay applied after {@link overlayPath} and before
|
||||
* this entry's own profile/flag patches. When absent, the personal
|
||||
* `$DSH_HOME/config.yaml` overlay is applied instead.
|
||||
*/
|
||||
extraOverlayPath?: string
|
||||
/** Whether to append the HMR row (the whole prod/dev difference; web surface only). */
|
||||
dev: boolean
|
||||
/** --host when explicitly passed; undefined keeps the yml engineering default. */
|
||||
@@ -142,12 +180,11 @@ export class AppCLIEntry {
|
||||
constructor(private readonly options: AppCLIEntryOptions) {}
|
||||
|
||||
/**
|
||||
* Run the boot chain: layered env → patch composition → Loader include
|
||||
* boot (dev row before await) → fail-loud triple.
|
||||
* Run the boot chain: patch composition → Loader include boot (dev row
|
||||
* before await) → fail-loud triple.
|
||||
* @returns the settled root context and the listening port.
|
||||
*/
|
||||
async run(): Promise<{ ctx: Context; port: number }> {
|
||||
this.loadEnvLayers()
|
||||
this.composePatches()
|
||||
await this.bootTree()
|
||||
this.assertBoot()
|
||||
@@ -157,16 +194,9 @@ export class AppCLIEntry {
|
||||
return { ctx: this.ctx, port }
|
||||
}
|
||||
|
||||
/** Layered .env: ambient > cwd (bin already loaded) > $DSH_HOME (loadEnvFile never overrides). */
|
||||
private loadEnvLayers(): void {
|
||||
loadEnv('dsh', resolveDshHome())
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the patch set from the non-yml config sources: computed
|
||||
* engineering defaults (the global session root), profile json (user
|
||||
* config, overriding those defaults), CLI flags, and the resolved frontend
|
||||
* dist. Patches replace a row's config wholesale, so each patched row's yml
|
||||
* Compose the patch set from profile json, CLI flags, and the resolved
|
||||
* frontend dist. Patches replace a row's config wholesale, so each patched row's yml
|
||||
* static values are re-read here (bypass parse) and merged under the overrides.
|
||||
*/
|
||||
private composePatches(): void {
|
||||
@@ -178,12 +208,6 @@ export class AppCLIEntry {
|
||||
overrides.set(entryId, bag)
|
||||
}
|
||||
|
||||
// Source 0: computed engineering defaults. The session store defaults to
|
||||
// a global dir under the Harness home ($DSH_HOME, else ~/.dsh) so history
|
||||
// is shared across every cwd, not a project-local ./.sessions. The profile
|
||||
// (Source 1) overwrites this same field via last-write-wins in put().
|
||||
put('session-persistence-jsonl', 'root', join(resolveDshHome(), 'sessions'))
|
||||
|
||||
// Source 1: profile json (missing file = empty; unmapped key = loud).
|
||||
for (const [key, value] of Object.entries(this.readProfile())) {
|
||||
const mapping = PROFILE_MAPPINGS.find(m => m.jsonPath === key)
|
||||
@@ -214,62 +238,68 @@ export class AppCLIEntry {
|
||||
if (yml === undefined) throw new Error(`dsh: patch target row "${id}" not found in ${this.options.configPath}`)
|
||||
return { id, config: { ...(yml.config ?? {}) as Record<string, unknown>, ...bag } }
|
||||
})
|
||||
|
||||
// Telemetry opt-out: a row can only be turned off at the patch layer
|
||||
// (config cannot disable an entry), and the switch must hold BEFORE the
|
||||
// plugin constructs — its exporter.url validation is load-time fail-loud.
|
||||
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
|
||||
if (telemetryPatch !== undefined) this.patches.push(telemetryPatch)
|
||||
}
|
||||
|
||||
/** Loader include boot; the dev HMR row mounts before await so the fail-loud triple covers it. */
|
||||
/** Shared Loader boot; the dev HMR row mounts before await so the activation audit covers it. */
|
||||
private async bootTree(): Promise<void> {
|
||||
const ctx = new Context()
|
||||
ctx.baseUrl = pathToFileURL(join(resolve(this.options.configPath), '..')).href + '/'
|
||||
await ctx.plugin(Loader)
|
||||
ctx.loader.builtins.include = Include
|
||||
await ctx.loader.create({
|
||||
name: 'cordis:include',
|
||||
config: {
|
||||
path: pathToFileURL(resolve(this.options.configPath)).href,
|
||||
...this.patches.length > 0 ? { patches: this.patches } : {},
|
||||
},
|
||||
// One include of the shared base with every overlay as a sibling patch
|
||||
// list: patches never cross an include boundary, so nesting them would
|
||||
// silently stop reaching base rows. The surface overlay applies first, then
|
||||
// this entry's profile-json and CLI-flag patches, which therefore win.
|
||||
const patches = [
|
||||
...loadOverlayPatches('dsh', this.options.overlayPath),
|
||||
...this.options.extraOverlayPath === undefined
|
||||
? loadPersonalPatches('dsh') ?? []
|
||||
: loadOverlayPatches('dsh', this.options.extraOverlayPath),
|
||||
...this.patches,
|
||||
]
|
||||
this.ctx = await boot('dsh', resolve(this.options.configPath), patches, async (ctx) => {
|
||||
if (this.options.dev) await ctx.loader.create({ name: '@deepseek-ai/dsh-client-hmr' })
|
||||
})
|
||||
if (this.options.dev) {
|
||||
await ctx.loader.create({ name: '@deepseek-ai/dsh-client-hmr' })
|
||||
}
|
||||
this.ctx = ctx
|
||||
await ctx.loader.await()
|
||||
}
|
||||
|
||||
/** Install the diagnostic for plugin rejections that happen after settled boot. */
|
||||
private assertBoot(): void {
|
||||
installFailLoud('dsh')
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail-loud triple: assertEntriesLoaded catches import failures,
|
||||
* installFailLoud catches late apply rejections, and the all-ACTIVE sweep
|
||||
* below catches PENDING fibers (cordis inject waiting has no timeout).
|
||||
* Bypass parse of the base and this surface's overlay (id → row) for
|
||||
* patch-merge inputs; the Loader still reads both files itself. The overlay
|
||||
* wins per row, matching the order its patches are applied in, and its
|
||||
* `insert` rows are indexed too because a flag may target one of them.
|
||||
*/
|
||||
private assertBoot(): void {
|
||||
installFailLoud('dsh')
|
||||
assertEntriesLoaded(this.ctx, 'dsh')
|
||||
const failures: string[] = []
|
||||
for (const entry of this.ctx.loader.entries()) {
|
||||
if (entry.fiber === undefined || entry.disabled) continue
|
||||
const state = entry.fiber.state
|
||||
if (state === FIBER_ACTIVE) continue
|
||||
if (state === FIBER_PENDING) {
|
||||
const missing = Object.keys(entry.fiber.inject).filter(service => this.ctx.get(service) === undefined)
|
||||
failures.push(`${entry.options.name}: pending (waiting for service${missing.length === 1 ? '' : 's'}: ${missing.join(', ') || 'unknown'})`)
|
||||
} else {
|
||||
failures.push(`${entry.options.name}: fiber state ${String(state)}`)
|
||||
private parseYmlRows(): Map<string, { config?: unknown }> {
|
||||
const rows = new Map<string, { config?: unknown }>()
|
||||
const files = [this.options.configPath, this.options.overlayPath]
|
||||
if (this.options.extraOverlayPath !== undefined) files.push(this.options.extraOverlayPath)
|
||||
for (const file of files) {
|
||||
for (const row of this.parseRowList(file)) {
|
||||
if (typeof row.id === 'string') rows.set(row.id, row)
|
||||
for (const inserted of row.insert ?? []) {
|
||||
if (typeof inserted.id === 'string') rows.set(inserted.id, inserted)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`dsh: ${String(failures.length)} entr${failures.length === 1 ? 'y' : 'ies'} did not activate\n${failures.join('\n')}`)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
/** Bypass parse of the shipped yml (id → row) for patch-merge inputs; Loader still reads the file itself. */
|
||||
private parseYmlRows(): Map<string, { config?: unknown }> {
|
||||
const doc = yaml.load(readFileSync(this.options.configPath, 'utf8'), { schema: includeYamlSchema })
|
||||
if (!Array.isArray(doc)) throw new Error(`dsh: ${this.options.configPath} is not a top-level entry list`)
|
||||
const rows = new Map<string, { config?: unknown }>()
|
||||
for (const row of doc as { id?: string; config?: unknown }[]) {
|
||||
if (typeof row.id === 'string') rows.set(row.id, row)
|
||||
}
|
||||
return rows
|
||||
/**
|
||||
* Parse one entry or patch list, rejecting anything that is not a top-level
|
||||
* array so a malformed file fails here rather than at row lookup.
|
||||
* @param file - absolute path of the config or overlay file.
|
||||
* @returns the parsed top-level entries.
|
||||
*/
|
||||
private parseRowList(file: string): { id?: string; config?: unknown; insert?: { id?: string; config?: unknown }[] }[] {
|
||||
const doc = yaml.load(readFileSync(file, 'utf8'), { schema: includeYamlSchema })
|
||||
if (!Array.isArray(doc)) throw new Error(`dsh: ${file} is not a top-level entry list`)
|
||||
return doc as { id?: string; config?: unknown; insert?: { id?: string; config?: unknown }[] }[]
|
||||
}
|
||||
|
||||
/** Profile json under cwd; read-only — never created here, absent = no user config. */
|
||||
|
||||
@@ -2,19 +2,24 @@
|
||||
* 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; `web` is a
|
||||
* real subcommand. Commander owns `--help`/`--version` and parse errors — it
|
||||
* prints and exits at the point of failure (a domain failure routes through
|
||||
* subcommand) is the TUI/headless surface with option-only flags; `meta` and
|
||||
* `web` are real subcommands. Commander owns `--help`/`--version` and parse
|
||||
* errors — it prints and exits at the point of failure (a domain failure routes through
|
||||
* `command.error`), so this returns only a resolved mode.
|
||||
* @module @deepseek-ai/dsh/args
|
||||
*/
|
||||
|
||||
import { Command, CommanderError } from 'commander'
|
||||
|
||||
/** Interactive TUI: the default mode. `--config` swaps the tree; `--resume <id>` rehydrates a session. */
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
@@ -24,18 +29,35 @@ interface HeadlessInvocation {
|
||||
prompt: string
|
||||
}
|
||||
|
||||
/** Interactive fresh TUI over this harness checkout; accepts no default-surface options. */
|
||||
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 — `--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 `cordis.yml` value stands when a flag is absent) and validity (a bad
|
||||
* 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.
|
||||
*/
|
||||
interface WebInvocation {
|
||||
mode: 'web'
|
||||
/** Overlay of loader patches applied over the shipped web composition. */
|
||||
config?: string
|
||||
host?: string
|
||||
port?: number
|
||||
dev: boolean
|
||||
@@ -45,10 +67,16 @@ interface WebInvocation {
|
||||
}
|
||||
|
||||
/** The resolved `dsh` invocation: exactly one mode. `--help`/`--version`/errors exit inside {@link parseDshArgs}. */
|
||||
export type DshInvocation = TuiInvocation | HeadlessInvocation | WebInvocation
|
||||
export type DshInvocation =
|
||||
| TuiInvocation
|
||||
| HeadlessInvocation
|
||||
| MetaInvocation
|
||||
| SkillSessionInvocation
|
||||
| WebInvocation
|
||||
|
||||
/** Raw web-subcommand options straight from Commander. */
|
||||
interface WebOptions {
|
||||
config?: string
|
||||
host?: string
|
||||
port?: string
|
||||
dev?: boolean
|
||||
@@ -65,6 +93,7 @@ interface WebOptions {
|
||||
function resolveWeb(options: WebOptions): WebInvocation {
|
||||
return {
|
||||
mode: 'web',
|
||||
...options.config !== undefined && { config: options.config },
|
||||
...options.host !== undefined && { host: options.host },
|
||||
...options.port !== undefined && { port: Number(options.port) },
|
||||
dev: options.dev === true,
|
||||
@@ -86,21 +115,34 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
|
||||
const program = new Command()
|
||||
.name('dsh')
|
||||
.version(version, '-V, --version', 'output the version number')
|
||||
.description('dsh: interactive TUI (default), headless task, and browser UI')
|
||||
.description('dsh: DeepSeek Harness — an interactive coding agent for your terminal.\nRun `dsh` with no arguments to start a session in the current directory.')
|
||||
// The default surface takes no positional task, so `dsh "task"` fails
|
||||
// commander's arity check with no hint; these examples are where a first
|
||||
// reader learns the entry points and that a one-shot task rides `-p`.
|
||||
.addHelpText('after', `
|
||||
Examples:
|
||||
dsh start an interactive session in this directory
|
||||
dsh -p "run the tests" answer one task, print the result, and exit
|
||||
dsh --resume <id> continue a past session
|
||||
`)
|
||||
.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('--config <path>', 'boot an alternate cordis.yml instead of the shipped tree (TUI mode)')
|
||||
.option('-p, --prompt <task>', 'run one headless turn for this task, print the result, and exit')
|
||||
.option('--resume <id>', 'resume the persisted session with this id (TUI mode)')
|
||||
.action((options: { config?: string; prompt?: string; resume?: string }) => {
|
||||
.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')
|
||||
.action((options: { config?: string; configReplace?: string; prompt?: string; resume?: string }) => {
|
||||
if (options.prompt !== undefined) {
|
||||
// A headless prompt owns the invocation; an empty task has nothing to
|
||||
// run, and --config/--resume are TUI inputs that must not silently
|
||||
// vanish from a headless run.
|
||||
if (options.prompt === '') program.error('error: --prompt needs a task')
|
||||
if (options.config !== undefined || options.resume !== undefined) {
|
||||
program.error('error: --prompt takes no --config or --resume')
|
||||
if (options.config !== undefined || options.configReplace !== undefined || options.resume !== undefined) {
|
||||
program.error('error: --prompt takes no --config, --config-replace, or --resume')
|
||||
}
|
||||
resolved = { mode: 'headless', prompt: options.prompt }
|
||||
return
|
||||
@@ -108,32 +150,67 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
|
||||
// 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 web = program.command('web').description('serve the browser UI (host/port default to the shipped 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.
|
||||
const rejectParentOptions = (command: string): void => {
|
||||
const parent = program.opts<{ config?: string; configReplace?: string; prompt?: string; resume?: string }>()
|
||||
if (parent.config !== undefined || parent.configReplace !== undefined
|
||||
|| parent.prompt !== undefined || parent.resume !== undefined) {
|
||||
program.error(`error: ${command} takes none of --config, --config-replace, -p/--prompt, or --resume`)
|
||||
}
|
||||
}
|
||||
|
||||
// Registration order is the rendered help order, so daily use comes first
|
||||
// and the harness-development surfaces (`web --dev`, `meta`) come last.
|
||||
// `upgrade` is a guided fresh-session entry: 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')
|
||||
.action(() => {
|
||||
rejectParentOptions('upgrade')
|
||||
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('--host <host>', 'override the config bind host (127.0.0.1 or 0.0.0.0)')
|
||||
.option('--port <port>', 'override the config listen port (0 requests an OS-assigned port)')
|
||||
.option('--dev', 'mount the client HMR driver and watch plugin bundles for rebuilds')
|
||||
.option('--workspace-root <path>', 'parent directory for name-created workspaces')
|
||||
.option('--config <path>', 'apply this overlay of loader patches over the shipped configuration')
|
||||
.option('--host <host>', 'bind host; pass 0.0.0.0 to reach it from another machine')
|
||||
.option('--port <port>', 'listen port; pass 0 to let the OS pick a free one')
|
||||
.option('--dev', 'developer mode: hot-reload the browser client')
|
||||
.option('--workspace-root <path>', 'parent directory for workspaces created from the browser UI')
|
||||
.option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
|
||||
.action((options: WebOptions) => {
|
||||
// Commander parses the parent (default-surface) options on either side of
|
||||
// the subcommand into `program.opts()`. `web` shares none of them, so a
|
||||
// leaked `--config`/`-p`/`--resume` is a mistyped invocation that must
|
||||
// fail loud rather than silently start the web server and drop it.
|
||||
const parent = program.opts<{ config?: string; prompt?: string; resume?: string }>()
|
||||
if (parent.config !== undefined || parent.prompt !== undefined || parent.resume !== undefined) {
|
||||
program.error('error: web takes none of --config, -p/--prompt, or --resume')
|
||||
}
|
||||
rejectParentOptions('web')
|
||||
resolved = resolveWeb(options)
|
||||
})
|
||||
|
||||
program
|
||||
.command('meta')
|
||||
.description('work on the dsh source that runs this command, from any directory')
|
||||
.action(() => {
|
||||
rejectParentOptions('meta')
|
||||
resolved = { mode: 'meta' }
|
||||
})
|
||||
|
||||
try {
|
||||
program.parse(argv, { from: 'user' })
|
||||
} catch (error) {
|
||||
|
||||
@@ -30,7 +30,7 @@ const invocation = parseDshArgs(process.argv.slice(2), readVersion())
|
||||
switch (invocation.mode) {
|
||||
case 'web': {
|
||||
const { runWeb } = await import('./web.ts')
|
||||
await runWeb(invocation.host, invocation.port, invocation.dev, invocation.workspaceRoot, invocation.trustedHosts)
|
||||
await runWeb(invocation.host, invocation.port, invocation.dev, invocation.workspaceRoot, invocation.trustedHosts, invocation.config)
|
||||
break
|
||||
}
|
||||
case 'headless': {
|
||||
@@ -40,7 +40,17 @@ switch (invocation.mode) {
|
||||
}
|
||||
case 'tui': {
|
||||
const { runTui } = await import('./tui.ts')
|
||||
await runTui(invocation.config, invocation.resume)
|
||||
await runTui(invocation.config, invocation.resume, undefined, undefined, invocation.configReplace)
|
||||
break
|
||||
}
|
||||
case 'meta': {
|
||||
const { runMeta } = await import('./tui.ts')
|
||||
await runMeta()
|
||||
break
|
||||
}
|
||||
case 'upgrade': {
|
||||
const { runSkillSession } = await import('./tui.ts')
|
||||
await runSkillSession(`dsh-${invocation.mode}`)
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* `dsh -p "task"` — headless over the one shared composition: AppCLIEntry
|
||||
* boots the same cordis.yml as `dsh web` (port 0, so parallel runs never
|
||||
* boots the same base plus Web overlay as `dsh web` (port 0, so parallel runs never
|
||||
* collide), then in-process isomorphic injection (InProcessApiClient over
|
||||
* toFetchHandler(ctx.apiProxy), so the full carrier chain — wire
|
||||
* serialization, zod, SSE framing — really runs). The printed URL opens the
|
||||
@@ -75,12 +75,24 @@ async function consumeUntilTurnEnd(frames: AsyncIterable<RpcRequest<MuxFrame>>,
|
||||
export async function runHeadless(task: string): Promise<void> {
|
||||
// A missing DEEPSEEK_API_KEY throws here (plugin load is fail-loud, uncaught by design).
|
||||
const entry = new AppCLIEntry({
|
||||
configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
|
||||
configPath: fileURLToPath(new URL('../config/base.cordis.yml', import.meta.url)),
|
||||
overlayPath: fileURLToPath(new URL('../config/web.cordis.yml', import.meta.url)),
|
||||
dev: false,
|
||||
port: 0,
|
||||
})
|
||||
const { ctx, port } = await entry.run()
|
||||
const dispose = async (): Promise<void> => { await ctx.fiber.dispose() }
|
||||
// Signal exits must still dispose the tree: the composition mounts
|
||||
// exit-drained plugins (telemetry's queued tail and shutdown marker would
|
||||
// otherwise be lost), and Node's default signal exit skips disposal.
|
||||
let signalled = false
|
||||
const disposeAndExit = (code: number): void => {
|
||||
if (signalled) return
|
||||
signalled = true
|
||||
void dispose().finally(() => { process.exit(code) })
|
||||
}
|
||||
process.on('SIGTERM', () => { disposeAndExit(143) })
|
||||
process.on('SIGINT', () => { disposeAndExit(130) })
|
||||
// The headless session is web-observable while it runs (same composition).
|
||||
process.stderr.write(`dsh: observing at http://127.0.0.1:${String(port)}\n`)
|
||||
const api = new InProcessApiClient(toFetchHandler(ctx.apiProxy))
|
||||
|
||||
@@ -1,40 +1,62 @@
|
||||
/**
|
||||
* `dsh` default surface — the interactive TUI coding agent. Boots the shipped
|
||||
* tui-agent config (or the `--config` override) with the personal overlay
|
||||
* 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: sessions, relative paths, and workspace instructions resolve from
|
||||
* the cwd, so `dsh` acts on whatever project it is launched in. After boot, the
|
||||
* agent's system prompt is told the path to this harness checkout so it can find
|
||||
* its own source.
|
||||
* 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`
|
||||
* ({@link runMeta}) is the one exception — it makes this harness checkout the
|
||||
* workspace. `dsh upgrade` ({@link runSkillSession}) 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 { join } from 'node:path'
|
||||
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,
|
||||
loadEnv,
|
||||
loadOverlayPatches,
|
||||
loadPersonalPatches,
|
||||
RESUME_SESSION_ID_KEY,
|
||||
resolveConfigPath,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
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'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
// Both the source tree (apps/cli/src) and the bundled bin (apps/cli/lib) sit
|
||||
// one directory under apps/cli, so the shipped default config resolves with
|
||||
// the same relative hop from either artifact.
|
||||
const DEFAULT_CONFIG = fileURLToPath(new URL('../../../examples/tui-agent/cordis.yml', import.meta.url))
|
||||
// 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
|
||||
@@ -42,17 +64,55 @@ const DEFAULT_CONFIG = fileURLToPath(new URL('../../../examples/tui-agent/cordis
|
||||
const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
|
||||
/* v8 ignore start -- composition over the unit-tested dsh-app-boot helpers;
|
||||
the tui-agent PTY smoke drives this path end to end, personal overlay included */
|
||||
the CLI PTY smoke drives this path end to end, personal overlay included */
|
||||
/**
|
||||
* Run the interactive TUI with this harness checkout as the workspace
|
||||
* (`dsh meta`), whatever directory it was launched from.
|
||||
*/
|
||||
export async function runMeta(): Promise<void> {
|
||||
return runTui(undefined, undefined, SOURCE_ROOT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the interactive TUI as a guided fresh session whose first turn invokes a
|
||||
* bundled skill (`dsh upgrade` → `dsh-upgrade`).
|
||||
* Always mints a fresh session in the invoking directory; the skill is seeded
|
||||
* only on this first launch, so a later `--resume` of the session is an ordinary
|
||||
* TUI session with no re-injection.
|
||||
* @param skill - the bundled skill name to auto-invoke as the first turn.
|
||||
*/
|
||||
export async function runSkillSession(skill: string): Promise<void> {
|
||||
return runTui(undefined, undefined, undefined, skill)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the interactive TUI from the invoking directory.
|
||||
* @param config - a config path to boot instead of the shipped default, or
|
||||
* `undefined` for the default; already parsed from `--config`.
|
||||
* @param resumeSessionId - a persisted session id to resume, or `undefined`;
|
||||
* already parsed and non-empty-validated from `--resume`. It is provided on the
|
||||
* boot context under {@link RESUME_SESSION_ID_KEY}, which the shipped config
|
||||
* reads through `!!js` to rehydrate that session.
|
||||
* @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 {@link runSkillSession} 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): Promise<void> {
|
||||
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.
|
||||
@@ -63,31 +123,56 @@ export async function runTui(config: string | undefined, resumeSessionId: string
|
||||
process.exit(1)
|
||||
}
|
||||
installFailLoud(NAME)
|
||||
// The bin already loaded the invoking directory's .env; the personal .env
|
||||
// only fills what is still unset (process.loadEnvFile never overrides).
|
||||
loadEnv(NAME, resolveDshHome())
|
||||
// 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)
|
||||
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 host
|
||||
// is offered only when Node exposes `process.execve` and knows its own entry.
|
||||
// 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 } = {}
|
||||
const resumeCommand = (sessionId: string): string =>
|
||||
`${NAME} --resume=${sessionId}${config === undefined ? '' : ` --config ${config}`}`
|
||||
// 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`)
|
||||
// Rebuild argv from the parsed config plus the selected id: TUI mode's
|
||||
// only arguments are `--config <path>` and `--resume <id>`.
|
||||
const nextArgv = [
|
||||
process.execPath,
|
||||
...process.execArgv,
|
||||
entry,
|
||||
`--resume=${sessionId}`,
|
||||
...config !== undefined ? ['--config', config] : [],
|
||||
...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) {
|
||||
@@ -103,18 +188,65 @@ export async function runTui(config: string | undefined, resumeSessionId: string
|
||||
}
|
||||
},
|
||||
}
|
||||
// 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 patches = [
|
||||
...replaceTree ? [] : [
|
||||
...loadOverlayPatches(NAME, TUI_OVERLAY),
|
||||
...resolvedConfig === undefined
|
||||
? loadPersonalPatches(NAME) ?? []
|
||||
: loadOverlayPatches(NAME, resolveConfigPath(resolvedConfig, undefined)),
|
||||
],
|
||||
...telemetryPatch === undefined ? [] : [telemetryPatch],
|
||||
]
|
||||
const queryIndexPath = join(tmpdir(), SESSION_QUERY_DB)
|
||||
const ctx = await boot(
|
||||
NAME,
|
||||
resolveConfigPath(config ?? DEFAULT_CONFIG, undefined),
|
||||
loadPersonalPatches(NAME),
|
||||
bootConfig,
|
||||
patches,
|
||||
(hostCtx) => {
|
||||
// Inject the resume id (or undefined) so the shipped config's `!!js`
|
||||
// reads it as a bare identifier; then offer the in-place handoff host.
|
||||
hostCtx.provide(RESUME_SESSION_ID_KEY, resumeSessionId)
|
||||
if (resumeSessionId !== undefined) {
|
||||
hostCtx.provide(TUI_GOODBYE_MESSAGE_KEY, `To resume this session: ${resumeCommand(resumeSessionId)}`)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
},
|
||||
)
|
||||
app.current = ctx
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
/**
|
||||
* `dsh web` — thin bin over the config-tree boot: run AppCLIEntry with the
|
||||
* already-parsed host/port/dev, print the URL line, wire signals. All
|
||||
* composition lives in cordis.yml; all boot glue lives in AppCLIEntry. Host and
|
||||
* composition lives in the shared base plus Web overlay; all boot glue lives in AppCLIEntry. Host and
|
||||
* port are unvalidated pass-through overrides — the `dsh-host-webserver` schema
|
||||
* gates them at boot.
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { AppCLIEntry } from './app-cli-entry.ts'
|
||||
|
||||
const CONFIG_PATH = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
// The shared core every `dsh` surface mounts, plus this surface's overlay over it.
|
||||
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))
|
||||
|
||||
// Display-only mirror of the webserver schema's loopback host: the address the
|
||||
// local URL always prints. Not a source of truth — the schema is.
|
||||
@@ -17,12 +20,15 @@ const LOOPBACK_HOST = '127.0.0.1'
|
||||
|
||||
/**
|
||||
* Serve the browser UI from the shipped config tree. `host`/`port` are passed
|
||||
* through only when the flag was given; absent, the `cordis.yml` value stands.
|
||||
* through only when the flag was given; absent, the shipped Web overlay value stands.
|
||||
* @param host - the bind host, or `undefined` to keep the config default.
|
||||
* @param port - the listen port (`0` requests an OS-assigned port), or `undefined` to keep the config default.
|
||||
* @param dev - mount the client HMR driver and watch plugin bundles for rebuilds.
|
||||
* @param workspaceRoot - parent directory for name-created workspaces, or `undefined` for the gateway's cwd fallback.
|
||||
* @param trustedHosts - extra authorities for the /api browser-trust fence, or `undefined` for the derived LAN literals alone.
|
||||
* @param config - an overlay of loader patches applied over the shipped web
|
||||
* composition instead of `$DSH_HOME/config.yaml`, or `undefined` to use the
|
||||
* personal overlay; already parsed from `--config`.
|
||||
*/
|
||||
export async function runWeb(
|
||||
host: string | undefined,
|
||||
@@ -30,9 +36,12 @@ export async function runWeb(
|
||||
dev: boolean,
|
||||
workspaceRoot: string | undefined,
|
||||
trustedHosts: string[] | undefined,
|
||||
config?: string,
|
||||
): Promise<void> {
|
||||
const entry = new AppCLIEntry({
|
||||
configPath: CONFIG_PATH,
|
||||
configPath: BASE_CONFIG,
|
||||
overlayPath: WEB_OVERLAY,
|
||||
...config !== undefined && { extraOverlayPath: resolveConfigPath(config, undefined) },
|
||||
dev,
|
||||
...host !== undefined && { host },
|
||||
...port !== undefined && { port },
|
||||
@@ -48,12 +57,14 @@ export async function runWeb(
|
||||
void Promise.resolve(ctx.fiber.dispose()).finally(() => { process.exit(code) })
|
||||
}
|
||||
|
||||
// Install shutdown handling before publishing readiness: supervisors may
|
||||
// send a signal as soon as they observe the URL line.
|
||||
process.on('SIGTERM', () => { shutdown(0) })
|
||||
process.on('SIGINT', () => { shutdown(130) })
|
||||
|
||||
// The entry's boot-time snapshot, not a fresh sample: the printed LAN URL
|
||||
// must name an address the /api trust fence was configured with.
|
||||
const lanCandidate = entry.lanAddresses[0]
|
||||
const localUrl = `http://${LOOPBACK_HOST}:${boundPort}`
|
||||
console.log(`dsh web: ${localUrl}${lanCandidate === undefined ? '' : ` (LAN: http://${lanCandidate}:${boundPort})`}`)
|
||||
|
||||
process.on('SIGTERM', () => { shutdown(0) })
|
||||
process.on('SIGINT', () => { shutdown(130) })
|
||||
}
|
||||
|
||||
@@ -24,17 +24,22 @@ function exitCode(argv: string[]): number {
|
||||
afterEach(() => { vi.restoreAllMocks() })
|
||||
|
||||
describe('parseDshArgs', () => {
|
||||
it('routes each mode by its shape: default TUI, -p headless, web subcommand', () => {
|
||||
it('routes each mode by its shape: default TUI, -p headless, meta 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' })
|
||||
expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
|
||||
// Bare `web` carries no host/port: the shipped cordis.yml owns the default.
|
||||
expect(parse(['meta'])).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'])).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'] })
|
||||
@@ -47,6 +52,8 @@ describe('parseDshArgs', () => {
|
||||
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)
|
||||
@@ -55,6 +62,20 @@ describe('parseDshArgs', () => {
|
||||
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', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1)
|
||||
expect(exitCode(['meta', '-p', 'task'])).toBe(1)
|
||||
// `upgrade` takes no options: any leaked default-surface flag is a
|
||||
// mistyped invocation, not a silently-dropped input.
|
||||
expect(exitCode(['upgrade', '--resume', 's'])).toBe(1)
|
||||
expect(exitCode(['upgrade', '--config', 'c.yml'])).toBe(1)
|
||||
expect(exitCode(['-p', 'task', 'upgrade'])).toBe(1)
|
||||
})
|
||||
|
||||
it('exits 0 for --help (disclosing web) and --version', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest'
|
||||
* 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
|
||||
* examples/tui-agent. Skips before the bin is built.
|
||||
* apps/cli/tests. Skips before the bin is built.
|
||||
*/
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
|
||||
171
apps/cli/tests/fixtures/tui-scripted-llm.ts
vendored
Normal file
171
apps/cli/tests/fixtures/tui-scripted-llm.ts
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
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 answered = blocks.some(block => block.type === 'tool-result' && block.toolCallId === BASH_FAILURE_CALL_ID)
|
||||
for (const chunk of textChunks(answered ? BASH_FAILURE_TEXT : 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?',
|
||||
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
Normal file
71
apps/cli/tests/fixtures/tui-scripted.cordis.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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
|
||||
112
apps/cli/tests/lazy-search-startup.compat.spec.ts
Normal file
112
apps/cli/tests/lazy-search-startup.compat.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Node 22 startup-output smoke for the shipped Web CLI composition.
|
||||
*
|
||||
* Only the dedicated Node compatibility gate opts this test in after building
|
||||
* both artifacts; ordinary Vitest inventory deterministically skips it.
|
||||
* The child runs built artifacts under plain Node with the real shipped
|
||||
* config (base.cordis.yml + the web.cordis.yml overlay).
|
||||
* Its URL line follows AppCLIEntry's settled boot; SIGTERM then exercises the
|
||||
* shipped quiescent disposer.
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import yaml from 'js-yaml'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const builtBin = join(repoRoot, 'apps/cli/lib/bin.js')
|
||||
const webDist = join(repoRoot, 'apps/web/dist/index.html')
|
||||
// The web overlay owns the session-query-sqlite lazy-open patch row.
|
||||
const configPath = join(repoRoot, 'apps/cli/config/web.cordis.yml')
|
||||
const requireBuiltArtifacts = process.env.DSH_REQUIRE_BUILT_CLI_SMOKE === '1'
|
||||
|
||||
interface ConfigRow {
|
||||
id?: string
|
||||
config?: { openAt?: unknown }
|
||||
}
|
||||
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
construct: value => String(value),
|
||||
})
|
||||
const configSchema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||
|
||||
/** Boot the built Web CLI, wait for its settled URL, then dispose through SIGTERM. */
|
||||
function runBuiltWeb(cwd: string): Promise<{ stdout: string; stderr: string; code: number }> {
|
||||
return new Promise((resolveRun, rejectRun) => {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
DEEPSEEK_API_KEY: 'dsh-cli-smoke-dummy-key',
|
||||
DSH_HOME: join(cwd, '.dsh'),
|
||||
}
|
||||
delete env.DEEPSEEK_BASE_URL
|
||||
delete env.NODE_OPTIONS
|
||||
delete env.NODE_NO_WARNINGS
|
||||
const child = spawn(process.execPath, [
|
||||
builtBin,
|
||||
'web',
|
||||
'--host',
|
||||
'127.0.0.1',
|
||||
'--port',
|
||||
'0',
|
||||
], {
|
||||
cwd,
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
let settled = false
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stderr.setEncoding('utf8')
|
||||
child.stdout.on('data', (chunk: string) => {
|
||||
stdout += chunk
|
||||
if (!settled && /dsh web: http:\/\/127\.0\.0\.1:\d+/u.test(stdout)) {
|
||||
settled = true
|
||||
child.kill('SIGTERM')
|
||||
}
|
||||
})
|
||||
child.stderr.on('data', (chunk: string) => { stderr += chunk })
|
||||
const timer = setTimeout(() => {
|
||||
child.kill('SIGKILL')
|
||||
rejectRun(new Error(`built Web CLI did not settle and dispose within 60s\nstdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, 60_000)
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(timer)
|
||||
rejectRun(error)
|
||||
})
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(timer)
|
||||
if (!settled) {
|
||||
rejectRun(new Error(`built Web CLI exited before settled startup (code ${String(code)})\nstdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
return
|
||||
}
|
||||
resolveRun({ stdout, stderr, code: code ?? -1 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe.skipIf(!requireBuiltArtifacts)('built CLI lazy-search startup', () => {
|
||||
it('boots and disposes the shipped composition without a SQLite startup warning', async () => {
|
||||
expect(existsSync(builtBin), `missing built CLI ${resolve(builtBin)}; run pnpm build`).toBe(true)
|
||||
expect(existsSync(webDist), `missing Web dist ${resolve(webDist)}; run pnpm run build:web`).toBe(true)
|
||||
const rows = yaml.load(await readFile(configPath, 'utf8'), { schema: configSchema }) as ConfigRow[]
|
||||
const searchRow = rows.find(row => row.id === 'session-query-sqlite')
|
||||
expect(searchRow?.config?.openAt).toBe('first-search')
|
||||
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'dsh-cli-lazy-search-'))
|
||||
try {
|
||||
const result = await runBuiltWeb(cwd)
|
||||
expect(result.stdout).toMatch(/dsh web: http:\/\/127\.0\.0\.1:\d+/u)
|
||||
expect(result.code).toBe(0)
|
||||
expect(result.stderr).not.toMatch(/ExperimentalWarning: SQLite/u)
|
||||
} finally {
|
||||
await rm(cwd, { recursive: true, force: true })
|
||||
}
|
||||
}, 70_000)
|
||||
})
|
||||
236
apps/cli/tests/pty-harness.ts
Normal file
236
apps/cli/tests/pty-harness.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
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, json, os, pty, select, signal, sys, time
|
||||
node, launch_args_json, launch_env_json, cwd, actions_json, expected_exit, timeout_seconds = sys.argv[1:]
|
||||
env = os.environ.copy()
|
||||
env.update(json.loads(launch_env_json))
|
||||
env.update({"COLUMNS": "100", "LINES": "30"})
|
||||
# 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)
|
||||
|
||||
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) and actions[action_index]["waitFor"].encode() in output:
|
||||
action = actions[action_index]
|
||||
if "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 send: string }
|
||||
| {
|
||||
readonly waitFor: string
|
||||
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
|
||||
/** 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),
|
||||
], {
|
||||
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: 100,
|
||||
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: '100',
|
||||
LINES: '30',
|
||||
}),
|
||||
})
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true
|
||||
terminal.kill()
|
||||
}, timeoutMs)
|
||||
terminal.onData((chunk) => {
|
||||
output += chunk
|
||||
while (actionIndex < actions.length && output.includes(actions[actionIndex]!.waitFor)) {
|
||||
const action = actions[actionIndex]!
|
||||
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 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 {
|
||||
await rm(cwd, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
30
apps/cli/tests/snapshots/bash-terminal-card/session.jsonl
Normal file
30
apps/cli/tests/snapshots/bash-terminal-card/session.jsonl
Normal file
@@ -0,0 +1,30 @@
|
||||
{"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"}}}
|
||||
@@ -0,0 +1,55 @@
|
||||
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>
|
||||
@@ -0,0 +1,32 @@
|
||||
{"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"}}}
|
||||
@@ -0,0 +1,59 @@
|
||||
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>
|
||||
34
apps/cli/tests/snapshots/code-mode/session.jsonl
Normal file
34
apps/cli/tests/snapshots/code-mode/session.jsonl
Normal file
@@ -0,0 +1,34 @@
|
||||
{"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"}}}
|
||||
143
apps/cli/tests/snapshots/code-mode/terminal.expected.txt
Normal file
143
apps/cli/tests/snapshots/code-mode/terminal.expected.txt
Normal file
@@ -0,0 +1,143 @@
|
||||
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
|
||||
@@ -0,0 +1,13 @@
|
||||
{"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"}}}
|
||||
@@ -0,0 +1,13 @@
|
||||
{"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"}}}
|
||||
@@ -0,0 +1,64 @@
|
||||
{"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"}}}
|
||||
@@ -0,0 +1,110 @@
|
||||
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
|
||||
16
apps/cli/tests/snapshots/dynamic-workflow/session.1.jsonl
Normal file
16
apps/cli/tests/snapshots/dynamic-workflow/session.1.jsonl
Normal file
@@ -0,0 +1,16 @@
|
||||
{"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"}}}
|
||||
29
apps/cli/tests/snapshots/dynamic-workflow/session.jsonl
Normal file
29
apps/cli/tests/snapshots/dynamic-workflow/session.jsonl
Normal file
@@ -0,0 +1,29 @@
|
||||
{"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"}}}
|
||||
@@ -0,0 +1,86 @@
|
||||
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
|
||||
@@ -0,0 +1,31 @@
|
||||
{"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"}}}
|
||||
@@ -0,0 +1,62 @@
|
||||
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>
|
||||
28
apps/cli/tests/snapshots/parallel-file-reads/session.jsonl
Normal file
28
apps/cli/tests/snapshots/parallel-file-reads/session.jsonl
Normal file
@@ -0,0 +1,28 @@
|
||||
{"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"}}}
|
||||
@@ -0,0 +1,58 @@
|
||||
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>
|
||||
@@ -0,0 +1 @@
|
||||
alpha
|
||||
@@ -0,0 +1 @@
|
||||
beta
|
||||
@@ -0,0 +1,5 @@
|
||||
{"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"}}}}
|
||||
@@ -0,0 +1,157 @@
|
||||
=== 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>
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: model-only-skill
|
||||
description: Model-only assembled snapshot skill.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
MODEL-ONLY BODY MUST NOT LOAD
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: user-only-skill
|
||||
description: User-only assembled snapshot skill.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
USER-ONLY BODY
|
||||
31
apps/cli/tests/snapshots/todo-plan/session.jsonl
Normal file
31
apps/cli/tests/snapshots/todo-plan/session.jsonl
Normal file
@@ -0,0 +1,31 @@
|
||||
{"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"}}}
|
||||
65
apps/cli/tests/snapshots/todo-plan/terminal.expected.txt
Normal file
65
apps/cli/tests/snapshots/todo-plan/terminal.expected.txt
Normal file
@@ -0,0 +1,65 @@
|
||||
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>
|
||||
23
apps/cli/tests/telemetry-switch.spec.ts
Normal file
23
apps/cli/tests/telemetry-switch.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveTelemetryPatch } from '../src/app-cli-entry.ts'
|
||||
|
||||
describe('resolveTelemetryPatch', () => {
|
||||
it('keeps telemetry enabled when the switch is unset or empty', () => {
|
||||
expect(resolveTelemetryPatch(undefined, true)).toBeUndefined()
|
||||
expect(resolveTelemetryPatch('', true)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('disables on ANY non-empty value, including falsy-looking ones', () => {
|
||||
for (const value of ['1', '0', 'false', 'no']) {
|
||||
expect(resolveTelemetryPatch(value, true)).toEqual({ id: 'telemetry-otel', disabled: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('fails loud when the switch is set but the row is absent', () => {
|
||||
expect(() => resolveTelemetryPatch('1', false)).toThrow('DSH_TELEMETRY_DISABLED is set but row "telemetry-otel" is not in this composition')
|
||||
})
|
||||
|
||||
it('ignores a missing row while the switch is unset', () => {
|
||||
expect(resolveTelemetryPatch(undefined, false)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
518
apps/cli/tests/tui-keyless-smoke.e2e.ts
Normal file
518
apps/cli/tests/tui-keyless-smoke.e2e.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
import { createUserMessage, createMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
||||
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 { 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'
|
||||
|
||||
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))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 }): Promise<string> {
|
||||
return runTuiPtySmoke({
|
||||
tempDirPrefix: 'dsh-tui-smoke-',
|
||||
binScript: dshBinScript,
|
||||
tsconfigPath,
|
||||
// Telemetry now mounts in the shared base: keep fixture sessions from
|
||||
// POSTing to the production endpoint when run outside CI's workflow env.
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call', DSH_TELEMETRY_DISABLED: '1' },
|
||||
// Artifact CI builds and smokes concurrently on a contended runner.
|
||||
...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}),
|
||||
...overrides,
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
|
||||
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')
|
||||
}, 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 • Enter submit • Esc interrupt', send: '\r' },
|
||||
{ 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('\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')
|
||||
}, LOADER_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')
|
||||
}, LOADER_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')
|
||||
}, LOADER_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')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
})
|
||||
|
||||
describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
|
||||
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')
|
||||
}, LOADER_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')
|
||||
}, LOADER_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')
|
||||
}, 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')
|
||||
}, LOADER_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:')
|
||||
}, LOADER_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/)
|
||||
}, LOADER_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}/)
|
||||
}, LOADER_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]')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('tells the model its source path 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 is a system-prompt section; the 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: '\r' },
|
||||
{ waitFor: 'Decision received. Scripted TUI run complete.', send: '/exit\r' },
|
||||
],
|
||||
inspect: async (cwd) => { context = await readLoggedRequestContext(cwd) },
|
||||
})
|
||||
expect(context.system).toContain(`Your own source code is the checkout at ${sourceRoot}; you can read it there to learn how dsh works and how to extend it.`)
|
||||
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.')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
536
apps/cli/tests/tui.snapshot.ts
Normal file
536
apps/cli/tests/tui.snapshot.ts
Normal file
@@ -0,0 +1,536 @@
|
||||
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 { 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 LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
|
||||
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
|
||||
composition: Composition
|
||||
expectedTools: string[]
|
||||
expectedEventCounts?: Record<string, number>
|
||||
childSessions?: number
|
||||
enterPlanMode?: boolean
|
||||
leavePlanModeAfterFirstTurn?: boolean
|
||||
recorded: boolean
|
||||
seedWorkspace?: 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
|
||||
}
|
||||
|
||||
const SCENARIOS: Scenario[] = [
|
||||
{
|
||||
name: 'multi-turn-conversation',
|
||||
composition: 'native',
|
||||
expectedTools: [],
|
||||
expectedEventCounts: { 'plan/mode': 2 },
|
||||
enterPlanMode: true,
|
||||
leavePlanModeAfterFirstTurn: true,
|
||||
recorded: 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: '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>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function childFixturePaths(scenario: Scenario): string[] {
|
||||
return Array.from(
|
||||
{ length: scenario.childSessions ?? 0 },
|
||||
(_, index) => join(scenarioDir(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')
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
async function mountScenarioContext(
|
||||
scenario: Scenario,
|
||||
cwd: string,
|
||||
displayCwd: string,
|
||||
fixtureFile: string,
|
||||
childFiles: string[],
|
||||
): 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') } },
|
||||
})
|
||||
await ctx.plugin(TokenMeterService)
|
||||
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.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 {
|
||||
installLlmReplay(ctx, { file: fixtureFile, childFiles, providers: PROVIDERS })
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
interface ScenarioResult {
|
||||
terminal: string
|
||||
parent: Session
|
||||
children: Session[]
|
||||
workflowEvents: string[]
|
||||
}
|
||||
|
||||
async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
|
||||
const clock = vi.spyOn(Date, 'now').mockReturnValue(new Date(2026, 6, 21, 12, 0, 0).getTime())
|
||||
const dir = scenarioDir(scenario)
|
||||
const fixtureFile = join(dir, '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 ctx: Context | undefined
|
||||
let controller: ReturnType<typeof createTuiChat> | undefined
|
||||
const terminal = new HeadlessTerminal(100, 36)
|
||||
try {
|
||||
if (scenario.seedWorkspace === true) {
|
||||
const source = join(scenarioDir(scenario), 'workspace')
|
||||
await cp(source, cwd, { recursive: true })
|
||||
}
|
||||
ctx = await mountScenarioContext(scenario, cwd, displayCwd, fixtureFile, childFiles)
|
||||
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
|
||||
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
|
||||
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) {
|
||||
terminal.send(prompt)
|
||||
terminal.send('\r')
|
||||
await agent.whenIdle()
|
||||
await settleTerminal(terminal)
|
||||
}
|
||||
|
||||
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)
|
||||
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.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 })
|
||||
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 () => {
|
||||
expect([...observedScenarios].sort()).toEqual(SCENARIOS.map(scenario => scenario.name).sort())
|
||||
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 = [
|
||||
'session.jsonl',
|
||||
'terminal.expected.txt',
|
||||
...scenario.seedWorkspace === true ? ['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(scenarioDir(scenario), fixture), 'utf8')
|
||||
expect(scrubRequestHeaders(content), `${scenario.name}/${fixture} carries request-header bulk`).toBe(content)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -29,6 +29,9 @@
|
||||
{
|
||||
"path": "../../packages/util/paths"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/session-query/session-query-sqlite"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/client/connection"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>DeepSeek Harness</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
3
apps/web/public/favicon.svg
Normal file
3
apps/web/public/favicon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50.000000" height="50.000000" viewBox="0 0 50 50" fill="none">
|
||||
<path id="path" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#000" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
181
apps/web/tests/approval-composer.e2e.ts
Normal file
181
apps/web/tests/approval-composer.e2e.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
// Web e2e scenario: the composer-takeover approval panel under a long
|
||||
// command. The shipped composition confines bash through the sandbox policy
|
||||
// and routes its escalation through the approval seam, so a read-only session
|
||||
// asked to write a file produces a REAL pending approval — the panel renders
|
||||
// in the browser, the test measures its geometry, answers through it, and the
|
||||
// escalated command then runs. Replay is deterministic: the denial, the
|
||||
// escalation retry and its command text arrive from replayed chunks, and the
|
||||
// answer click is the test's own gesture (the same sanctioned reaction to
|
||||
// model content as the question composer: the turn cannot complete without it).
|
||||
//
|
||||
// Geometry is the point of the scenario. The command is unbounded model text,
|
||||
// and before the cap a long one grew the card until the refuse/allow buttons
|
||||
// left the viewport — an approval the user could see and not answer.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
// Empty type import: carries the approval package's session-event merge, so
|
||||
// the decided-outcome assertion below type-checks against the real union.
|
||||
import type {} from '@deepseek-ai/dsh-user-approval'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/approval-composer', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
// The scenario's one golden: the waiting panel. Everything the answered state
|
||||
// proves is asserted directly — see the world-state block at the end.
|
||||
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
// Irreducible payload: the command has to be long enough to pass the card's
|
||||
// height cap, which is the only shape that reproduces an action row pushed off
|
||||
// screen. Unrelated tokens, not a repeated word — a repeated word is what the
|
||||
// model compressed into `printf 'alpha %.0s' {1..400}` while recording, and a
|
||||
// short command proves nothing here. The formula keeps the source small; the
|
||||
// model receives the expanded literal it has to put in the command.
|
||||
const TOKENS = Array.from({ length: 220 }, (_, index) => `tok${((index + 1) * 7919 % 99991).toString(36)}`).join(' ')
|
||||
const PROMPT = `Write a file named notes.txt in the workspace containing exactly this text on one line: ${TOKENS}. Use one bash command with the literal text inline. Then reply with the single word DONE and stop.`
|
||||
|
||||
/** Draft used to measure the composer's own text cap: enough lines to pass it. */
|
||||
const CAP_PROBE = Array.from({ length: 40 }, (_, index) => `line ${index}`).join('\n')
|
||||
|
||||
describe('web e2e: approval takeover keeps its actions reachable', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 })
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('caps the long command, answers through the panel, and runs the escalated command', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-approval'))
|
||||
if (MODE !== 'record') {
|
||||
expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
|
||||
}
|
||||
const input = page.locator('textarea').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
|
||||
// The composer's own text cap, measured on the live textarea before the
|
||||
// takeover replaces it. The panel's scroll region must stop at the same
|
||||
// height (the designer's requirement: one cap for the composer seat), and
|
||||
// measuring it here keeps the assertion free of the px value itself.
|
||||
await input.fill(CAP_PROBE)
|
||||
const composerCap = await input.evaluate(el => el.clientHeight)
|
||||
expect(composerCap).toBeGreaterThan(0)
|
||||
await input.fill('')
|
||||
|
||||
// Read-only: the mode whose denial the model escalates from. Switched
|
||||
// through the shipped access-mode chip, not a test-only seam.
|
||||
await page.locator('[aria-label^="Access mode"]').click()
|
||||
await page.getByRole('menuitem', { name: 'Read Only' }).click()
|
||||
await expect.poll(
|
||||
() => page.locator('[aria-label="Access mode, current: Read Only"]').count(),
|
||||
{ timeout: 15_000 },
|
||||
).toBe(1)
|
||||
|
||||
const settled = scaffold.whenTurnSettled(MODE === 'record' ? 240_000 : 60_000)
|
||||
await input.fill(PROMPT)
|
||||
await input.press('Enter')
|
||||
|
||||
// The panel takes over the input area while the tool blocks. Its presence
|
||||
// is a STABLE waiting state (it stays until answered), so waitFor is
|
||||
// race-free.
|
||||
const panel = page.locator('[data-approval-key]')
|
||||
await panel.waitFor({ timeout: MODE === 'record' ? 180_000 : 60_000 })
|
||||
const scroll = panel.locator('[data-approval-scroll]')
|
||||
await expect.poll(() => scroll.getByText(/tok/).count(), { timeout: 15_000 }).toBeGreaterThan(0)
|
||||
|
||||
if (MODE !== 'record') {
|
||||
// This golden owns the stable waiting surface; the answered golden below
|
||||
// owns the resulting transcript.
|
||||
const snapshot = await captureStableAria(page, '[data-approval-key]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
|
||||
// The regression this scenario exists for: an uncapped card grew with
|
||||
// the command until the action row left the viewport. Measured at the
|
||||
// lane baseline and at a short viewport, on the live panel.
|
||||
const original = page.viewportSize() ?? { width: 1680, height: 1000 }
|
||||
for (const height of [1000, 700]) {
|
||||
await page.setViewportSize({ width: 900, height })
|
||||
const geometry = await panel.evaluate((root) => {
|
||||
const region = root.querySelector<HTMLElement>('[data-approval-scroll]')
|
||||
const card = region?.parentElement ?? null
|
||||
// Role/text, not the CSS-module class names: the built client hashes those.
|
||||
const buttons = [...root.querySelectorAll<HTMLElement>('button')]
|
||||
const rows = buttons.map(button => button.getBoundingClientRect())
|
||||
return {
|
||||
buttons: buttons.length,
|
||||
capped: region === null ? 0 : region.clientHeight,
|
||||
// A scrolling region proves the cap is genuinely engaged; without
|
||||
// it every assertion below would hold vacuously.
|
||||
scrolls: region === null ? false : region.scrollHeight > region.clientHeight,
|
||||
cardBottom: card === null ? Number.NaN : card.getBoundingClientRect().bottom,
|
||||
actionsTop: Math.min(...rows.map(rect => rect.top)),
|
||||
actionsBottom: Math.max(...rows.map(rect => rect.bottom)),
|
||||
viewport: window.innerHeight,
|
||||
}
|
||||
})
|
||||
expect(geometry.buttons).toBe(2)
|
||||
expect(geometry.scrolls).toBe(true)
|
||||
// One cap for the seat: the panel's text region stops where the
|
||||
// composer draft does (sub-pixel tolerance for the shared padding).
|
||||
expect(Math.abs(geometry.capped - composerCap)).toBeLessThan(1)
|
||||
// Both buttons stay inside the card AND inside the viewport — the
|
||||
// answerable state the cap exists to guarantee.
|
||||
expect(geometry.actionsTop).toBeGreaterThan(0)
|
||||
expect(geometry.actionsBottom).toBeLessThanOrEqual(geometry.viewport)
|
||||
expect(geometry.actionsBottom).toBeLessThanOrEqual(geometry.cardBottom)
|
||||
}
|
||||
await page.setViewportSize(original)
|
||||
}
|
||||
|
||||
await panel.getByRole('button', { name: 'Allow once' }).click()
|
||||
|
||||
const sessionId = await settled
|
||||
if (MODE === 'record') {
|
||||
await recordFixture(scaffold, sessionId, FIXTURE)
|
||||
return
|
||||
}
|
||||
// World state: the granted escalation is what let the command run, and the
|
||||
// panel leaves with the regular composer restored. Asserted on the world
|
||||
// and the DOM rather than through a transcript golden — the denied first
|
||||
// attempt renders the OS's own refusal ("Operation not permitted" on
|
||||
// macOS, "Read-only file system" on Linux), so the answered transcript is
|
||||
// not a platform-neutral golden surface.
|
||||
expect(JSON.stringify(sessionEvents.filter(e => e.type === 'approval/decided').at(-1)))
|
||||
.toContain('allowed-once')
|
||||
const written = await readFile(join(scaffold.workspaceCwd, 'workspace', 'notes.txt'), 'utf8')
|
||||
expect(written).toContain(TOKENS.slice(0, 64))
|
||||
await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 20_000 }).toBeGreaterThanOrEqual(1)
|
||||
expect(await page.locator('[data-approval-key]').count()).toBe(0)
|
||||
await expect.poll(() => page.locator('textarea').first().isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 300_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'ui.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -60,6 +60,9 @@ let unmount: (() => void) | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
// English pinned before boot: role/text locators stay deterministic across
|
||||
// localized component migrations (the newEnglishPage e2e convention).
|
||||
localStorage.setItem('dsh.locale', 'en')
|
||||
document.title = 'DeepSeek Harness'
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/code-mode-round/session.jsonl', import.meta.url))
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/code-mode-round/ui.expected.md', import.meta.url))
|
||||
@@ -44,7 +44,7 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
|
||||
})
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -118,14 +118,14 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => {
|
||||
expect(await nest.locator('[data-state="error"]').count()).toBeGreaterThanOrEqual(1)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('a bash sub-row click leaves the default details panel open', async () => {
|
||||
it.skipIf(MODE === 'record')('a bash sub-row click leaves the default details panel closed', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-code-mode-details'))
|
||||
const nest = page.locator('[data-subcalls]').first()
|
||||
const frame = page.locator('[style*="grid-template-columns"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
|
||||
await nest.locator('[data-sample="bash-global"]').first().click()
|
||||
// Tool rows do not drive layout geometry; the Session's default panel stays open.
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
// Tool rows do not drive layout geometry; the Session's default panel stays closed.
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('matches the conversation aria golden with stable anchors', async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/cordis-tool-round/session.jsonl', import.meta.url))
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/cordis-tool-round/ui.expected.md', import.meta.url))
|
||||
@@ -62,7 +62,7 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
|
||||
})
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -107,7 +107,8 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
|
||||
|
||||
const mountRow = page.locator('[data-tool="cordis_mount"]').filter({ hasText: 'Mount temporary Plugin' }).first()
|
||||
await mountRow.waitFor({ timeout: 10_000 })
|
||||
await mountRow.locator('button[aria-expanded]').click()
|
||||
// The whole summary row is the expand toggle (unified tool-row interaction).
|
||||
await mountRow.locator('[aria-expanded]').first().click()
|
||||
await expect.poll(() => mountRow.locator('pre.shiki').textContent(), { timeout: 10_000 })
|
||||
.toContain(MOUNT_CODE)
|
||||
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
// Keyless browser regression for the details column's Session ownership.
|
||||
// The shipped composition retains geometry through unselected states and closes it only when a different Session takes ownership.
|
||||
// Keyless browser regression for the details column's default visibility and Session ownership.
|
||||
// The shipped composition starts closed after selection and reload, retains an explicitly opened width through
|
||||
// unselected states, and closes it only when a different Session takes ownership.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, compareOrRefreshGolden,
|
||||
fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole, webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/details-session-lifecycle', import.meta.url))
|
||||
const HANDLES_EXPECTED = join(SNAPSHOT_DIR, 'handles.expected.md')
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/lifecycle-chrome/session.jsonl', import.meta.url))
|
||||
const SEED_FIXTURE = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.'
|
||||
@@ -23,11 +29,41 @@ async function detailsTrack(page: Page): Promise<number> {
|
||||
})
|
||||
}
|
||||
|
||||
/** First AppFrame grid track in CSS pixels. */
|
||||
async function sidebarTrack(page: Page): Promise<number> {
|
||||
return await appFrame(page).evaluate((element) => {
|
||||
const tracks = getComputedStyle(element).gridTemplateColumns.split(' ')
|
||||
return Number.parseFloat(tracks[0] ?? 'NaN')
|
||||
})
|
||||
}
|
||||
|
||||
/** AppFrame is the only product element with an inline grid track template. */
|
||||
function appFrame(page: Page) {
|
||||
return page.locator('[style*="grid-template-columns"]').first()
|
||||
}
|
||||
|
||||
/** Render the two boundary affordances without platform-dependent coordinates. */
|
||||
async function handleSnapshot(page: Page): Promise<string> {
|
||||
const handles = await page.locator('[class*="handle"]').evaluateAll(elements =>
|
||||
elements.map(element => ({
|
||||
side: element.getAttribute('data-side'),
|
||||
cursor: getComputedStyle(element).cursor,
|
||||
pillGenerated: getComputedStyle(element, '::after').content !== 'none',
|
||||
})))
|
||||
return [
|
||||
'# AppFrame drag handles',
|
||||
'',
|
||||
...handles.flatMap(handle => [
|
||||
`## ${handle.side}`,
|
||||
'',
|
||||
'- hit strip present: true',
|
||||
`- cursor: ${handle.cursor}`,
|
||||
`- pill generated: ${String(handle.pillGenerated)}`,
|
||||
'',
|
||||
]),
|
||||
].join('\n').trimEnd()
|
||||
}
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: details panel follows the current Session lifecycle', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
@@ -40,7 +76,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
scaffold = await launchWebScaffold({ replayFixture: FIXTURE, paceMs: 5 })
|
||||
await seedSession(scaffold, await readFile(SEED_FIXTURE, 'utf8'), 'details-session-lifecycle-seed')
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await appFrame(page).waitFor({ timeout: 30_000 })
|
||||
@@ -52,7 +88,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('retains geometry through hero and closes it for a different Session', async () => {
|
||||
it('starts and reloads closed, then stays closed across Session ownership changes', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-details-session-lifecycle'))
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
const input = page.locator('textarea').first()
|
||||
@@ -61,19 +97,39 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
await settled
|
||||
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(360)
|
||||
expect(await page.getByText('详情', { exact: true }).count()).toBe(1)
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false)
|
||||
await compareOrRefreshGolden(HANDLES_EXPECTED, await handleSnapshot(page), MODE)
|
||||
|
||||
await page.getByRole('button', { name: 'New session', exact: true }).last().click()
|
||||
const sidebarBefore = await sidebarTrack(page)
|
||||
const sidebarHandle = page.locator('[data-side="sidebar"]')
|
||||
const sidebarBox = await sidebarHandle.boundingBox()
|
||||
expect(sidebarBox).not.toBeNull()
|
||||
const dragStartX = sidebarBox!.x + sidebarBox!.width / 2
|
||||
await page.mouse.move(dragStartX, sidebarBox!.y + 200)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(dragStartX + 70, sidebarBox!.y + 200, { steps: 6 })
|
||||
await page.mouse.up()
|
||||
await expect.poll(() => sidebarTrack(page), { timeout: 5_000 }).toBe(sidebarBefore + 70)
|
||||
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await appFrame(page).waitFor({ timeout: 30_000 })
|
||||
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false)
|
||||
|
||||
await page.getByRole('button', { name: /^(?:New session|新.*会话)$/ }).last().click()
|
||||
await page.getByText("Let's start building", { exact: false }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
|
||||
expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false)
|
||||
|
||||
const original = page.locator('[role=treeitem]').filter({ hasText: 'Reply with the single word' }).first()
|
||||
await original.click()
|
||||
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(360)
|
||||
expect(await page.getByText('详情', { exact: true }).count()).toBe(1)
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false)
|
||||
|
||||
const ungrouped = page.getByText('Ungrouped', { exact: true })
|
||||
const ungroupedRow = ungrouped.locator('..').locator('..')
|
||||
@@ -91,5 +147,6 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
|
||||
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['handles.expected.md'])
|
||||
}, 90_000)
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/lifecycle-chrome', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
@@ -43,7 +43,7 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
|
||||
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 })
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/live-interactions', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
@@ -90,7 +90,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
})
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -167,6 +167,9 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
// "no crash, composer recovers, turn logged as error".
|
||||
await expect.poll(() => page.locator('textarea').first().isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
expect(await page.locator('[data-streaming="true"]').count()).toBe(0)
|
||||
// The blank workspace also has an enabled composer. Wait for the driven
|
||||
// session's only visible message before capturing its no-error-copy state.
|
||||
await expect.poll(() => page.getByText(PROMPT, { exact: true }).first().isVisible(), { timeout: 10_000 }).toBe(true)
|
||||
// Golden of the same gap: the prompt bubble alone, no error copy in the
|
||||
// tree — the diff that changes when web-error-surface lands.
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
|
||||
@@ -175,6 +178,29 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps a terminal request marker inside the trajectory table', async () => {
|
||||
await launch(() => ({
|
||||
patches: [{ at: 0, entry: { kind: 'throw', chunks: [], message: 'invalid api key', code: 'AUTH' } }],
|
||||
}))
|
||||
const { settled } = await sendPrompt()
|
||||
await settled
|
||||
await page.getByRole('tab', { name: 'Trajectory' }).click()
|
||||
const tailRequest = page.locator('tr[data-request-only="true"]').last()
|
||||
await tailRequest.waitFor({ timeout: 10_000 })
|
||||
const requestMarker = tailRequest.getByRole('button', { name: /Request #/ })
|
||||
|
||||
const markerWithinTable = await requestMarker.evaluate((element) => {
|
||||
const marker = element.getBoundingClientRect()
|
||||
const table = element.closest('table')?.getBoundingClientRect()
|
||||
if (table === undefined) throw new Error('request marker has no table')
|
||||
return marker.bottom <= table.bottom
|
||||
})
|
||||
|
||||
expect(markerWithinTable).toBe(true)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('recovers a transient SERVER failure through llm-retry and completes', async () => {
|
||||
const derived = deriveReplayScript(parseSessionLog(await readFile(FIXTURE, 'utf8')))
|
||||
expect(derived).toHaveLength(1)
|
||||
|
||||
@@ -8,17 +8,19 @@ import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/message-actions', import.meta.url))
|
||||
// Borrowed read-only: this scenario needs any settled user+assistant pair, not
|
||||
// a new recording (workspace-management / sidebar-scrollbar pattern).
|
||||
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
|
||||
const FORK_EXPECTED = join(SNAPSHOT_DIR, 'fork.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'message-actions-web-e2e'
|
||||
|
||||
@@ -40,7 +42,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
expect(fixtureUserPrompts(raw), 'borrowed seed must carry the drive prompt').toEqual([PROMPT])
|
||||
await seedSession(scaffold, raw, SEED_ID)
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -62,32 +64,81 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 15_000 }).toBe(1)
|
||||
|
||||
// Focus-reveal the footers (hover:hover keeps them opacity-hidden until
|
||||
// hover/focus-within). User has three actions; each finalized assistant
|
||||
// text node has copy + branch.
|
||||
const copyButtons = page.getByRole('button', { name: '复制' })
|
||||
// hover/focus-within). User has three actions; each turn's last content
|
||||
// assistant has copy + branch.
|
||||
const copyButtons = page.getByRole('button', { name: 'Copy' })
|
||||
await expect.poll(() => copyButtons.count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
|
||||
await copyButtons.first().focus()
|
||||
await expect.poll(() => page.getByRole('button', { name: '在新对话中分支' }).count(), { timeout: 5_000 })
|
||||
await expect.poll(() => page.getByRole('button', { name: 'Branch into a new conversation' }).count(), { timeout: 5_000 })
|
||||
.toBeGreaterThanOrEqual(2)
|
||||
await expect.poll(() => page.getByRole('button', { name: '编辑' }).count(), { timeout: 5_000 }).toBe(1)
|
||||
await expect.poll(() => page.getByRole('button', { name: 'Edit' }).count(), { timeout: 5_000 }).toBe(1)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('matches the conversation aria golden with IconActions and clocks', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-actions-aria'))
|
||||
await page.getByRole('button', {
|
||||
name: '选择模型,当前 deepseek-v4-flash',
|
||||
name: 'Select model, current deepseek-v4-flash',
|
||||
}).waitFor({ timeout: 10_000 })
|
||||
// Keep a footer focused so opacity-hidden actions stay in the a11y tree
|
||||
// as an active/focused control during the capture.
|
||||
await page.getByRole('button', { name: '复制' }).first().focus()
|
||||
await page.getByRole('button', { name: 'Copy' }).first().focus()
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('forks through the settled-message and session-row actions', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-fork'))
|
||||
// Exercise the assistant action specifically; package coverage pins the
|
||||
// user action separately at its own event seq.
|
||||
await page.getByRole('button', { name: 'Branch into a new conversation' }).last().click()
|
||||
await expect.poll(
|
||||
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SEED_ID)),
|
||||
{ timeout: 15_000 },
|
||||
).toBeDefined()
|
||||
await expect.poll(
|
||||
() => page.locator('[role="treeitem"]').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(3)
|
||||
await expect.poll(
|
||||
() => page.locator('[role="treeitem"][aria-selected="true"]').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(1)
|
||||
// The row action owns a distinct ui-workspace injection from the message
|
||||
// action above, so exercise both through the loaded app before capture.
|
||||
const sourceRow = page.locator('[role="treeitem"][aria-selected="true"]')
|
||||
const rowBox = await sourceRow.boundingBox()
|
||||
if (rowBox === null) throw new Error('fork source row has no layout box')
|
||||
const actionButton = sourceRow.locator('button[aria-label^="Session actions for "]')
|
||||
await sourceRow.hover({ position: { x: rowBox.width - 16, y: rowBox.height / 2 } })
|
||||
await expect.poll(() => actionButton.isVisible(), { timeout: 2_000 }).toBe(true)
|
||||
const buttonBox = await actionButton.boundingBox()
|
||||
if (buttonBox === null) throw new Error('fork source row action has no layout box')
|
||||
await page.mouse.click(buttonBox.x + buttonBox.width / 2, buttonBox.y + buttonBox.height / 2)
|
||||
await page.getByRole('menuitem', { name: 'Fork session' }).click()
|
||||
await expect.poll(
|
||||
() => scaffold.ctx.agents.list().filter(agent => agent.session.header.parentSession !== undefined).length,
|
||||
{ timeout: 15_000 },
|
||||
).toBe(2)
|
||||
await expect.poll(
|
||||
() => page.locator('[role="treeitem"]').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(4)
|
||||
await expect.poll(
|
||||
() => page.locator('[role="treeitem"][aria-selected="true"]').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(1)
|
||||
const tree = await captureStableAria(
|
||||
page,
|
||||
'[role="tree"][aria-label="Sessions"]',
|
||||
scaffold.workspaceCwd,
|
||||
)
|
||||
await compareOrRefreshGolden(FORK_EXPECTED, tree, MODE)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('issued zero model calls and kept a closed inventory', async () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['fork.expected.md', 'ui.expected.md'])
|
||||
})
|
||||
})
|
||||
|
||||
119
apps/web/tests/models-settings.e2e.ts
Normal file
119
apps/web/tests/models-settings.e2e.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// Web e2e scenario: the Models settings page end to end through the real
|
||||
// wire — the add card offers the dormant pi-ai catalog, typing an API key
|
||||
// stores it write-only under the derived reference (`MINIMAX_CN_API_KEY`)
|
||||
// while the settings document records only that reference, and the saved
|
||||
// route registers live (the row's 已启用 badge is the topology invalidation
|
||||
// landing). The customized-settings fold writes the curated reasoning field
|
||||
// as a merge patch. Zero model calls: configuration is pure
|
||||
// settings/credentials/llm-domain traffic, so there is no fixture and a
|
||||
// stray stream would fail loud on the open seam. The provider under test is
|
||||
// minimax-cn so a developer's real ANTHROPIC/OPENAI environment keys can
|
||||
// never shadow the derived reference.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/models-settings', import.meta.url))
|
||||
const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md')
|
||||
const CONFIGURED_EXPECTED = join(SNAPSHOT_DIR, 'configured.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('opens the add card over the dormant directory vocabulary', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-empty'))
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '模型' }).click()
|
||||
await dialog.getByText('填入各提供方的 API 密钥即可使用其模型。').waitFor({ timeout: 10_000 })
|
||||
// The dormant pi-ai adapter contributes its whole installed catalog; no
|
||||
// provider is configured yet, so the page is one add button.
|
||||
const add = dialog.getByRole('button', { name: '+ 添加提供方' })
|
||||
await add.waitFor({ timeout: 10_000 })
|
||||
// The button enables once the dormant catalog lands in the join.
|
||||
await expect.poll(async () => add.isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
await add.click()
|
||||
const pick = dialog.getByLabel('提供方')
|
||||
await pick.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(async () => pick.locator('option').count(), { timeout: 10_000 }).toBeGreaterThan(30)
|
||||
const options = await pick.locator('option').allTextContents()
|
||||
expect(options).toContain('anthropic')
|
||||
expect(options).toContain('minimax-cn')
|
||||
await pick.selectOption('minimax-cn')
|
||||
await dialog.getByLabel('API 密钥').waitFor({ timeout: 10_000 })
|
||||
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(EMPTY_EXPECTED, snapshot, MODE)
|
||||
}, 60_000)
|
||||
|
||||
it('stores the key under the derived reference and the route registers live', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-add'))
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.getByLabel('API 密钥').fill('sk-e2e-minimax')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
// The profile lands in settings.yaml with only the derived reference, the
|
||||
// key value lands in the harness home's .env, the dormant route
|
||||
// registers, and the topology frame invalidates the page into the row.
|
||||
const row = dialog.getByText('minimax-cn', { exact: true }).first()
|
||||
await row.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByText('已启用').waitFor({ timeout: 10_000 })
|
||||
const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
|
||||
expect(document).toContain('minimax-cn:')
|
||||
expect(document).toContain('apiKeyEnv: MINIMAX_CN_API_KEY')
|
||||
expect(document).not.toContain('sk-e2e-minimax')
|
||||
const stored = await readFile(join(scaffold.harnessHome, '.env'), 'utf8')
|
||||
expect(stored).toContain('MINIMAX_CN_API_KEY=sk-e2e-minimax')
|
||||
expect(await page.content()).not.toContain('sk-e2e-minimax')
|
||||
}, 60_000)
|
||||
|
||||
it('applies a customized-settings field as a merge patch', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-customized'))
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.getByRole('button', { name: '编辑' }).click()
|
||||
await dialog.getByText('自定义设置').click()
|
||||
const effort = dialog.getByLabel('推理强度')
|
||||
await effort.waitFor({ timeout: 10_000 })
|
||||
await effort.selectOption('high')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
// The editor closes back to the row; the fold's write merged into the
|
||||
// stored profile beside the reference.
|
||||
await expect.poll(async () => dialog.getByLabel('推理强度').count(), { timeout: 10_000 }).toBe(0)
|
||||
const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
|
||||
expect(document).toContain('reasoning: high')
|
||||
expect(document).toContain('apiKeyEnv: MINIMAX_CN_API_KEY')
|
||||
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(CONFIGURED_EXPECTED, snapshot, MODE)
|
||||
await page.keyboard.press('Escape')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['configured.expected.md', 'empty.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -18,11 +18,12 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/navigation-panes', import.meta.url))
|
||||
const SEED = join(SNAPSHOT_DIR, 'seed.jsonl')
|
||||
const TRAJECTORY_EXPECTED = join(SNAPSHOT_DIR, 'trajectory.expected.md')
|
||||
const SEARCH_EXPECTED = join(SNAPSHOT_DIR, 'search-results.expected.md')
|
||||
const TERMINAL_EXPECTED = join(SNAPSHOT_DIR, 'terminal-card.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'navigation-panes-web-e2e'
|
||||
@@ -56,7 +57,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
await seedSession(scaffold, raw, SEED_ID)
|
||||
}
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
slotErrors = []
|
||||
page.on('console', (message) => {
|
||||
@@ -95,39 +96,39 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
expect(calls.map(e => e.data.name).sort()).toEqual(['bash', 'read', 'read'])
|
||||
}, 400_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('opens the seeded session and renders both turns from the log', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-open'))
|
||||
// Expand the collapsed group row, then open the revealed session row.
|
||||
const groupRow = page.locator('[role="treeitem"]').first()
|
||||
await groupRow.waitFor({ timeout: 15_000 })
|
||||
await groupRow.click()
|
||||
const sessionRow = page.locator('[role="treeitem"]').nth(1)
|
||||
await sessionRow.waitFor({ timeout: 10_000 })
|
||||
await sessionRow.click()
|
||||
it.skipIf(MODE === 'record')('finds an unopened seeded session by message content and opens it', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-search'))
|
||||
const search = page.getByPlaceholder('Search name, keywords', { exact: false })
|
||||
// The cold row has not been opened, so only the persisted log can satisfy
|
||||
// this query. First search lazily reconciles the SQLite content index.
|
||||
await search.fill('zzzqx-no-such-session')
|
||||
await page.getByText('No matching sessions').waitFor({ timeout: 30_000 })
|
||||
await expect.poll(
|
||||
() => page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem').count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(0)
|
||||
|
||||
await search.fill('WATERFALL')
|
||||
const resultTree = page.getByRole('tree', { name: 'Search results' })
|
||||
const result = resultTree.getByRole('treeitem')
|
||||
await expect.poll(() => result.count(), { timeout: 30_000 }).toBe(1)
|
||||
await expect.poll(() => result.getByText('WATERFALL', { exact: false }).count(), {
|
||||
timeout: 10_000,
|
||||
}).toBeGreaterThanOrEqual(1)
|
||||
const snapshot = (await captureStableAria(page, '[class*="listArea"]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(SEARCH_EXPECTED, snapshot, MODE)
|
||||
|
||||
await result.click()
|
||||
// Search navigation addresses the session, not a specific event, and the
|
||||
// query remains until the user explicitly clears it.
|
||||
await expect.poll(() => search.inputValue(), { timeout: 5_000 }).toBe('WATERFALL')
|
||||
await expect.poll(() => page.getByText('FIRST_DONE', { exact: true }).count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(1)
|
||||
await expect.poll(() => page.getByRole('heading', { name: 'Navigation Summary' }).count(), { timeout: 15_000 }).toBe(1)
|
||||
}, 90_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('filters the sidebar tree by title through the search box', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-search'))
|
||||
// Runs after the session is open: a cold summary carries no title (the
|
||||
// sidebar shows the cwd basename), and the durable title lands with the
|
||||
// attach subscription's baseline — which is itself worth pinning: search
|
||||
// matches the title the user sees, not a hidden cold field.
|
||||
const search = page.getByPlaceholder('Search name, keywords', { exact: false })
|
||||
await expect.poll(() => page.getByText('NavScenario', { exact: false }).count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(1)
|
||||
// Negative: a garbage query empties the tree (group rows hide too).
|
||||
await search.fill('zzzqx-no-such-session')
|
||||
await expect.poll(() => page.locator('[role="treeitem"]').count(), { timeout: 10_000 }).toBe(0)
|
||||
// Positive: a title word narrows to the matched session + its group,
|
||||
// force-expanded by search mode (case-insensitive client-side filter).
|
||||
await search.fill('navscenario')
|
||||
await expect.poll(() => page.locator('[role="treeitem"]').count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
|
||||
// Clear restores the unfiltered tree.
|
||||
await page.getByRole('button', { name: 'Clear search' }).click()
|
||||
await expect.poll(() => search.inputValue(), { timeout: 5_000 }).toBe('')
|
||||
await expect.poll(() => page.locator('[role="treeitem"]').count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(1)
|
||||
}, 60_000)
|
||||
}, 90_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('renders the trajectory ledger and opens its local record inspector', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-trajectory'))
|
||||
@@ -173,33 +174,39 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
await expect.poll(() => page.locator('tr[data-timeline-focus]').count(), { timeout: 10_000 }).toBe(0)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column open', async () => {
|
||||
it.skipIf(MODE === 'record')('bash and file-path rows leave the default details column closed', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-details'))
|
||||
await page.getByRole('tab', { name: 'Chat' }).click()
|
||||
const bashRow = page.locator('[data-sample="bash-global"]').first()
|
||||
await bashRow.waitFor({ timeout: 15_000 })
|
||||
const frame = page.locator('[style*="grid-template-columns"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
|
||||
// The row click is the card's expand toggle (unified tool-row
|
||||
// interaction); it must not drive layout geometry either way.
|
||||
await bashRow.click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
|
||||
// The card's own controls are outside the summary row and must not open
|
||||
// details either — the terminal card is read in place.
|
||||
await page.locator('[data-sample="bash-global"] ~ [data-terminal] [class*="_copyButton_"]').first().click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
// details either — the expanded terminal card is read in place.
|
||||
await page.locator('[data-sample="bash-global"] ~ div [data-terminal] [class*="_copyButton_"]').first().click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
|
||||
// Read summaries are host-open file links; they also must not open details.
|
||||
const fileLink = page.locator('[data-variant="read"] button').first()
|
||||
await fileLink.waitFor({ timeout: 10_000 })
|
||||
await fileLink.click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('renders the bash row as a terminal card in the real browser', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-terminal'))
|
||||
await page.getByRole('tab', { name: 'Chat' }).click()
|
||||
// The card is resident in the keyed bash row (no expand gesture): the
|
||||
// recorded command's own output sits in the message flow, derived from the
|
||||
// logged call/result presentations alone.
|
||||
const card = page.locator('[data-sample="bash-global"] ~ [data-terminal], [data-sample="bash-global"] [data-terminal]').first()
|
||||
// The card is expand-gated behind the whole-row toggle (the unified
|
||||
// tool-row interaction): open it if a previous case left it collapsed.
|
||||
// Expanded, the recorded command's own output sits in the message flow,
|
||||
// derived from the logged call/result presentations alone.
|
||||
const bashRow = page.locator('[data-sample="bash-global"]').first()
|
||||
await bashRow.waitFor({ timeout: 15_000 })
|
||||
if (await bashRow.getAttribute('aria-expanded') !== 'true') await bashRow.click()
|
||||
const card = page.locator('[data-sample="bash-global"] ~ div [data-terminal]').first()
|
||||
await card.waitFor({ timeout: 15_000 })
|
||||
// Real layout, not jsdom's stub (which computes no geometry at all):
|
||||
// squeeze the output pane below its content width and the line must keep
|
||||
@@ -253,7 +260,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
}
|
||||
})
|
||||
expect(dot.state).toBe('done')
|
||||
expect(dot.label).toBe('已完成')
|
||||
expect(dot.label).toBe('Done')
|
||||
expect(dot.beforePrompt).toBe(true)
|
||||
expect(dot.insideCard).toBe(true)
|
||||
expect(dot.leftOfPrompt).toBe(true)
|
||||
@@ -270,7 +277,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
|
||||
await card.locator('[class*="_copyButton_"]').first().click()
|
||||
await expect.poll(() => card.locator('[class*="_copyButton_"]').first().textContent(), { timeout: 5_000 })
|
||||
.toBe('复制成功')
|
||||
.toBe('Copied')
|
||||
expect(await page.evaluate(() => navigator.clipboard.readText())).toContain('NAVIGATION_OK')
|
||||
}, 60_000)
|
||||
|
||||
@@ -279,7 +286,8 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
expect(slotErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, [
|
||||
'seed.jsonl', 'trajectory.expected.md', 'terminal-card.expected.md',
|
||||
'seed.jsonl', 'search-results.expected.md', 'trajectory.expected.md',
|
||||
'terminal-card.expected.md',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
91
apps/web/tests/onboarding-deepseek-config.e2e.ts
Normal file
91
apps/web/tests/onboarding-deepseek-config.e2e.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// Keyless browser e2e: the shipped DeepSeek adapter stays mounted while its
|
||||
// credential is absent, onboarding routes to the real Models editor, and its
|
||||
// write lands in an isolated harness home without a reload or model call.
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/onboarding-deepseek-config', import.meta.url))
|
||||
const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const browserConsole: string[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 960 } })
|
||||
tripwire = watchConsole(page)
|
||||
page.on('console', message => browserConsole.push(message.text()))
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('stores a key write-only and observes configured state without restarting', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-config'))
|
||||
const dialog = page.getByRole('dialog', { name: '添加一个 API Key 开始使用' })
|
||||
await dialog.waitFor({ timeout: 15_000 })
|
||||
expect(await dialog.getByRole('textbox').count()).toBe(0)
|
||||
const initial = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE)
|
||||
|
||||
await dialog.getByRole('button', { name: '前往配置' }).click()
|
||||
await dialog.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
const keyInput = settings.getByLabel('API 密钥', { exact: true })
|
||||
await keyInput.waitFor({ timeout: 10_000 })
|
||||
|
||||
const secret = `dsh_onboarding_${randomBytes(12).toString('hex')}`
|
||||
await keyInput.fill(secret)
|
||||
await settings.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await keyInput.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
|
||||
const stored = await readFile(join(scaffold.harnessHome, '.env'), 'utf8')
|
||||
expect(stored.includes(`DEEPSEEK_API_KEY=${secret}`)).toBe(true)
|
||||
expect((await page.content()).includes(secret)).toBe(false)
|
||||
expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false)
|
||||
expect(browserConsole.some(line => line.includes(secret))).toBe(false)
|
||||
|
||||
// The same open Models surface reuses the refreshed join and exposes the
|
||||
// configured write-only placeholder without a reload.
|
||||
const deepSeekRow = settings.getByText('DeepSeek', { exact: true }).first()
|
||||
await deepSeekRow.waitFor({ timeout: 10_000 })
|
||||
await deepSeekRow.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click()
|
||||
const configuredInput = settings.getByLabel('API 密钥', { exact: true })
|
||||
await configuredInput.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(
|
||||
() => configuredInput.getAttribute('placeholder'),
|
||||
{ timeout: 10_000 },
|
||||
).toBe('已配置——输入新值可替换')
|
||||
|
||||
expect((await page.content()).includes(secret)).toBe(false)
|
||||
expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false)
|
||||
expect(browserConsole.some(line => line.includes(secret))).toBe(false)
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['missing.expected.md'])
|
||||
})
|
||||
})
|
||||
113
apps/web/tests/plan-review.e2e.ts
Normal file
113
apps/web/tests/plan-review.e2e.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// Web e2e scenario: the plan-review takeover. The shipped composition mounts
|
||||
// plan mode and its client seat, so `/plan <task>` enters plan mode for real
|
||||
// and the recorded turn ends on exit_plan_mode blocking against the live
|
||||
// userInteraction seam. The composer is then occupied by the plan decision
|
||||
// card — not the generic question flow — and approving it through the card
|
||||
// completes the turn with the approval in the log.
|
||||
// Replay is deterministic: the plan content arrives from replayed chunks, the
|
||||
// review wait is real, and the approve click is the test's own gesture (the
|
||||
// turn cannot complete without it, in record and replay alike).
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/plan-review', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
// The waiting golden owns the decision card; the approved golden owns the
|
||||
// transcript the approval leaves behind — the state the card cannot see.
|
||||
const REVIEW_EXPECTED = join(SNAPSHOT_DIR, 'review.expected.md')
|
||||
const APPROVED_EXPECTED = join(SNAPSHOT_DIR, 'approved.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
// One command line: /plan enters plan mode and submits the rest as the turn's
|
||||
// message. The task is deliberately self-contained (nothing to explore in a
|
||||
// fresh workspace) so the recorded turn is a plan and its review, and the
|
||||
// approved continuation is one word.
|
||||
const TASK = 'Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. '
|
||||
+ 'Call exit_plan_mode with a short plan of at most five bullet points. '
|
||||
+ 'Once the plan is approved, reply with the single word DONE and stop.'
|
||||
const LINE = `/plan ${TASK}`
|
||||
|
||||
describe('web e2e: plan review takeover round trip', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 })
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
// English page: the decision copy is the surface under test, and the
|
||||
// golden pins one language.
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('reviews the plan on a decision card and approves through it', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-plan-review'))
|
||||
if (MODE !== 'record') {
|
||||
expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([TASK])
|
||||
}
|
||||
const input = page.locator('textarea').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
const settled = scaffold.whenTurnSettled(MODE === 'record' ? 180_000 : 30_000)
|
||||
await input.fill(LINE)
|
||||
await input.press('Enter')
|
||||
|
||||
// The card takes over the input area while exit_plan_mode blocks. Its
|
||||
// presence is a STABLE waiting state (it stays until answered), so a plain
|
||||
// waitFor is race-free.
|
||||
const card = page.locator('[data-plan-review-key]')
|
||||
await card.waitFor({ timeout: MODE === 'record' ? 120_000 : 30_000 })
|
||||
// The plan-review request must NOT land on the generic question flow.
|
||||
expect(await page.locator('[data-question-key]').count()).toBe(0)
|
||||
await expect.poll(() => card.getByText('Plan review').count(), { timeout: 10_000 }).toBeGreaterThan(0)
|
||||
|
||||
if (MODE !== 'record') {
|
||||
const snapshot = await captureStableAria(page, '[data-plan-review-key]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(REVIEW_EXPECTED, snapshot, MODE)
|
||||
}
|
||||
|
||||
await card.getByRole('button', { name: 'Approve' }).click()
|
||||
|
||||
const sessionId = await settled
|
||||
if (MODE === 'record') {
|
||||
await recordFixture(scaffold, sessionId, FIXTURE)
|
||||
return
|
||||
}
|
||||
// World state: the approval reached the tool, and plan mode is left behind.
|
||||
const results = sessionEvents.filter(e => e.type === 'tool/result')
|
||||
expect(JSON.stringify(results.at(-1))).toContain('Plan approved')
|
||||
await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(1)
|
||||
// Card gone; regular input restored.
|
||||
expect(await page.locator('[data-plan-review-key]').count()).toBe(0)
|
||||
await expect.poll(() => page.locator('textarea').first().isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(APPROVED_EXPECTED, snapshot, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 200_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'review.expected.md', 'approved.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/question-composer', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
@@ -44,7 +44,7 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 })
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
|
||||
@@ -16,10 +16,11 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/queue-actions', import.meta.url))
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/live-interactions/session.jsonl', import.meta.url))
|
||||
const COLLAPSED_EXPECTED = join(SNAPSHOT_DIR, 'collapsed.expected.md')
|
||||
const EDITING_EXPECTED = join(SNAPSHOT_DIR, 'editing.expected.md')
|
||||
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -63,7 +64,7 @@ describe('web e2e: queue row actions', () => {
|
||||
scaffold = await launchWebScaffold({ replayFixture: FIXTURE, replayOverride: overridePath })
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
const tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -80,8 +81,18 @@ describe('web e2e: queue row actions', () => {
|
||||
await input.fill(text)
|
||||
await input.press('Enter')
|
||||
}
|
||||
const queueHeader = page.getByRole('button', { name: '2 queued messages' })
|
||||
await expect.poll(() => queueHeader.getAttribute('aria-expanded'), { timeout: 10_000 })
|
||||
.toBe('false')
|
||||
const collapsedSnapshot = await captureStableAria(
|
||||
page,
|
||||
'[class*="centerCol"]',
|
||||
scaffold.workspaceCwd,
|
||||
)
|
||||
await compareOrRefreshGolden(COLLAPSED_EXPECTED, collapsedSnapshot, MODE)
|
||||
await queueHeader.click()
|
||||
await expect.poll(
|
||||
() => page.getByRole('button', { name: '删除排队消息' }).count(),
|
||||
() => page.getByRole('button', { name: 'Remove queued message' }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(2)
|
||||
|
||||
@@ -106,16 +117,16 @@ describe('web e2e: queue row actions', () => {
|
||||
await page.setViewportSize({ width: 1680, height: 1000 })
|
||||
|
||||
const editRow = page.getByText(EDIT, { exact: true }).locator('..')
|
||||
await editRow.getByRole('button', { name: '编辑排队消息' }).click()
|
||||
const editor = page.getByRole('textbox', { name: '编辑排队消息' })
|
||||
await editRow.getByRole('button', { name: 'Edit queued message' }).click()
|
||||
const editor = page.getByRole('textbox', { name: 'Edit queued message' })
|
||||
await editor.fill(EDITED)
|
||||
const editingSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(EDITING_EXPECTED, editingSnapshot, MODE)
|
||||
await page.getByRole('button', { name: '保存排队消息' }).click()
|
||||
await page.getByRole('button', { name: 'Save queued message' }).click()
|
||||
await page.getByText(EDITED, { exact: true }).waitFor()
|
||||
|
||||
const removeRow = page.getByText(REMOVE, { exact: true }).locator('..')
|
||||
await removeRow.getByRole('button', { name: '删除排队消息' }).click()
|
||||
await removeRow.getByRole('button', { name: 'Remove queued message' }).click()
|
||||
await expect.poll(() => page.getByText(REMOVE, { exact: true }).count()).toBe(0)
|
||||
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
@@ -125,13 +136,16 @@ describe('web e2e: queue row actions', () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
|
||||
const editedRow = page.getByText(EDITED, { exact: true }).locator('..')
|
||||
await editedRow.getByRole('button', { name: '删除排队消息' }).click()
|
||||
await editedRow.getByRole('button', { name: 'Remove queued message' }).click()
|
||||
await expect.poll(() => page.getByText(EDITED, { exact: true }).count()).toBe(0)
|
||||
await page.getByRole('button', { name: 'Stop generating' }).click()
|
||||
await settled
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps its snapshot inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['editing.expected.md', 'ui.expected.md'])
|
||||
await assertFixtureInventory(
|
||||
SNAPSHOT_DIR,
|
||||
['collapsed.expected.md', 'editing.expected.md', 'ui.expected.md'],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/fresh-round-trip', import.meta.url))
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/fresh-round-trip/session.jsonl', import.meta.url))
|
||||
@@ -43,7 +43,7 @@ describe('web e2e: fresh round trip through the real assembly', () => {
|
||||
})
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -106,7 +106,7 @@ describe('web e2e: fresh round trip through the real assembly', () => {
|
||||
await expect(page.getByRole('textbox').first().isVisible()).resolves.toBe(true)
|
||||
expect(await page.getByText('WEB_E2E_OK', { exact: false }).count()).toBeGreaterThanOrEqual(1)
|
||||
await page.getByRole('button', {
|
||||
name: '选择模型,当前 DeepSeek-V4-Flash',
|
||||
name: 'Select model, current DeepSeek-V4-Flash',
|
||||
}).waitFor({ timeout: 10_000 })
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
|
||||
57
apps/web/tests/scaffold-hermetic.e2e.ts
Normal file
57
apps/web/tests/scaffold-hermetic.e2e.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import type {} from '@deepseek-ai/dsh-skill'
|
||||
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
|
||||
|
||||
async function writeSkill(root: string, name: string): Promise<void> {
|
||||
const bundle = join(root, name)
|
||||
await mkdir(bundle, { recursive: true })
|
||||
await writeFile(join(bundle, 'SKILL.md'), `---
|
||||
name: ${name}
|
||||
description: Must not enter the Web replay scaffold
|
||||
---
|
||||
|
||||
Ambient host state.
|
||||
`)
|
||||
}
|
||||
|
||||
it('isolates replay skill discovery from every ambient host root', async () => {
|
||||
const ambient = await mkdtemp(join(tmpdir(), 'dsh-web-ambient-skills-'))
|
||||
const dshHome = join(ambient, 'dsh-home')
|
||||
const agentsHome = join(ambient, 'agents-home')
|
||||
const bundled = join(ambient, 'bundled')
|
||||
await Promise.all([
|
||||
writeSkill(join(dshHome, 'skills'), 'ambient-dsh'),
|
||||
writeSkill(join(agentsHome, 'skills'), 'ambient-agents'),
|
||||
writeSkill(bundled, 'ambient-bundled'),
|
||||
])
|
||||
|
||||
const originalDshHome = process.env.DSH_HOME
|
||||
const originalAgentsHome = process.env.DSH_AGENTS_HOME
|
||||
const originalBundled = process.env.DSH_BUNDLED_SKILL_DIR
|
||||
process.env.DSH_HOME = dshHome
|
||||
process.env.DSH_AGENTS_HOME = agentsHome
|
||||
process.env.DSH_BUNDLED_SKILL_DIR = bundled
|
||||
let scaffold: WebScaffold | undefined
|
||||
try {
|
||||
scaffold = await launchWebScaffold()
|
||||
const names = (await scaffold.ctx.skills.list({ cwd: scaffold.workspaceCwd })).map(skill => skill.name)
|
||||
expect(names).not.toContain('ambient-dsh')
|
||||
expect(names).not.toContain('ambient-agents')
|
||||
expect(names).not.toContain('ambient-bundled')
|
||||
} finally {
|
||||
try {
|
||||
await scaffold?.close()
|
||||
} finally {
|
||||
if (originalDshHome === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = originalDshHome
|
||||
if (originalAgentsHome === undefined) delete process.env.DSH_AGENTS_HOME
|
||||
else process.env.DSH_AGENTS_HOME = originalAgentsHome
|
||||
if (originalBundled === undefined) delete process.env.DSH_BUNDLED_SKILL_DIR
|
||||
else process.env.DSH_BUNDLED_SKILL_DIR = originalBundled
|
||||
await rm(ambient, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,20 +1,23 @@
|
||||
// Shared scaffold for the keyless browser e2e lane (Agent Note:
|
||||
// .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md).
|
||||
// Boots the REAL web composition — the shipped apps/cli/cordis.yml through
|
||||
// Boots the REAL web composition — the shipped base plus web overlay through
|
||||
// the vendored Loader (the same include boot AppCLIEntry drives), patched the
|
||||
// snapshot way — so a real chromium exercises the real HTTP/SSE wire, the
|
||||
// api-gateway, agent loop, tools, and persistence. Modes ride $DSH_SNAPSHOT:
|
||||
// replay (default, keyless: llm-deepseek row disabled, dsh-llm-replay row
|
||||
// inserted in providers mode), record (real adapter + key, harvests fixtures
|
||||
// from live session memory), refresh (keyless replay that rewrites goldens).
|
||||
// replay (default, keyless: normally disables the llm-deepseek row and
|
||||
// inserts dsh-llm-replay in providers mode), record (real adapter + key,
|
||||
// harvests fixtures from live session memory), refresh (keyless replay that
|
||||
// rewrites goldens). A first-run option keeps the real adapter mounted while
|
||||
// masking its credential, without making a model call.
|
||||
//
|
||||
// Composition divergences from `dsh web`, all deliberate, all via include
|
||||
// patches over the SAME tree (never a second yml): temp persistenceRoot;
|
||||
// workspace-context disabled (recorded fixtures must not embed this repo's
|
||||
// AGENTS.md); session-title-llm disabled (its fire-and-forget title call
|
||||
// would race the loop for the session's replay cursor); webserver pinned to
|
||||
// port 0 with the built dist; keyless modes disable llm-deepseek and fill
|
||||
// the open llm seam post-boot with installLlmReplay on the settled root ctx
|
||||
// patches after the shipped surface overlay: temp persistenceRoot; local skill
|
||||
// roots confined to the temp workspace; workspace-context disabled (recorded
|
||||
// fixtures must not embed this repo's AGENTS.md); session-title-llm disabled
|
||||
// (its fire-and-forget title call would race the loop for the session's replay
|
||||
// cursor); webserver pinned to port 0 with the built dist; ordinary keyless
|
||||
// modes disable llm-deepseek and fill the open llm seam post-boot with
|
||||
// installLlmReplay on the settled root ctx
|
||||
// (the plugin-row path discards the ReplayHandle; the direct install keeps
|
||||
// assertConsumed for the teardown fixture-consumption check).
|
||||
import { existsSync } from 'node:fs'
|
||||
@@ -28,7 +31,7 @@ import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include, { type PatchOptions } from '@cordisjs/plugin-include'
|
||||
import { scrubRequestHeaders } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { assertEntriesLoaded } from '@deepseek-ai/dsh-app-boot'
|
||||
import { assertEntriesLoaded, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
|
||||
import type { ReplayHandle } from '@deepseek-ai/dsh-llm-replay'
|
||||
import { installLlmReplay, parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
||||
import SessionStore, {
|
||||
@@ -60,8 +63,9 @@ export function webSnapshotMode(): WebSnapshotMode {
|
||||
throw new Error(`DSH_SNAPSHOT must be replay, record, or refresh; got ${JSON.stringify(value)}`)
|
||||
}
|
||||
|
||||
/** The shipped composition under test: apps/cli's config tree. */
|
||||
const CONFIG_PATH = join(REPO_ROOT, 'apps/cli/cordis.yml')
|
||||
/** The shipped composition under test: apps/cli's shared base and web overlay. */
|
||||
const CONFIG_PATH = join(REPO_ROOT, 'apps/cli/config/base.cordis.yml')
|
||||
const WEB_OVERLAY_PATH = join(REPO_ROOT, 'apps/cli/config/web.cordis.yml')
|
||||
|
||||
// Replay publishes the provider catalog the gateway routes to (providers
|
||||
// mode, never catch-all: with llm-deepseek disabled no adapter exists, so a
|
||||
@@ -69,7 +73,7 @@ const CONFIG_PATH = join(REPO_ROOT, 'apps/cli/cordis.yml')
|
||||
// post-step pressure check would warn every step). The published
|
||||
// contextWindow keeps that pressure path provably inert for small fixtures.
|
||||
const REPLAY_PROVIDERS = [{
|
||||
id: 'deepseek',
|
||||
id: 'deepseek-official',
|
||||
name: 'DeepSeek',
|
||||
models: [{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', contextWindow: 128_000 }],
|
||||
}]
|
||||
@@ -86,6 +90,8 @@ export interface WebScaffold {
|
||||
workspaceCwd: string
|
||||
/** Temp persistence root (seeded sessions land here through the real API). */
|
||||
persistenceRoot: string
|
||||
/** Isolated harness home the settings/credentials rows write ($DSH_HOME double). */
|
||||
harnessHome: string
|
||||
/** Await a settled turn end: in-process turn/end, then the agent's idle flip (which follows the persistence flush). */
|
||||
whenTurnSettled(timeoutMs?: number): Promise<SessionId>
|
||||
/** Tear everything down; asserts the replay fixture was fully consumed first (replay/refresh). */
|
||||
@@ -123,6 +129,12 @@ export interface LaunchOptions {
|
||||
* remain reconstructable without making the tools a product default.
|
||||
*/
|
||||
cordisTools?: boolean
|
||||
/**
|
||||
* Keep the shipped DeepSeek adapter mounted while masking the process
|
||||
* environment's DEEPSEEK_API_KEY for this scaffold lifetime. This is the
|
||||
* keyless first-run configuration lane; the default disables the adapter.
|
||||
*/
|
||||
deepSeekMissingCredential?: boolean
|
||||
}
|
||||
|
||||
/** Dispose the booted tree and remove both owned temp roots, reporting every independent cleanup failure. */
|
||||
@@ -149,7 +161,26 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
throw new Error('web e2e record mode needs DEEPSEEK_API_KEY (env or repo-root .env)')
|
||||
}
|
||||
}
|
||||
if (mode === 'record' && options.deepSeekMissingCredential === true) {
|
||||
throw new Error('deepSeekMissingCredential is a keyless replay/refresh option')
|
||||
}
|
||||
const maskDeepSeekCredential = mode !== 'record' && options.deepSeekMissingCredential === true
|
||||
const originalDeepSeekCredential = process.env.DEEPSEEK_API_KEY
|
||||
let credentialEnvironmentRestored = false
|
||||
const restoreCredentialEnvironment = (): void => {
|
||||
if (credentialEnvironmentRestored || !maskDeepSeekCredential) return
|
||||
credentialEnvironmentRestored = true
|
||||
if (originalDeepSeekCredential === undefined) {
|
||||
Reflect.deleteProperty(process.env, 'DEEPSEEK_API_KEY')
|
||||
} else {
|
||||
process.env.DEEPSEEK_API_KEY = originalDeepSeekCredential
|
||||
}
|
||||
}
|
||||
const workspaceCwd = await realpath(await mkdtemp(join(tmpdir(), 'dsh-web-e2e-ws-')))
|
||||
// Isolated harness home: the settings/credentials rows resolve $DSH_HOME
|
||||
// paths at load, and an in-process boot must NEVER touch the developer's
|
||||
// real ~/.dsh document or credential file.
|
||||
const harnessHome = join(workspaceCwd, '.dsh-home')
|
||||
let persistenceRoot: string
|
||||
try {
|
||||
persistenceRoot = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-sessions-'))
|
||||
@@ -159,27 +190,60 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'web scaffold temp-root setup failed')
|
||||
throw error
|
||||
}
|
||||
if (maskDeepSeekCredential) Reflect.deleteProperty(process.env, 'DEEPSEEK_API_KEY')
|
||||
|
||||
// The include patch set — the same mechanism AppCLIEntry and the ACP
|
||||
// snapshot overlay use, applied over the SAME shipped tree (a patch id that
|
||||
// stops matching a row fails the boot sweep loudly instead of drifting).
|
||||
const surfacePatches = loadOverlayPatches('web e2e scaffold', WEB_OVERLAY_PATH)
|
||||
const patches: PatchOptions[] = [
|
||||
...surfacePatches,
|
||||
{ id: 'session-persistence-jsonl', config: { root: persistenceRoot } },
|
||||
{ id: 'session-query-sqlite', config: { path: ':memory:', openAt: 'first-search' } },
|
||||
// storage-json's './.storages' yml default is cwd-relative and resolves
|
||||
// per write; the scaffold restores the original cwd after boot, so the
|
||||
// row gets an absolute temp root (removed with the workspace at close).
|
||||
{ id: 'storage-json', config: { root: join(workspaceCwd, '.dsh-storages') } },
|
||||
// Skill discovery is model-visible input. Pin every host-level root inside
|
||||
// the owned temp world so ~/.dsh, ~/.agents, and a bundled-root env setting
|
||||
// cannot change replay requests or conversation goldens. Project roots stay
|
||||
// enabled against the same empty temp workspace, preserving the real seam.
|
||||
{
|
||||
id: 'skill-local',
|
||||
config: {
|
||||
dshHome: join(workspaceCwd, '.dsh-home'),
|
||||
agentsHome: join(workspaceCwd, '.agents-home'),
|
||||
bundledSkillDir: join(workspaceCwd, '.bundled-skills'),
|
||||
watch: false,
|
||||
},
|
||||
},
|
||||
// fs/bash cwd default to process.cwd(); the gateway injects the same
|
||||
// value into session.cwd — chdir below anchors all three to the temp
|
||||
// workspace, keeping the composition untouched.
|
||||
{ id: 'workspace-context', disabled: true },
|
||||
{ id: 'session-title-llm', disabled: true },
|
||||
// Fixture sessions must never leave the process: the shipped row defaults
|
||||
// to the production OTLP endpoint (or whatever DSH_TELEMETRY_OTLP_URL
|
||||
// names in the ambient environment).
|
||||
{ id: 'telemetry-otel', disabled: true },
|
||||
{ id: 'webserver', config: { host: '127.0.0.1', port: 0, distIndex: DIST_INDEX } },
|
||||
{ id: 'settings', config: { dshHome: harnessHome } },
|
||||
{ id: 'credentials', config: { dshHome: harnessHome } },
|
||||
// The shipped directory-picker row is the -auto chooser, which resolves
|
||||
// the interaction from the RUNNING host (display, SSH launch, bind). The
|
||||
// lane's goldens are interaction-specific (workspace-management drives
|
||||
// the in-app browse dialog), so pin -browse deterministically on every
|
||||
// host: patch `name` is an assertion, not an override, hence the
|
||||
// disable+insert pair.
|
||||
{ id: 'directory-picker', disabled: true },
|
||||
{ insert: [{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' }] },
|
||||
...options.toolsMode === undefined ? [] : [{ id: 'tools', config: { mode: options.toolsMode } }],
|
||||
...options.cordisTools === true
|
||||
? [{ insert: [{ id: 'tool-cordis', name: 'cordis:tool-cordis' }] }]
|
||||
: [],
|
||||
...mode === 'record' ? [] : [{ id: 'llm-deepseek', disabled: true }],
|
||||
...mode === 'record' || options.deepSeekMissingCredential === true
|
||||
? []
|
||||
: [{ id: 'llm-deepseek', disabled: true }],
|
||||
]
|
||||
|
||||
// Sessions inherit the gateway's process.cwd() default; run the boot from
|
||||
@@ -208,10 +272,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
}
|
||||
port = boundPort
|
||||
|
||||
// Fill the open llm seam on the settled root ctx (llm-deepseek is disabled
|
||||
// in keyless modes; a scenario with no fixture leaves the seam empty so a
|
||||
// stray stream fails loud with NO_ADAPTER). The direct install, unlike the
|
||||
// plugin row, returns the ReplayHandle for the teardown consumption check.
|
||||
// Fill the open llm seam on the settled root ctx. Ordinary keyless modes
|
||||
// disable llm-deepseek; the first-run lane keeps it mounted but has no
|
||||
// replay fixture and never streams. The direct install, unlike the plugin
|
||||
// row, returns the ReplayHandle for the teardown consumption check.
|
||||
if (mode !== 'record' && options.replayFixture !== undefined) {
|
||||
replayHandle = installLlmReplay(ctx, {
|
||||
file: options.replayFixture,
|
||||
@@ -223,6 +287,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
} catch (error) {
|
||||
if (process.cwd() !== originalCwd) process.chdir(originalCwd)
|
||||
const cleanupFailures = await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot)
|
||||
restoreCredentialEnvironment()
|
||||
if (cleanupFailures.length > 0) {
|
||||
throw new AggregateError([error, ...cleanupFailures], 'web scaffold setup failed and cleanup was incomplete')
|
||||
}
|
||||
@@ -232,6 +297,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
}
|
||||
|
||||
return {
|
||||
harnessHome,
|
||||
mode,
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
ctx,
|
||||
@@ -270,7 +336,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
} catch (error) {
|
||||
failures.push(error)
|
||||
}
|
||||
failures.push(...await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot))
|
||||
try {
|
||||
failures.push(...await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot))
|
||||
} finally {
|
||||
restoreCredentialEnvironment()
|
||||
}
|
||||
if (failures.length > 0) throw new AggregateError(failures, 'web scaffold teardown failed')
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
// else covers: sidebar cold listing, the implicit resume/attach inside the
|
||||
// history RPC, history-page tool views, and the client fold of historical
|
||||
// events — with ZERO model calls in replay (no replay fixture; a stray stream
|
||||
// fails loud on the open llm seam). The seed is a recorded fixture under the
|
||||
// fails loud on the open llm seam). The cold session also carries the one
|
||||
// keyless command-row surface: an Access-chip pick runs `/permission` on the
|
||||
// host, so the settled row's copy has a golden here. The seed is a recorded fixture under the
|
||||
// same record discipline as every other: DSH_SNAPSHOT=record drives the turn
|
||||
// live through the composer (real read tool against seeded workspace files)
|
||||
// and harvests seed.jsonl; replay/refresh seed it cold and only render.
|
||||
@@ -12,16 +14,21 @@ import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/seeded-history', import.meta.url))
|
||||
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/ui.expected.md', import.meta.url))
|
||||
// The command-row golden: the same conversation after one /permission switch,
|
||||
// which is the only surface that shows a settled command row's copy.
|
||||
const COMMAND_ROW_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/command-row.expected.md', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'seeded-history-web-e2e'
|
||||
|
||||
@@ -49,7 +56,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
await seedSession(scaffold, raw, SEED_ID)
|
||||
}
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
@@ -117,6 +124,30 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
const toolRows = page.locator('[data-variant], [data-sample]')
|
||||
await expect.poll(() => toolRows.count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
|
||||
expect(await page.getByText('a.txt', { exact: false }).count()).toBeGreaterThan(0)
|
||||
|
||||
const agent = scaffold.ctx.agents.get(SessionId(SEED_ID))
|
||||
if (agent === undefined) throw new Error('seeded session did not attach an agent')
|
||||
agent.inject(createUserMessage({
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: '<system-reminder>\n'
|
||||
+ 'The following workspace instructions may be relevant to your work. '
|
||||
+ 'Use them as guidance when applicable.\n\n'
|
||||
+ Array.from({ length: 24 }, (_, index) => `Instruction ${index + 1}: preserve the logged context contract.`).join('\n')
|
||||
+ '\n</system-reminder>',
|
||||
}],
|
||||
source: {
|
||||
kind: 'workspace-instructions',
|
||||
baseline: true,
|
||||
changes: [{
|
||||
action: 'set',
|
||||
scope: '.\u0000AGENTS.md',
|
||||
path: 'AGENTS.md',
|
||||
digest: 'context-injection-browser-snapshot',
|
||||
}],
|
||||
},
|
||||
}))
|
||||
await page.getByRole('button', { name: 'Context injection' }).waitFor({ timeout: 10_000 })
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('matches the historical conversation aria golden', async () => {
|
||||
@@ -125,13 +156,65 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
// This scenario deliberately leaves the LLM seam open to prove zero
|
||||
// model calls. History still restores the selected id, but no catalog
|
||||
// adapter exists to provide its presentation name.
|
||||
name: '选择模型,当前 deepseek-v4-flash',
|
||||
name: 'Select model, current deepseek-v4-flash',
|
||||
}).waitFor({ timeout: 10_000 })
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('matches the Figma context disclosure geometry', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-context-injection'))
|
||||
const disclosure = page.getByRole('button', { name: 'Context injection' })
|
||||
expect(await disclosure.getAttribute('aria-expanded')).toBe('false')
|
||||
const collapsedIcon = disclosure.locator('svg').first()
|
||||
const collapsedIconBox = await collapsedIcon.boundingBox()
|
||||
expect(collapsedIconBox?.width).toBe(14)
|
||||
expect(collapsedIconBox?.height).toBe(14)
|
||||
|
||||
await disclosure.click()
|
||||
await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('true')
|
||||
const body = page.locator('[data-context-injection-body]')
|
||||
await body.waitFor({ timeout: 5_000 })
|
||||
const headerBox = await disclosure.boundingBox()
|
||||
const bodyBox = await body.boundingBox()
|
||||
if (headerBox === null || bodyBox === null) throw new Error('context disclosure geometry is not measurable')
|
||||
expect(headerBox.height).toBe(24)
|
||||
expect(bodyBox.x - headerBox.x).toBe(22)
|
||||
expect(bodyBox.y - headerBox.y - headerBox.height).toBe(4)
|
||||
expect(bodyBox.height).toBe(141)
|
||||
|
||||
const style = await body.evaluate((element) => {
|
||||
const computed = getComputedStyle(element)
|
||||
return {
|
||||
backgroundColor: computed.backgroundColor,
|
||||
borderRadius: computed.borderRadius,
|
||||
color: computed.color,
|
||||
fontSize: computed.fontSize,
|
||||
lineHeight: computed.lineHeight,
|
||||
padding: [
|
||||
computed.paddingTop,
|
||||
computed.paddingRight,
|
||||
computed.paddingBottom,
|
||||
computed.paddingLeft,
|
||||
],
|
||||
scrolls: element.scrollHeight > element.clientHeight,
|
||||
}
|
||||
})
|
||||
expect(style).toEqual({
|
||||
backgroundColor: 'rgb(249, 250, 251)',
|
||||
borderRadius: '8px',
|
||||
color: 'rgb(129, 133, 140)',
|
||||
fontSize: '11px',
|
||||
lineHeight: '16px',
|
||||
padding: ['10px', '16px', '12px', '12px'],
|
||||
scrolls: true,
|
||||
})
|
||||
|
||||
await disclosure.click()
|
||||
await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('false')
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('file-path tool rows rebuilt from the cold log stay details-inert', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-toolrow'))
|
||||
// Interaction over cold-resumed history: read summaries are host-open
|
||||
@@ -140,18 +223,40 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
const fileLink = page.locator('[data-variant="read"] button').first()
|
||||
await fileLink.waitFor({ timeout: 10_000 })
|
||||
const frame = page.locator('[style*="grid-template-columns"]').first()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBeNull()
|
||||
expect(await frame.getAttribute('data-details-collapsed')).toBe('true')
|
||||
await fileLink.click()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBeNull()
|
||||
await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true')
|
||||
// Path label survives from the recorded args (a.txt).
|
||||
await expect.poll(() => page.getByText('a.txt', { exact: false }).count(), { timeout: 5_000 }).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('an Access-chip switch lands one command row: bare name, non-repeating settlement text', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-command-row'))
|
||||
// The Access chip submits `/permission <preset>` — a host command with no
|
||||
// model call, so the settled row renders keylessly over this cold history.
|
||||
// The row copy is the assertion: `permission · preset workspace-write`,
|
||||
// where neither half repeats the other (the dispatched `/` and its
|
||||
// argument stay out of the title, and the settlement text never restates
|
||||
// the command's own name).
|
||||
await page.getByRole('button', { name: 'Access mode, current: Danger Full Access' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Workspace Write' }).click()
|
||||
await page.getByRole('button', { name: 'Access mode, current: Workspace Write' }).waitFor({ timeout: 10_000 })
|
||||
// Scoped to the row itself, so unrelated page text that happens to read
|
||||
// `permission` (a future resident slash menu) cannot satisfy or break it.
|
||||
const row = page.locator('[data-variant="others"]').filter({ hasText: 'preset workspace-write' })
|
||||
await expect.poll(() => row.count(), { timeout: 10_000 }).toBe(1)
|
||||
expect(await row.getByText('permission', { exact: true }).count()).toBe(1)
|
||||
expect(await row.getByText('/permission workspace-write', { exact: true }).count()).toBe(0)
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
.split(SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(COMMAND_ROW_EXPECTED, snapshot, MODE)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('issued zero model calls and stayed clean', async () => {
|
||||
// No replay fixture was installed and the llm seam is open — any stray
|
||||
// stream would have failed the turn loudly. Cleanliness pins the wire.
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['seed.jsonl', 'ui.expected.md'])
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['command-row.expected.md', 'seed.jsonl', 'ui.expected.md'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('web e2e: settings modal, appearance gesture, language switch', () => {
|
||||
// Golden of the freshly opened dialog (default zh, General active).
|
||||
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(DIALOG_EXPECTED, snapshot, MODE)
|
||||
// Section switch: aria-current moves; Models is deliberately empty.
|
||||
// Section switch: aria-current moves (the Models page itself has its own scenario file).
|
||||
await dialog.getByRole('button', { name: '模型' }).click()
|
||||
await expect.poll(() => dialog.getByRole('button', { name: '模型' }).getAttribute('aria-current'), { timeout: 5_000 }).toBe('true')
|
||||
expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBeNull()
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
assertFixtureInventory, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/sidebar-scrollbar', import.meta.url))
|
||||
@@ -275,7 +275,7 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
||||
browser = await chromium.launch()
|
||||
// Shorter than the other scenarios' 1000px so SEED_COUNT rows overflow
|
||||
// the list with room to spare.
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 800 } })
|
||||
page = await newEnglishPage(browser, 800)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/skill-invocation-policy', import.meta.url))
|
||||
const MENU_EXPECTED = join(SNAPSHOT_DIR, 'menu.expected.md')
|
||||
@@ -80,7 +80,7 @@ describe('web e2e: skill invocation policy through the real host', () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
await seedSkills(scaffold.workspaceCwd)
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
|
||||
@@ -24,7 +24,7 @@ import { pathToFileURL } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { REPO_ROOT, connectFreshWorkspace, probeFreePort, requireDist, saveFailureShot } from './support.ts'
|
||||
import { REPO_ROOT, connectFreshWorkspace, newEnglishPage, probeFreePort, requireDist, saveFailureShot } from './support.ts'
|
||||
|
||||
function waitForReadyLine(child: ChildProcess): Promise<string> {
|
||||
return new Promise((resolveReady, reject) => {
|
||||
@@ -376,7 +376,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
)
|
||||
baseUrl = (await waitForReadyLine(child)).replace('0.0.0.0', '127.0.0.1')
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
||||
page = await newEnglishPage(browser)
|
||||
page.on('pageerror', e => pageErrors.push(String(e)))
|
||||
await page.goto(baseUrl, { waitUntil: 'load' })
|
||||
}, 120_000)
|
||||
@@ -454,7 +454,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
await screen(page, '07-back-to-chat')
|
||||
})
|
||||
|
||||
it('5 bash differential rendering: tool row click leaves the default details column open', async () => {
|
||||
it('5 bash differential rendering: tool row click leaves the default details column closed', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'w5-tool-details'))
|
||||
const input = page.locator('textarea').first()
|
||||
await input.fill('请用 bash 工具运行命令 echo w5marker 然后告诉我结果')
|
||||
@@ -466,11 +466,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
const toolRow = page.locator('[data-sample="bash-global"]')
|
||||
await toolRow.waitFor({ timeout: 120_000 })
|
||||
await screen(page, '08-bash-round')
|
||||
expect(await detailsTrack(page)).toBe(360)
|
||||
expect(await detailsTrack(page)).toBe(0)
|
||||
await toolRow.click()
|
||||
// Tool rows no longer drive layout.openDetails; the default column stays open.
|
||||
expect(await detailsTrack(page)).toBe(360)
|
||||
await screen(page, '09-details-open')
|
||||
// Tool rows no longer drive layout.openDetails; the default column stays closed.
|
||||
expect(await detailsTrack(page)).toBe(0)
|
||||
await screen(page, '09-details-closed')
|
||||
}, 150_000)
|
||||
|
||||
it('6 sidebar drag widens the column and resets across reload', async () => {
|
||||
|
||||
64
apps/web/tests/snapshots/approval-composer/session.jsonl
Normal file
64
apps/web/tests/snapshots/approval-composer/session.jsonl
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
- text: Waiting for approval
|
||||
- group "Approval details": "escalate sandbox to workspace-write: Need to write the notes.txt file as requested by the user. echo 'tok63z tokc7y tokibx tokofw tokujv tok10nu tok16rt tok1cvs tok1izr tok1p3q tok1v7p tok21bo tok2a4 tok8e3 tokei2 tokkm1 tokqq0 tokwtz tok12xy tok191x tok1f5w tok1l9v tok1rdu tok1xht tok23ls tok4k8 tokao7 tokgs6 tokmw5 tokt04 tokz43 tok1582 tok1bc1 tok1hg0 tok1njz tok1tny tok1zrx tokqd tok6uc tokcyb tokj2a tokp69 tokva8 tok11e7 tok17i6 tok1dm5 tok1jq4 tok1pu3 tok1vy2 tok2221 tok30h tok94g tokf8f toklce tokrgd tokxkc tok13ob tok19sa tok1fw9 tok1m08 tok1s47 tok1y86 tok24c5 tok5al tokbek tokhij toknmi toktqh tokzug tok15yf tok1c2e tok1i6d tok1oac tok1ueb tok20ia tok1gq tok7kp tokdoo tokjsn tokpwm tokw0l tok124k tok188j tok1eci tok1kgh tok1qkg tok1wof tok22se tok3qu tok9ut tokfys tokm2r toks6q tokyap tok14eo tok1ain tok1gmm tok1mql tok1suk tok1yyj tok252i tok60y tokc4x toki8w tokocv tokugu tok10kt tok16os tok1csr tok1iwq tok1p0p tok1v4o tok218n tok273 tok8b2 tokef1 tokkj0 tokqmz tokwqy tok12ux tok18yw tok1f2v tok1l6u tok1rat tok1xes tok23ir tok4h7 tokal6 tokgp5 tokmt4 toksx3 tokz12 tok1551 tok1b90 tok1hcz tok1ngy tok1tkx tok1zow toknc tok6rb tokcva tokiz9 tokp38 tokv77 tok11b6 tok17f5 tok1dj4 tok1jn3 tok1pr2 tok1vv1 tok21z0 tok2xg tok91f tokf5e tokl9d tokrdc tokxhb tok13la tok19p9 tok1ft8 tok1lx7 tok1s16 tok1y55 tok2494 tok57k tokbbj tokhfi toknjh tokktng tokzrf tok15ve tok1bzd tok1i3c tok1o7b tok1uba tok20f9 tok1dp tok7ho tokdln tokjpm tokptl tokvxk tok121j tok185i tok1e9h tok1kdg tok1qhf tok1wle tok22pd tok3nt tok9rs tokfvr toklzq toks3p toky7o tok14bn tok1afm tok1gjl tok1mnk tok1srj tok1yvi tok24zh tok5xx tokc1w toki5v toko9u tokudt tok10hs tok16lr tok1cpq tok1itp tok1oxo tok1v1n tok215m tok242 tok881 tokec0 tokfz tokqjy tokwnx' > notes.txt"
|
||||
- button "Reject"
|
||||
- button "Allow once"
|
||||
@@ -1,9 +1,9 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785013630399,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785013630411,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785013630411,"data":{"content":[{"type":"text","text":"Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"1c4a6a0f-a7bd-4642-919d-6ed7395df98b"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785013630411,"data":{"content":[{"type":"text","text":"Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785013630418,"data":{"title":"Using ONE run_code program: run","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785013630479,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785013630480,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"request/header","seq":4,"time":1785013630480,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785013631481,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1785013631481,"data":{"turn":1,"step":1,"index":0,"dt":[182,27,0,0,1,39,1,0,11,0,0,1,0,25,0,1,0,0,25,28,0,0,0,26,1,0,0,0,0,25,1,0,0,0,0,28,1,0,0,0,23,1,0,1,0,0,25,1,0,0,0,26,0,0,0,26,0,26,0,0,1,25,1,0,0,0,0,25,1,26,0,1,26,29],"texts":["The"," user"," wants"," me"," to"," write"," a"," single"," `","run","_code","`"," program"," that",":\n","1","."," Runs"," bash"," to"," echo"," \"","CODE","_RO","UND","_OK","\"\n","2","."," T","ries"," to"," read"," a"," file"," \"","missing",".txt","\""," and"," catches"," the"," error","\n","3","."," Returns"," an"," object"," with"," both"," outcomes","\n","4","."," They"," also"," want"," me"," to"," reply"," \"","D","ONE","\""," and"," stop"," after","\n\n","Let"," me"," write"," this"," program","."]}}
|
||||
{"type":"assistant/chunk","seq":81,"time":1785013632219,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
@@ -12,13 +12,13 @@
|
||||
{"type":"assistant/chunk","seq":202,"time":1785013633103,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":203,"time":1785013633104,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":8082,"outputTokens":235,"cacheReadTokens":384,"reasoningTokens":75}}}}
|
||||
{"type":"assistant/chunk","seq":204,"time":1785013633104,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":205,"time":1785013633108,"data":{"turn":1,"step":1,"usage":{"inputTokens":8082,"outputTokens":235,"cacheReadTokens":384,"reasoningTokens":75},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to write a single `run_code` program that:\n1. Runs bash to echo \"CODE_ROUND_OK\"\n2. Tries to read a file \"missing.txt\" and catches the error\n3. Returns an object with both outcomes\n4. They also want me to reply \"DONE\" and stop after\n\nLet me write this program."},{"type":"tool-call","id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"0d217af9-3bb3-425b-b53e-7ab0366f0c66"}},"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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":205,"time":1785013633108,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to write a single `run_code` program that:\n1. Runs bash to echo \"CODE_ROUND_OK\"\n2. Tries to read a file \"missing.txt\" and catches the error\n3. Returns an object with both outcomes\n4. They also want me to reply \"DONE\" and stop after\n\nLet me write this program."},{"type":"tool-call","id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":8082,"outputTokens":235,"cacheReadTokens":384,"reasoningTokens":75}},"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],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":206,"time":1785013633108,"data":{"turn":1,"step":1,"callId":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}}
|
||||
{"type":"tool/code-dispatch-start","seq":207,"time":1785013633173,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"}}}
|
||||
{"type":"tool/code-dispatch","seq":208,"time":1785013633196,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"},"isError":false,"content":[{"type":"text","text":"CODE_ROUND_OK\n"}]}}
|
||||
{"type":"tool/code-dispatch-start","seq":209,"time":1785013633197,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"}}}
|
||||
{"type":"tool/code-dispatch","seq":210,"time":1785013633198,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"},"isError":true,"content":[{"type":"text","text":"Error: cannot read \"{{cwd}}/workspace/missing.txt\": not found"}]}}
|
||||
{"type":"tool/result","seq":211,"time":1785013633201,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_6VNoF1gDSerTBKoCfYSH3765"},"content":[{"type":"tool-result","toolCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","content":[{"type":"text","text":"{\n \"bash\": \"CODE_ROUND_OK\",\n \"readError\": {\n \"toolName\": \"read\",\n \"message\": \"cannot read \\\"{{cwd}}/workspace/missing.txt\\\": not found\"\n }\n}"}],"isError":false}],"role":"user","id":"1580d4a8-5760-415f-984f-f93927271d3f"}},"sourceEventSeqs":[206],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":211,"time":1785013633201,"data":{"turn":1,"step":1,"callId":"call_00_6VNoF1gDSerTBKoCfYSH3765","content":[{"type":"text","text":"{\n \"bash\": \"CODE_ROUND_OK\",\n \"readError\": {\n \"toolName\": \"read\",\n \"message\": \"cannot read \\\"{{cwd}}/workspace/missing.txt\\\": not found\"\n }\n}"}],"isError":false},"sourceEventSeqs":[206],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":212,"time":1785013633204,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":213,"time":1785013633207,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":214,"time":1785013633985,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -30,6 +30,6 @@
|
||||
{"type":"assistant/chunk","seq":233,"time":1785013634223,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":234,"time":1785013634223,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":196,"outputTokens":17,"cacheReadTokens":8576,"reasoningTokens":14}}}}
|
||||
{"type":"assistant/chunk","seq":235,"time":1785013634223,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":236,"time":1785013634224,"data":{"turn":1,"step":2,"usage":{"inputTokens":196,"outputTokens":17,"cacheReadTokens":8576,"reasoningTokens":14},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The program ran successfully. Let me now reply DONE as instructed."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"3037ec34-6c2a-4e01-91eb-a0ef89ce0d15"}},"sourceEventSeqs":[214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":236,"time":1785013634224,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The program ran successfully. Let me now reply DONE as instructed."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":196,"outputTokens":17,"cacheReadTokens":8576,"reasoningTokens":14}},"sourceEventSeqs":[214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":237,"time":1785013634225,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":238,"time":1785013634225,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -5,48 +5,42 @@
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: "Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "编辑":
|
||||
- button "Edit":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- 'button "Think The user wants me to write a single `run_code` program that:"':
|
||||
- img
|
||||
- img
|
||||
- text: "Think The user wants me to write a single `run_code` program that:"
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button:
|
||||
- button "Code Run bash echo and catch missing file read":
|
||||
- img
|
||||
- img
|
||||
- text: Code Run bash echo and catch missing file read
|
||||
- text: Code Run bash echo and catch missing file read
|
||||
- img
|
||||
- text: Bash Echo CODE_ROUND_OK Read
|
||||
- button "missing.txt"
|
||||
- text: Bash Echo CODE_ROUND_OK
|
||||
- 'button "Read Error: cannot read \"{{cwd}}/workspace/missing.txt\": not found"':
|
||||
- img
|
||||
- text: "Read Error: cannot read \"{{cwd}}/workspace/missing.txt\": not found"
|
||||
- button "Think The program ran successfully. Let me now reply DONE as instructed.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The program ran successfully. Let me now reply DONE as instructed.
|
||||
- paragraph: DONE
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} cache hit 52% · 17,490 tokens · 1 turns · 2 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 52% Input 17.2K tok · Output 252 tok
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785157562825,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785157562881,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785157562882,"data":{"content":[{"type":"text","text":"Use only Cordis tools. First call cordis_inspect with what \"temporary\". Then call cordis_mount with this exact code: \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\". Read its returned id and call cordis_unmount with that exact id. After all three calls succeed, reply exactly CORDIS_UI_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"27e1fc72-20d0-4875-bf5b-d34750441f30"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785157562882,"data":{"content":[{"type":"text","text":"Use only Cordis tools. First call cordis_inspect with what \"temporary\". Then call cordis_mount with this exact code: \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\". Read its returned id and call cordis_unmount with that exact id. After all three calls succeed, reply exactly CORDIS_UI_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785157562883,"data":{"title":"Use only Cordis tools. First","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785157562937,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785157562938,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash","reasoningEffort":"high"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"request/header","seq":4,"time":1785157562938,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"high"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785157564667,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1785157564667,"data":{"turn":1,"step":1,"index":0,"dt":[115,31,3,0,1,0,1,21,1,0,0,0,1,23,0,1,0,0,0,24,2,1,0,0,0,28,2,0,0,0,1,23,2,22,2,1,25,2,0,0,25,27,1,1,0,0,28,2,0,0,0,0,32,1,31,1,0,0,0,9,29,3,0,0,0,1,23,1,0,0,0,27,4,0,0,0,23,2,0,0],"texts":["The"," user"," wants"," me"," to",":\n","1","."," Call"," `","cord","is","_in","spect","`"," with"," `","what",":"," \"","t","emporary","\"`\n","2","."," Call"," `","cord","is","_m","ount","`"," with"," the"," exact"," code"," provided","\n","3","."," Read"," the"," returned"," id"," and"," call"," `","cord","is","_un","mount","`"," with"," that"," exact"," id","\n","4","."," Reply"," exactly"," \"","C","ORD","IS","_","UI","_D","ONE","\""," and"," stop","\n\n","Let"," me"," start"," with"," step"," ","1","."]}}
|
||||
{"type":"assistant/chunk","seq":87,"time":1785157565360,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
@@ -12,9 +12,9 @@
|
||||
{"type":"assistant/chunk","seq":100,"time":1785157565491,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_KZk918WtlKan9pHMULIT8794","name":"cordis_inspect","arguments":"{\"what\": \"temporary\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":101,"time":1785157565491,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":15137,"outputTokens":128,"cacheReadTokens":1280,"reasoningTokens":81}}}}
|
||||
{"type":"assistant/chunk","seq":102,"time":1785157565491,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":103,"time":1785157565495,"data":{"turn":1,"step":1,"usage":{"inputTokens":15137,"outputTokens":128,"cacheReadTokens":1280,"reasoningTokens":81},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to:\n1. Call `cordis_inspect` with `what: \"temporary\"`\n2. Call `cordis_mount` with the exact code provided\n3. Read the returned id and call `cordis_unmount` with that exact id\n4. Reply exactly \"CORDIS_UI_DONE\" and stop\n\nLet me start with step 1."},{"type":"tool-call","id":"call_00_KZk918WtlKan9pHMULIT8794","name":"cordis_inspect","arguments":"{\"what\": \"temporary\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"a3b56157-c6fd-43f8-bc8a-0e2a0c99a8b5"}},"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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":103,"time":1785157565495,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to:\n1. Call `cordis_inspect` with `what: \"temporary\"`\n2. Call `cordis_mount` with the exact code provided\n3. Read the returned id and call `cordis_unmount` with that exact id\n4. Reply exactly \"CORDIS_UI_DONE\" and stop\n\nLet me start with step 1."},{"type":"tool-call","id":"call_00_KZk918WtlKan9pHMULIT8794","name":"cordis_inspect","arguments":"{\"what\": \"temporary\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":15137,"outputTokens":128,"cacheReadTokens":1280,"reasoningTokens":81}},"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],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":104,"time":1785157565496,"data":{"turn":1,"step":1,"callId":"call_00_KZk918WtlKan9pHMULIT8794","name":"cordis_inspect","arguments":"{\"what\": \"temporary\"}"}}
|
||||
{"type":"tool/result","seq":105,"time":1785157565500,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_KZk918WtlKan9pHMULIT8794"},"content":[{"type":"tool-result","toolCallId":"call_00_KZk918WtlKan9pHMULIT8794","content":[{"type":"text","text":"## Temporary Plugins\nNo temporary Plugins are running. Temporary Plugins created with cordis_mount disappear when DSH restarts."}],"isError":false}],"role":"user","id":"95ae124e-f065-4aab-a9c5-bd33c7eafff1"}},"sourceEventSeqs":[104],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":105,"time":1785157565500,"data":{"turn":1,"step":1,"callId":"call_00_KZk918WtlKan9pHMULIT8794","content":[{"type":"text","text":"## Temporary Plugins\nNo temporary Plugins are running. Temporary Plugins created with cordis_mount disappear when DSH restarts."}],"isError":false},"sourceEventSeqs":[104],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":106,"time":1785157565503,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":107,"time":1785157565503,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":108,"time":1785157566524,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -25,9 +25,9 @@
|
||||
{"type":"assistant/chunk","seq":157,"time":1785157567041,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_OyrlxqqpnvzTe11sN5Oy2361","name":"cordis_mount","arguments":"{\"code\": \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":158,"time":1785157567042,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":72,"outputTokens":82,"cacheReadTokens":16512,"reasoningTokens":22}}}}
|
||||
{"type":"assistant/chunk","seq":159,"time":1785157567042,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":160,"time":1785157567043,"data":{"turn":1,"step":2,"usage":{"inputTokens":72,"outputTokens":82,"cacheReadTokens":16512,"reasoningTokens":22},"message":{"role":"assistant","content":[{"type":"reasoning","text":"Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."},{"type":"tool-call","id":"call_00_OyrlxqqpnvzTe11sN5Oy2361","name":"cordis_mount","arguments":"{\"code\": \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"b90af675-f604-4fb4-9c27-89c91b27f4ce"}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":160,"time":1785157567043,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."},{"type":"tool-call","id":"call_00_OyrlxqqpnvzTe11sN5Oy2361","name":"cordis_mount","arguments":"{\"code\": \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":72,"outputTokens":82,"cacheReadTokens":16512,"reasoningTokens":22}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":161,"time":1785157567043,"data":{"turn":1,"step":2,"callId":"call_00_OyrlxqqpnvzTe11sN5Oy2361","name":"cordis_mount","arguments":"{\"code\": \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\"}"}}
|
||||
{"type":"tool/result","seq":162,"time":1785157567049,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_OyrlxqqpnvzTe11sN5Oy2361"},"content":[{"type":"tool-result","toolCallId":"call_00_OyrlxqqpnvzTe11sN5Oy2361","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"snapshot-noop\"; available until unmounted or DSH restarts)."}],"isError":false}],"role":"user","id":"3148fc0a-f511-4b72-abd5-d184e974cd49"}},"sourceEventSeqs":[161],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":162,"time":1785157567049,"data":{"turn":1,"step":2,"callId":"call_00_OyrlxqqpnvzTe11sN5Oy2361","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"snapshot-noop\"; available until unmounted or DSH restarts)."}],"isError":false},"sourceEventSeqs":[161],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":163,"time":1785157567050,"data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","seq":164,"time":1785157567050,"data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/chunk","seq":165,"time":1785157567835,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -38,9 +38,9 @@
|
||||
{"type":"assistant/chunk","seq":204,"time":1785157568279,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_e38S6zeYdZGvbhecUCil6659","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":205,"time":1785157568279,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":63,"outputTokens":72,"cacheReadTokens":16640,"reasoningTokens":23}}}}
|
||||
{"type":"assistant/chunk","seq":206,"time":1785157568279,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":207,"time":1785157568280,"data":{"turn":1,"step":3,"usage":{"inputTokens":63,"outputTokens":72,"cacheReadTokens":16640,"reasoningTokens":23},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."},{"type":"tool-call","id":"call_00_e38S6zeYdZGvbhecUCil6659","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"cdc5b840-4302-486a-8065-fa59bf13f2d9"}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":207,"time":1785157568280,"data":{"turn":1,"step":3,"content":[{"type":"reasoning","text":"The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."},{"type":"tool-call","id":"call_00_e38S6zeYdZGvbhecUCil6659","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":63,"outputTokens":72,"cacheReadTokens":16640,"reasoningTokens":23}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":208,"time":1785157568280,"data":{"turn":1,"step":3,"callId":"call_00_e38S6zeYdZGvbhecUCil6659","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}
|
||||
{"type":"tool/result","seq":209,"time":1785157568281,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_00_e38S6zeYdZGvbhecUCil6659"},"content":[{"type":"tool-result","toolCallId":"call_00_e38S6zeYdZGvbhecUCil6659","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false}],"role":"user","id":"758a7b27-56e3-4997-8c4d-effc699cc5e6"}},"sourceEventSeqs":[208],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":209,"time":1785157568281,"data":{"turn":1,"step":3,"callId":"call_00_e38S6zeYdZGvbhecUCil6659","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false},"sourceEventSeqs":[208],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":210,"time":1785157568282,"data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","seq":211,"time":1785157568282,"data":{"turn":1,"step":4}}
|
||||
{"type":"assistant/chunk","seq":212,"time":1785157569185,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -51,6 +51,6 @@
|
||||
{"type":"assistant/chunk","seq":244,"time":1785157569551,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CORDIS_UI_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":245,"time":1785157569551,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":29,"outputTokens":30,"cacheReadTokens":16768,"reasoningTokens":22}}}}
|
||||
{"type":"assistant/chunk","seq":246,"time":1785157569551,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":247,"time":1785157569553,"data":{"turn":1,"step":4,"usage":{"inputTokens":29,"outputTokens":30,"cacheReadTokens":16768,"reasoningTokens":22},"message":{"role":"assistant","content":[{"type":"reasoning","text":"All three calls succeeded. I should now reply exactly \"CORDIS_UI_DONE\" and stop."},{"type":"text","text":"CORDIS_UI_DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"71540121-7bf6-410e-975c-f5ea99d8175a"}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":247,"time":1785157569553,"data":{"turn":1,"step":4,"content":[{"type":"reasoning","text":"All three calls succeeded. I should now reply exactly \"CORDIS_UI_DONE\" and stop."},{"type":"text","text":"CORDIS_UI_DONE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":29,"outputTokens":30,"cacheReadTokens":16768,"reasoningTokens":22}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":248,"time":1785157569554,"data":{"turn":1,"step":4}}
|
||||
{"type":"turn/end","seq":249,"time":1785157569554,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -5,72 +5,57 @@
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: "Use only Cordis tools. First call cordis_inspect with what \"temporary\". Then call cordis_mount with this exact code: \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\". Read its returned id and call cordis_unmount with that exact id. After all three calls succeed, reply exactly CORDIS_UI_DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "编辑":
|
||||
- button "Edit":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to:":
|
||||
- img
|
||||
- img
|
||||
- text: "Think The user wants me to:"
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button:
|
||||
- button "Inspect temporary":
|
||||
- img
|
||||
- img
|
||||
- text: Inspect temporary
|
||||
- text: Inspect temporary
|
||||
- 'button "Think Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."':
|
||||
- img
|
||||
- img
|
||||
- text: "Think Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."
|
||||
- button "复制":
|
||||
- 'button "Mount temporary Plugin return { name: \"snapshot-noop\", apply(ctx) {} }" [expanded]':
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button [expanded]:
|
||||
- img
|
||||
- text: Mount temporary Plugin typescript
|
||||
- button "复制"
|
||||
- text: "Mount temporary Plugin return { name: \"snapshot-noop\", apply(ctx) {} }"
|
||||
- text: typescript
|
||||
- button "Copy"
|
||||
- code: "return { name: \"snapshot-noop\", apply(ctx) {} }"
|
||||
- text: OUT Temporary Plugin dyn-1 is running (plugin "snapshot-noop"; available until unmounted or DSH restarts).
|
||||
- button "Inspect"
|
||||
- 'button "Think The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."':
|
||||
- img
|
||||
- img
|
||||
- text: "Think The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- button:
|
||||
- button "Unmount temporary Plugin dyn-1":
|
||||
- img
|
||||
- img
|
||||
- text: Unmount temporary Plugin dyn-1
|
||||
- text: Unmount temporary Plugin dyn-1
|
||||
- button "Think All three calls succeeded. I should now reply exactly \"CORDIS_UI_DONE\" and stop.":
|
||||
- img
|
||||
- img
|
||||
- text: Think All three calls succeeded. I should now reply exactly "CORDIS_UI_DONE" and stop.
|
||||
- paragraph: CORDIS_UI_DONE
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} cache hit 77% · 66,813 tokens · 1 turns · 4 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 4 steps Tool call {{duration}} Cache hit 77% Input 66.5K tok · Output 312 tok
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# AppFrame drag handles
|
||||
|
||||
## sidebar
|
||||
|
||||
- hit strip present: true
|
||||
- cursor: col-resize
|
||||
- pill generated: false
|
||||
@@ -1,9 +1,9 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1784973850091,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1784973850102,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1784973850103,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"2c594e5a-bbcc-4c64-b5ee-8e84eb5dd949"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1784973850103,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1784973850105,"data":{"title":"Use the bash tool to","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1784973850164,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1784973850165,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"request/header","seq":4,"time":1784973850165,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1784973850888,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1784973850889,"data":{"turn":1,"step":1,"index":0,"dt":[199,1,0,0,0,18,1,0,0,0,0,27,0,1,0,0],"texts":["The"," user"," wants"," me"," to"," run"," a"," simple"," bash"," command"," and"," reply"," with"," \"","D","ONE","\"."]}}
|
||||
{"type":"assistant/chunk","seq":23,"time":1784973851217,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
@@ -12,9 +12,9 @@
|
||||
{"type":"assistant/chunk","seq":52,"time":1784973851493,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_BYXlxjFaalMg95YVqEeF2495","name":"bash","arguments":"{\"command\": \"echo WEB_E2E_OK\", \"description\": \"Echo the test string\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":53,"time":1784973851493,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":122,"outputTokens":85,"cacheReadTokens":7680,"reasoningTokens":17}}}}
|
||||
{"type":"assistant/chunk","seq":54,"time":1784973851493,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":55,"time":1784973851498,"data":{"turn":1,"step":1,"usage":{"inputTokens":122,"outputTokens":85,"cacheReadTokens":7680,"reasoningTokens":17},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to run a simple bash command and reply with \"DONE\"."},{"type":"tool-call","id":"call_00_BYXlxjFaalMg95YVqEeF2495","name":"bash","arguments":"{\"command\": \"echo WEB_E2E_OK\", \"description\": \"Echo the test string\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"e5d8d5fb-f555-4fda-948f-9ab178145929"}},"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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":55,"time":1784973851498,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a simple bash command and reply with \"DONE\"."},{"type":"tool-call","id":"call_00_BYXlxjFaalMg95YVqEeF2495","name":"bash","arguments":"{\"command\": \"echo WEB_E2E_OK\", \"description\": \"Echo the test string\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":122,"outputTokens":85,"cacheReadTokens":7680,"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,50,51,52,53,54],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":56,"time":1784973851499,"data":{"turn":1,"step":1,"callId":"call_00_BYXlxjFaalMg95YVqEeF2495","name":"bash","arguments":"{\"command\": \"echo WEB_E2E_OK\", \"description\": \"Echo the test string\"}"}}
|
||||
{"type":"tool/result","seq":57,"time":1784973851515,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_BYXlxjFaalMg95YVqEeF2495"},"content":[{"type":"tool-result","toolCallId":"call_00_BYXlxjFaalMg95YVqEeF2495","content":[{"type":"text","text":"WEB_E2E_OK\n"}],"isError":false}],"role":"user","id":"215febf9-ff3c-42b8-93b7-27ce1505c1c2"}},"sourceEventSeqs":[56],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":57,"time":1784973851515,"data":{"turn":1,"step":1,"callId":"call_00_BYXlxjFaalMg95YVqEeF2495","content":[{"type":"text","text":"WEB_E2E_OK\n"}],"isError":false},"sourceEventSeqs":[56],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":58,"time":1784973851517,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":59,"time":1784973851518,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":60,"time":1784973852194,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -26,6 +26,6 @@
|
||||
{"type":"assistant/chunk","seq":88,"time":1784973852460,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":89,"time":1784973852460,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":97,"outputTokens":26,"cacheReadTokens":7808,"reasoningTokens":23}}}}
|
||||
{"type":"assistant/chunk","seq":90,"time":1784973852460,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":91,"time":1784973852461,"data":{"turn":1,"step":2,"usage":{"inputTokens":97,"outputTokens":26,"cacheReadTokens":7808,"reasoningTokens":23},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\"."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"61e83a98-18a9-4f1f-9151-c8783cc39901"}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":91,"time":1784973852461,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\"."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":97,"outputTokens":26,"cacheReadTokens":7808,"reasoningTokens":23}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":92,"time":1784973852461,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":93,"time":1784973852462,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -5,43 +5,37 @@
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: "Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "编辑":
|
||||
- button "Edit":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to run a simple bash command and reply with \"DONE\".":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to run a simple bash command and reply with "DONE".
|
||||
- button "复制":
|
||||
- button "Bash Echo the test string":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- img
|
||||
- text: Bash Echo the test string
|
||||
- text: Bash Echo the test string
|
||||
- button "Think The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\".":
|
||||
- img
|
||||
- img
|
||||
- text: Think The command executed successfully and output "WEB_E2E_OK". I just need to reply with "DONE".
|
||||
- paragraph: DONE
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} cache hit 99% · 15,818 tokens · 1 turns · 2 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 99% Input 15.7K tok · Output 111 tok
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
- img
|
||||
- text: workspace 1 session
|
||||
- treeitem "New Session now" [selected]
|
||||
- button "设置":
|
||||
- button "Settings":
|
||||
- img
|
||||
- text: 设置
|
||||
- text: Settings
|
||||
- text: Let's start building
|
||||
- button "Choose workspace":
|
||||
- img
|
||||
@@ -28,15 +28,12 @@
|
||||
- textbox "Describe what you want to build"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 详情
|
||||
- button "关闭详情"
|
||||
- text: 点击消息流中的工具行查看详情
|
||||
- text: Details
|
||||
- button "Close details"
|
||||
- text: Click a tool row in the message flow to view its details
|
||||
|
||||
@@ -5,32 +5,29 @@
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Reply with the single word LIGHTHOUSE and stop. {{clock}}
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "编辑":
|
||||
- button "Edit":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to reply with a single word. Let me comply.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to reply with a single word. Let me comply.
|
||||
- paragraph: LIGHTHOUSE
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} cache hit 99% · 7,810 tokens · 1 turns · 1 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps Cache hit 99% Input 7.8K tok · Output 21 tok
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785015039278,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785015039291,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785015039292,"data":{"content":[{"type":"text","text":"Reply with the single word LIGHTHOUSE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"ee28d2ee-08b2-4ed7-a1e9-84865f55e2af"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785015039292,"data":{"content":[{"type":"text","text":"Reply with the single word LIGHTHOUSE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785015039294,"data":{"title":"Reply with the single word","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785015039362,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785015039363,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"request/header","seq":4,"time":1785015039363,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785015039930,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1785015039930,"data":{"turn":1,"step":1,"index":0,"dt":[162,28,1,0,0,46,1,0,0,0,11,0,0,30],"texts":["The"," user"," wants"," me"," to"," reply"," with"," a"," single"," word","."," Let"," me"," comply","."]}}
|
||||
{"type":"assistant/chunk","seq":21,"time":1785015040209,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
@@ -12,6 +12,6 @@
|
||||
{"type":"assistant/chunk","seq":28,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"LIGHTHOUSE"}}}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}}}}
|
||||
{"type":"assistant/chunk","seq":30,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":31,"time":1785015040244,"data":{"turn":1,"step":1,"usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."},{"type":"text","text":"LIGHTHOUSE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"2101eadc-3475-4c77-ae0a-0107b12c34bc"}},"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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":31,"time":1785015040244,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."},{"type":"text","text":"LIGHTHOUSE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}},"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],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":32,"time":1785015040246,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":33,"time":1785015040247,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -5,29 +5,26 @@
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "编辑":
|
||||
- button "Edit":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- paragraph: partial
|
||||
- text: 已停止
|
||||
- button "复制":
|
||||
- text: Stopped
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} 0 tokens · 1 turns · 1 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps Input 0 tok · Output 0 tok
|
||||
|
||||
@@ -5,22 +5,18 @@
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "编辑":
|
||||
- button "Edit":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -5,32 +5,29 @@
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "编辑":
|
||||
- button "Edit":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.
|
||||
- paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures.
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} cache hit 99% · 7,869 tokens · 1 turns · 1 steps
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps Cache hit 99% Input 7.8K tok · Output 79 tok
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1784998084441,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1784998084454,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1784998084454,"data":{"content":[{"type":"text","text":"Reply with a one-sentence description of event sourcing, then stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"dc9a43b2-63a3-49bc-97e9-9dfb6465f6c1"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1784998084454,"data":{"content":[{"type":"text","text":"Reply with a one-sentence description of event sourcing, then stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1784998084457,"data":{"title":"Reply with a one-sentence description","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1784998084519,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1784998084520,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"request/header","seq":4,"time":1784998084520,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1784998084900,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1784998084900,"data":{"turn":1,"step":1,"index":0,"dt":[153,3,0,29,1,0,0,28,0,1,0,0,0,28,0,0,0,29,1,0,29,0,0,29,0,0,36,0,21,29],"texts":["The"," user"," is"," asking"," for"," a"," one","-s","entence"," description"," of"," event"," sourcing","."," This"," is"," a"," straightforward"," knowledge"," question"," that"," doesn","'t"," require"," any"," skill"," loading"," or"," tool"," calls","."]}}
|
||||
{"type":"assistant/chunk","seq":37,"time":1784998085318,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
@@ -12,6 +12,6 @@
|
||||
{"type":"assistant/chunk","seq":86,"time":1784998085814,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures."}}}}
|
||||
{"type":"assistant/chunk","seq":87,"time":1784998085814,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":110,"outputTokens":79,"cacheReadTokens":7680,"reasoningTokens":31}}}}
|
||||
{"type":"assistant/chunk","seq":88,"time":1784998085814,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":89,"time":1784998085818,"data":{"turn":1,"step":1,"usage":{"inputTokens":110,"outputTokens":79,"cacheReadTokens":7680,"reasoningTokens":31},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls."},{"type":"text","text":"Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures."}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"3396618c-56a2-4f97-a9c7-2a3cbb16a3c8"}},"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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":89,"time":1784998085818,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls."},{"type":"text","text":"Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures."}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":110,"outputTokens":79,"cacheReadTokens":7680,"reasoningTokens":31}},"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],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":90,"time":1784998085820,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":91,"time":1784998085821,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
- tree "Sessions":
|
||||
- treeitem "Ungrouped 3 sessions" [expanded]:
|
||||
- img
|
||||
- text: Ungrouped 3 sessions
|
||||
- treeitem "Use the read tool twice (2) now" [selected]
|
||||
- treeitem "Use the read tool twice (1) now"
|
||||
- treeitem "Use the read tool twice 1min"
|
||||
@@ -4,48 +4,45 @@
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}"
|
||||
- button "复制":
|
||||
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. 7/25 {{clock}}"
|
||||
- button "Copy":
|
||||
- img
|
||||
- tooltip "复制"
|
||||
- button "在新对话中分支":
|
||||
- tooltip "Copy"
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "编辑":
|
||||
- button "Edit":
|
||||
- img
|
||||
- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel.
|
||||
- button "复制":
|
||||
- button "Read a.txt":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- img
|
||||
- text: Read
|
||||
- button "a.txt"
|
||||
- img
|
||||
- text: Read
|
||||
- button "b.txt"
|
||||
- text: Read
|
||||
- button "a.txt"
|
||||
- button "Read b.txt":
|
||||
- img
|
||||
- img
|
||||
- text: Read
|
||||
- button "b.txt"
|
||||
- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed.":
|
||||
- img
|
||||
- img
|
||||
- text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed.
|
||||
- paragraph: DONE
|
||||
- button "复制":
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} cache hit 98% · 15,962 tokens · 1 turns · 2 steps
|
||||
- text: 7/25 {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 98% Input 15.8K tok · Output 135 tok
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
- dialog "设置":
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置":
|
||||
- img
|
||||
- text: 通用设置
|
||||
- button "模型":
|
||||
- img
|
||||
- text: 模型
|
||||
- button "关闭":
|
||||
- img
|
||||
- text: 关闭
|
||||
- heading "模型" [level=2]
|
||||
- paragraph: 填入各提供方的 API 密钥即可使用其模型。
|
||||
- list:
|
||||
- listitem:
|
||||
- text: minimax-cn 已启用
|
||||
- button "编辑"
|
||||
- button "删除"
|
||||
- button "+ 添加提供方"
|
||||
60
apps/web/tests/snapshots/models-settings/empty.expected.md
Normal file
60
apps/web/tests/snapshots/models-settings/empty.expected.md
Normal file
@@ -0,0 +1,60 @@
|
||||
- dialog "设置":
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置":
|
||||
- img
|
||||
- text: 通用设置
|
||||
- button "模型":
|
||||
- img
|
||||
- text: 模型
|
||||
- button "关闭":
|
||||
- img
|
||||
- text: 关闭
|
||||
- heading "模型" [level=2]
|
||||
- paragraph: 填入各提供方的 API 密钥即可使用其模型。
|
||||
- list
|
||||
- text: 提供方
|
||||
- combobox "提供方":
|
||||
- option "amazon-bedrock"
|
||||
- option "ant-ling"
|
||||
- option "anthropic"
|
||||
- option "azure-openai-responses"
|
||||
- option "cerebras"
|
||||
- option "cloudflare-ai-gateway"
|
||||
- option "cloudflare-workers-ai"
|
||||
- option "deepseek"
|
||||
- option "fireworks"
|
||||
- option "github-copilot"
|
||||
- option "google"
|
||||
- option "google-vertex"
|
||||
- option "groq"
|
||||
- option "huggingface"
|
||||
- option "kimi-coding"
|
||||
- option "minimax"
|
||||
- option "minimax-cn" [selected]
|
||||
- option "mistral"
|
||||
- option "moonshotai"
|
||||
- option "moonshotai-cn"
|
||||
- option "nvidia"
|
||||
- option "openai"
|
||||
- option "openai-codex"
|
||||
- option "opencode"
|
||||
- option "opencode-go"
|
||||
- option "openrouter"
|
||||
- option "qwen-token-plan"
|
||||
- option "qwen-token-plan-cn"
|
||||
- option "together"
|
||||
- option "vercel-ai-gateway"
|
||||
- option "xai"
|
||||
- option "xiaomi"
|
||||
- option "xiaomi-token-plan-ams"
|
||||
- option "xiaomi-token-plan-cn"
|
||||
- option "xiaomi-token-plan-sgp"
|
||||
- option "zai"
|
||||
- option "zai-coding-cn"
|
||||
- text: API 密钥
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 输入 API 密钥
|
||||
- group: 自定义设置
|
||||
- button "取消"
|
||||
- button "保存"
|
||||
@@ -0,0 +1,2 @@
|
||||
- tree "Search results":
|
||||
- 'treeitem "{{workspace}} {{workspace}} ## Navigation Summary - alpha nav - beta nav ``` echo WATERFALL ```"'
|
||||
@@ -1,9 +1,9 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785011380476,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785011380489,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785011380490,"data":{"content":[{"type":"text","text":"NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"7dcfe547-5555-4e5d-b2a0-3f9d2e0836a9"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785011380490,"data":{"content":[{"type":"text","text":"NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785011380492,"data":{"title":"NavScenario: first run bash to","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785011380549,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785011380550,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"request/header","seq":4,"time":1785011380550,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785011380917,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1785011380917,"data":{"turn":1,"step":1,"index":0,"dt":[110,25,1,0,0,25,1,0,26,0,1,0,27,1,0,0,0,26,1,0,0,26,0,0,0,0,1,25,0,0,25,0,1,0,0,0,26,1,0,25,0,0,27,1,0,0,0,0,25,28,0,1,0,0,0,27,0,0,0,25,1,24,1,0,25],"texts":["The"," user"," wants"," me"," to"," follow"," a"," specific"," navigation"," scenario","."," Let"," me",":\n\n","1","."," Run"," bash"," to"," print"," \"","NA","V","IG","ATION","_OK","\"\n","2","."," Read"," nav","-a",".md"," and"," nav","-b",".md"," in"," two"," read"," calls"," in"," ONE"," message","\n","3","."," Reply"," with"," \"","FIR","ST","_D","ONE","\"\n\n","Let"," me"," start"," with"," the"," bash"," command"," and"," the"," reads","."]}}
|
||||
{"type":"assistant/chunk","seq":72,"time":1785011381556,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
@@ -18,13 +18,13 @@
|
||||
{"type":"assistant/chunk","seq":130,"time":1785011382087,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":3,"block":{"type":"tool-call","id":"call_02_k8Z6wGirxfnW96Iv8mkz9224","name":"read","arguments":"{\"file_path\": \"nav-b.md\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":131,"time":1785011382087,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":140,"outputTokens":197,"cacheReadTokens":7680,"reasoningTokens":66}}}}
|
||||
{"type":"assistant/chunk","seq":132,"time":1785011382087,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":133,"time":1785011382091,"data":{"turn":1,"step":1,"usage":{"inputTokens":140,"outputTokens":197,"cacheReadTokens":7680,"reasoningTokens":66},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to follow a specific navigation scenario. Let me:\n\n1. Run bash to print \"NAVIGATION_OK\"\n2. Read nav-a.md and nav-b.md in two read calls in ONE message\n3. Reply with \"FIRST_DONE\"\n\nLet me start with the bash command and the reads."},{"type":"tool-call","id":"call_00_kFKHaEXcTYEex0iDZw0C2432","name":"bash","arguments":"{\"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\"}"},{"type":"tool-call","id":"call_01_tK4hIIRVTMgAvdzs7m9j6212","name":"read","arguments":"{\"file_path\": \"nav-a.md\"}"},{"type":"tool-call","id":"call_02_k8Z6wGirxfnW96Iv8mkz9224","name":"read","arguments":"{\"file_path\": \"nav-b.md\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"83b27ad0-4286-478f-92ef-3bc82bf0589b"}},"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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":133,"time":1785011382091,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to follow a specific navigation scenario. Let me:\n\n1. Run bash to print \"NAVIGATION_OK\"\n2. Read nav-a.md and nav-b.md in two read calls in ONE message\n3. Reply with \"FIRST_DONE\"\n\nLet me start with the bash command and the reads."},{"type":"tool-call","id":"call_00_kFKHaEXcTYEex0iDZw0C2432","name":"bash","arguments":"{\"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\"}"},{"type":"tool-call","id":"call_01_tK4hIIRVTMgAvdzs7m9j6212","name":"read","arguments":"{\"file_path\": \"nav-a.md\"}"},{"type":"tool-call","id":"call_02_k8Z6wGirxfnW96Iv8mkz9224","name":"read","arguments":"{\"file_path\": \"nav-b.md\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":140,"outputTokens":197,"cacheReadTokens":7680,"reasoningTokens":66}},"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],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":134,"time":1785011382092,"data":{"turn":1,"step":1,"callId":"call_00_kFKHaEXcTYEex0iDZw0C2432","name":"bash","arguments":"{\"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\"}"}}
|
||||
{"type":"tool/result","seq":135,"time":1785011382105,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_kFKHaEXcTYEex0iDZw0C2432"},"content":[{"type":"tool-result","toolCallId":"call_00_kFKHaEXcTYEex0iDZw0C2432","content":[{"type":"text","text":"NAVIGATION_OK\n"}],"isError":false}],"role":"user","id":"d6e29f9e-8177-4f12-b1b6-2eda4c8f5c31"}},"sourceEventSeqs":[134],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":135,"time":1785011382105,"data":{"turn":1,"step":1,"callId":"call_00_kFKHaEXcTYEex0iDZw0C2432","content":[{"type":"text","text":"NAVIGATION_OK\n"}],"isError":false},"sourceEventSeqs":[134],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":136,"time":1785011382105,"data":{"turn":1,"step":1,"callId":"call_01_tK4hIIRVTMgAvdzs7m9j6212","name":"read","arguments":"{\"file_path\": \"nav-a.md\"}"}}
|
||||
{"type":"tool/call","seq":137,"time":1785011382106,"data":{"turn":1,"step":1,"callId":"call_02_k8Z6wGirxfnW96Iv8mkz9224","name":"read","arguments":"{\"file_path\": \"nav-b.md\"}"}}
|
||||
{"type":"tool/result","seq":138,"time":1785011382113,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_01_tK4hIIRVTMgAvdzs7m9j6212"},"content":[{"type":"tool-result","toolCallId":"call_01_tK4hIIRVTMgAvdzs7m9j6212","content":[{"type":"text","text":"<path>{{cwd}}/workspace/nav-a.md</path>\n<type>file</type>\n<content>\n1: # alpha nav\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"efca7baf-cbb8-462f-ad75-580357b58676"}},"sourceEventSeqs":[136],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":139,"time":1785011382114,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_02_k8Z6wGirxfnW96Iv8mkz9224"},"content":[{"type":"tool-result","toolCallId":"call_02_k8Z6wGirxfnW96Iv8mkz9224","content":[{"type":"text","text":"<path>{{cwd}}/workspace/nav-b.md</path>\n<type>file</type>\n<content>\n1: # beta nav\n\n(End of file - total 1 lines)\n</content>"}],"isError":false}],"role":"user","id":"e1c70190-9e68-429d-963d-8a30ab779ddd"}},"sourceEventSeqs":[137],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":138,"time":1785011382113,"data":{"turn":1,"step":1,"callId":"call_01_tK4hIIRVTMgAvdzs7m9j6212","content":[{"type":"text","text":"<path>{{cwd}}/workspace/nav-a.md</path>\n<type>file</type>\n<content>\n1: # alpha nav\n\n(End of file - total 1 lines)\n</content>"}],"isError":false},"sourceEventSeqs":[136],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":139,"time":1785011382114,"data":{"turn":1,"step":1,"callId":"call_02_k8Z6wGirxfnW96Iv8mkz9224","content":[{"type":"text","text":"<path>{{cwd}}/workspace/nav-b.md</path>\n<type>file</type>\n<content>\n1: # beta nav\n\n(End of file - total 1 lines)\n</content>"}],"isError":false},"sourceEventSeqs":[137],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":140,"time":1785011382117,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":141,"time":1785011382118,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":142,"time":1785011382673,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -35,11 +35,11 @@
|
||||
{"type":"assistant/chunk","seq":200,"time":1785011383090,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"FIRST_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":201,"time":1785011383090,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":349,"outputTokens":56,"cacheReadTokens":7808,"reasoningTokens":51}}}}
|
||||
{"type":"assistant/chunk","seq":202,"time":1785011383090,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":203,"time":1785011383091,"data":{"turn":1,"step":2,"usage":{"inputTokens":349,"outputTokens":56,"cacheReadTokens":7808,"reasoningTokens":51},"message":{"role":"assistant","content":[{"type":"reasoning","text":"All three calls succeeded:\n1. bash printed \"NAVIGATION_OK\"\n2. nav-a.md contains \"# alpha nav\"\n3. nav-b.md contains \"# beta nav\"\n\nNow I need to reply with the single word \"FIRST_DONE\"."},{"type":"text","text":"FIRST_DONE"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"0115844e-fac0-4985-9ba7-dcf1dde63b83"}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":203,"time":1785011383091,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"All three calls succeeded:\n1. bash printed \"NAVIGATION_OK\"\n2. nav-a.md contains \"# alpha nav\"\n3. nav-b.md contains \"# beta nav\"\n\nNow I need to reply with the single word \"FIRST_DONE\"."},{"type":"text","text":"FIRST_DONE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":349,"outputTokens":56,"cacheReadTokens":7808,"reasoningTokens":51}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":204,"time":1785011383091,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":205,"time":1785011383092,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"turn/start","seq":206,"time":1785011383106,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":207,"time":1785011383107,"data":{"content":[{"type":"text","text":"Reply in markdown with: a level-2 heading \"Navigation Summary\", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"1d7a8563-4ae6-49f3-9a65-41f3df300a75"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":207,"time":1785011383107,"data":{"content":[{"type":"text","text":"Reply in markdown with: a level-2 heading \"Navigation Summary\", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":208,"time":1785011383107,"data":{"turn":2,"step":1}}
|
||||
{"type":"assistant/chunk","seq":209,"time":1785011383497,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":210,"time0":1785011383497,"data":{"turn":2,"step":1,"index":0,"dt":[125,23,1,0,0,88,0,0,5,0,1,0,0,7,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," a"," specific"," format","."," Let"," me"," do"," that","."]}}
|
||||
@@ -49,6 +49,6 @@
|
||||
{"type":"assistant/chunk","seq":247,"time":1785011383903,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"## Navigation Summary\n\n- alpha nav\n- beta nav\n\n```\necho WATERFALL\n```"}}}}
|
||||
{"type":"assistant/chunk","seq":248,"time":1785011383903,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":141,"outputTokens":36,"cacheReadTokens":8064,"reasoningTokens":16}}}}
|
||||
{"type":"assistant/chunk","seq":249,"time":1785011383903,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":250,"time":1785011383904,"data":{"turn":2,"step":1,"usage":{"inputTokens":141,"outputTokens":36,"cacheReadTokens":8064,"reasoningTokens":16},"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with a specific format. Let me do that."},{"type":"text","text":"## Navigation Summary\n\n- alpha nav\n- beta nav\n\n```\necho WATERFALL\n```"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"70b38db9-38a2-4f9f-8094-1bd799f5f270"}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":250,"time":1785011383904,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with a specific format. Let me do that."},{"type":"text","text":"## Navigation Summary\n\n- alpha nav\n- beta nav\n\n```\necho WATERFALL\n```"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":141,"outputTokens":36,"cacheReadTokens":8064,"reasoningTokens":16}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":251,"time":1785011383904,"data":{"turn":2,"step":1}}
|
||||
{"type":"turn/end","seq":252,"time":1785011383904,"data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
- text: 已完成 {{workspace}} echo NAVIGATION_OK
|
||||
- button "复制"
|
||||
- text: Done {{workspace}} echo NAVIGATION_OK
|
||||
- button "Copy"
|
||||
- text: NAVIGATION_OK
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- cell "SYSTEM"
|
||||
- cell "Initial System Prompt"
|
||||
- 'row "USER, NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop."':
|
||||
- cell "Turn 1 USER"
|
||||
- cell "Turn 1 USER": USER
|
||||
- 'cell "NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop."'
|
||||
- 'row "Request 1, ASSISTANT, The user wants me to follow a specific navigation scenario. Let me: Run bash to print \"NAVIGATION_OK\" Read nav-a.md and nav-b.md in two read calls in ONE message Reply with \"FIRST_DONE\" Let me start with the bash command and the reads."':
|
||||
- 'cell "Request #1 ASSISTANT"':
|
||||
@@ -33,7 +33,7 @@
|
||||
- text: ASSISTANT
|
||||
- cell "FIRST_DONE"
|
||||
- 'row "USER, Reply in markdown with: a level-2 heading \"Navigation Summary\", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop."':
|
||||
- cell "Turn 2 USER"
|
||||
- cell "Turn 2 USER": USER
|
||||
- 'cell "Reply in markdown with: a level-2 heading \"Navigation Summary\", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop."'
|
||||
- row "Request 3, ASSISTANT, Navigation Summary alpha nav beta nav echo WATERFALL":
|
||||
- 'cell "Request #3 ASSISTANT"':
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
- dialog "添加一个 API Key 开始使用":
|
||||
- heading "添加一个 API Key 开始使用" [level=2]
|
||||
- button "稍后配置":
|
||||
- img
|
||||
- paragraph: 配置 DeepSeek 官方模型,即可开始使用。
|
||||
- button "前往配置"
|
||||
46
apps/web/tests/snapshots/plan-review/approved.expected.md
Normal file
46
apps/web/tests/snapshots/plan-review/approved.expected.md
Normal file
@@ -0,0 +1,46 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Plan a small change: add" [disabled]'
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- img
|
||||
- text: "plan Plan mode on. Use /plan off to leave. Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}"
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "Edit":
|
||||
- img
|
||||
- 'button "Think The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."':
|
||||
- img
|
||||
- img
|
||||
- text: "Think The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."
|
||||
- paragraph:
|
||||
- text: Since the user has explicitly asked me not to read or write any files and to go straight to planning, I'll proceed with
|
||||
- code: exit_plan_mode
|
||||
- text: .
|
||||
- 'button "Tool call exit_plan_mode · # Add `--greeting` flag to CLI"':
|
||||
- img
|
||||
- img
|
||||
- text: "Tool call exit_plan_mode · # Add `--greeting` flag to CLI"
|
||||
- 'button "Think The plan was approved. The user''s last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop."':
|
||||
- img
|
||||
- img
|
||||
- text: "Think The plan was approved. The user's last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop."
|
||||
- paragraph: DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}}
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- 'button "Access mode, current: Danger Full Access"': Danger Full Access
|
||||
- button "Plan mode off, press to turn on": Plan off
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps Tool call {{duration}} Cache hit 51% Input 10.2K tok · Output 346 tok
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user