Merge commit '5ae8c6487ad9556b8793b890a4fe5ece24c55805' into codex/product-subagent-one-shot-background
# Conflicts: # packages/subagent/subagent-codex/README.i18n.yaml # packages/subagent/subagent-codex/README.zh.md # packages/subagent/tool-subagent/tests/tool-subagent.spec.ts
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: 4fbd0692a2df403c6395235e096e193c994ea198
|
||||
README.zh.md: 7861f5b4f8cb447aff01ec8b64b12fd298485b4b
|
||||
README.md: 9a8d722b044ed5d8e31e3c27e54f8c9ef0839f82
|
||||
README.zh.md: 7889b4180b5ce2f65550f98708d498f324ac349d
|
||||
|
||||
@@ -9,7 +9,7 @@ The `dsh` command is the product launcher for profiles: ordered stacks of plugin
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `dsh --profile <name>` | Boot the named profile under `$DSH_HOME/profiles/<name>`. |
|
||||
| `dsh --profile headless "task"` | Run one fresh persisted session, print the final answer, and exit. |
|
||||
| `dsh --profile headless "job"` | Run one fresh persisted session, print the final answer, and exit. |
|
||||
| `dsh web` | Alias of `--profile web`. |
|
||||
| `dsh plugin --profile <name> <pnpm args>` | Manage a profile's plugins by forwarding to pnpm in the profile directory. |
|
||||
|
||||
@@ -21,7 +21,7 @@ The launcher parses only its own flags and hands everything after them to the bo
|
||||
|
||||
```sh
|
||||
dsh --profile web --port 8080 # --port belongs to the web app
|
||||
dsh --profile tui --resume <id> # --resume belongs to the terminal app
|
||||
dsh --profile tui --resume <id> # example, assuming the tui profile is installed; --resume belongs to the terminal app
|
||||
dsh --profile headless "run the tests"
|
||||
dsh --profile web --help # the web app's flags, not the launcher's
|
||||
dsh --help # the launcher's own help
|
||||
@@ -29,7 +29,16 @@ dsh --help # the launcher's own help
|
||||
|
||||
## Profiles
|
||||
|
||||
A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer, hot-reloaded on long-lived surfaces). The tree composes over an empty root: each bundle's patch in `dsh.profile.bundles` order, then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`, then `--patch` overlays. Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it.
|
||||
A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer).
|
||||
|
||||
The tree composes over an empty root:
|
||||
- each bundle's patch in `dsh.profile.bundles` order
|
||||
- then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`
|
||||
- then `--patch` overlays
|
||||
|
||||
Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins.
|
||||
|
||||
Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it.
|
||||
|
||||
The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and source execution.
|
||||
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`dsh` 命令是 profile 的产品启动器:profile 是按序叠放的插件组合包 patch 层,之上再叠加用户自己的覆盖层。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
|
||||
`dsh` 是 DeepSeek Harness 中用于启动 profile 的命令;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
|
||||
|
||||
## 入口模式
|
||||
|
||||
| 命令 | 用途 |
|
||||
|---|---|
|
||||
| `dsh --profile <name>` | 启动位于 `$DSH_HOME/profiles/<name>` 的指定 profile。 |
|
||||
| `dsh --profile headless "task"` | 运行一个新的持久化会话,打印最终答案并退出。 |
|
||||
| `dsh --profile headless "job"` | 运行一个全新的持久化会话,打印最终答案并退出。 |
|
||||
| `dsh web` | `--profile web` 的别名。 |
|
||||
| `dsh plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |
|
||||
|
||||
调用目录是默认 workspace 根目录。`web` 和 `headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
|
||||
运行命令时所在的目录将作为默认 workspace 根目录。`web` 和 `headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
|
||||
|
||||
## 应用参数
|
||||
|
||||
启动器只解析属于自己的 flag,并把其后的一切交给启动起来的 profile,任何注入它的应用插件都可以解析这份共享的不可变快照([`dsh-cmdline`](../../packages/boot/cmdline/README.md))。因此启动器的 flag 必须写在前面,而启动器不认识的第一个 token 就是应用参数的起点:
|
||||
启动器只解析自身的 flag,并将其后的所有内容交给已启动的 profile;注入该 profile 的任意应用插件都可以解析这份共享的不可变快照([`dsh-cmdline`](../../packages/boot/cmdline/README.md))。因此,启动器的 flag 必须写在最前面;启动器无法识别的第一个 token 标志着应用参数的开始:
|
||||
|
||||
```sh
|
||||
dsh --profile web --port 8080 # --port belongs to the web app
|
||||
dsh --profile tui --resume <id> # --resume belongs to the terminal app
|
||||
dsh --profile tui --resume <id> # example, assuming the tui profile is installed; --resume belongs to the terminal app
|
||||
dsh --profile headless "run the tests"
|
||||
dsh --profile web --help # the web app's flags, not the launcher's
|
||||
dsh --help # the launcher's own help
|
||||
@@ -29,10 +29,19 @@ dsh --help # the launcher's own help
|
||||
|
||||
## Profile
|
||||
|
||||
profile 目录包含一个 `package.json`(树外插件依赖,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 列表)和一个 `cordis.patch.yml`(用户自己的 patch 层,在长期运行的 surface 上热重载)。配置树在空根之上组合:先按 `dsh.profile.bundles` 顺序应用各组合包的 patch,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`,然后是 `--patch` overlay。`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自己的 `node_modules` 解析;pnpm 把树外插件安装在后者。使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。
|
||||
profile 目录包含一个 `package.json`,其中记录树外插件依赖,以及 profile manifest(元数据清单)`dsh.profile` 和其中按顺序排列的 `bundles` 列表;还包含一个 `cordis.patch.yml`,其中保存用户自己的 patch 层。
|
||||
|
||||
[CLI(命令行界面)行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码执行。
|
||||
配置树以空根为起点,依次叠加以下配置层:
|
||||
- `dsh.profile.bundles` 中各组合包的 patch
|
||||
- profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`
|
||||
- `--patch` 指定的覆盖层
|
||||
|
||||
`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。
|
||||
|
||||
使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。
|
||||
|
||||
层的确切优先级、flag、关闭行为、部署默认值和源码执行方式,以 [CLI(命令行界面)行为参考](reference/README.md)为准。
|
||||
|
||||
## 开发
|
||||
|
||||
生产运行需要已构建的包与前端产物。请在仓库根目录单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>` 运行 TypeScript 入口并转发所有参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。
|
||||
生产运行需要已构建的包与前端产物。请在仓库根目录单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>` 运行 TypeScript 入口并转发所有参数;模块解析约定以[源码执行参考](reference/README.md#source-execution)为准。
|
||||
|
||||
@@ -24,19 +24,19 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_typert_gateway
|
||||
plugin_dsh_base_session_title["session-title<br/>@deepseek-ai/dsh-session-title"]
|
||||
cfg --> plugin_dsh_base_session_title
|
||||
plugin_dsh_base_session_title_llm["session-title-llm<br/>@deepseek-ai/dsh-session-title-first-message-llm"]
|
||||
plugin_dsh_base_session_title_llm["session-title-llm<br/>@deepseek-ai/dsh-session-title-first-prompt-llm"]
|
||||
cfg --> plugin_dsh_base_session_title_llm
|
||||
plugin_dsh_base_user_interaction["user-interaction<br/>@deepseek-ai/dsh-user-interaction"]
|
||||
cfg --> plugin_dsh_base_user_interaction
|
||||
plugin_dsh_base_user_questions["user-questions<br/>@deepseek-ai/dsh-user-questions"]
|
||||
cfg --> plugin_dsh_base_user_questions
|
||||
plugin_dsh_base_agent["agent<br/>@deepseek-ai/dsh-agent"]
|
||||
cfg --> plugin_dsh_base_agent
|
||||
plugin_dsh_base_agent_default_model["agent-default-model<br/>@deepseek-ai/dsh-agent-default-model"]
|
||||
cfg --> plugin_dsh_base_agent_default_model
|
||||
plugin_dsh_base_tasks["tasks<br/>@deepseek-ai/dsh-tasks-local"]
|
||||
cfg --> plugin_dsh_base_tasks
|
||||
plugin_dsh_base_jobs["jobs<br/>@deepseek-ai/dsh-jobs-local"]
|
||||
cfg --> plugin_dsh_base_jobs
|
||||
plugin_dsh_base_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
|
||||
cfg --> plugin_dsh_base_llm_retry
|
||||
plugin_dsh_base_settings["settings<br/>@deepseek-ai/dsh-settings-local"]
|
||||
plugin_dsh_base_settings["settings<br/>@deepseek-ai/dsh-settings-file"]
|
||||
cfg --> plugin_dsh_base_settings
|
||||
plugin_dsh_base_credentials["credentials<br/>@deepseek-ai/dsh-credentials-local"]
|
||||
cfg --> plugin_dsh_base_credentials
|
||||
@@ -50,8 +50,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_session_query_sqlite
|
||||
plugin_dsh_base_session_projection["session-projection<br/>@deepseek-ai/dsh-session-projection"]
|
||||
cfg --> plugin_dsh_base_session_projection
|
||||
plugin_dsh_base_telemetry_otel["telemetry-otel<br/>@deepseek-ai/dsh-session-telemetry-otel"]
|
||||
cfg --> plugin_dsh_base_telemetry_otel
|
||||
plugin_dsh_base_session_telemetry_otel["session-telemetry-otel<br/>@deepseek-ai/dsh-session-telemetry-otel"]
|
||||
cfg --> plugin_dsh_base_session_telemetry_otel
|
||||
plugin_dsh_base_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
|
||||
cfg --> plugin_dsh_base_subprocess
|
||||
plugin_dsh_base_sandbox["sandbox<br/>@deepseek-ai/dsh-sandbox-local"]
|
||||
@@ -64,28 +64,28 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_pwsh_sandbox
|
||||
plugin_dsh_base_approval["approval<br/>@deepseek-ai/dsh-user-approval"]
|
||||
cfg --> plugin_dsh_base_approval
|
||||
plugin_dsh_base_permission["permission<br/>@deepseek-ai/dsh-permission"]
|
||||
plugin_dsh_base_permission["permission<br/>@deepseek-ai/dsh-permission-presets"]
|
||||
cfg --> plugin_dsh_base_permission
|
||||
plugin_dsh_base_bash_env["bash-env<br/>@deepseek-ai/dsh-bash-env"]
|
||||
cfg --> plugin_dsh_base_bash_env
|
||||
plugin_dsh_base_shell_env["shell-env<br/>@deepseek-ai/dsh-shell-env"]
|
||||
cfg --> plugin_dsh_base_shell_env
|
||||
plugin_dsh_base_tool_bash["tool-bash<br/>@deepseek-ai/dsh-tool-bash"]
|
||||
cfg --> plugin_dsh_base_tool_bash
|
||||
plugin_dsh_base_tool_pwsh["tool-pwsh<br/>@deepseek-ai/dsh-tool-pwsh"]
|
||||
cfg --> plugin_dsh_base_tool_pwsh
|
||||
plugin_dsh_base_tool_tasks["tool-tasks<br/>@deepseek-ai/dsh-tool-tasks"]
|
||||
cfg --> plugin_dsh_base_tool_tasks
|
||||
plugin_dsh_base_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"]
|
||||
cfg --> plugin_dsh_base_fs_policy
|
||||
plugin_dsh_base_tool_jobs["tool-jobs<br/>@deepseek-ai/dsh-tool-jobs"]
|
||||
cfg --> plugin_dsh_base_tool_jobs
|
||||
plugin_dsh_base_fs_observation_policy["fs-observation-policy<br/>@deepseek-ai/dsh-fs-observation-policy"]
|
||||
cfg --> plugin_dsh_base_fs_observation_policy
|
||||
plugin_dsh_base_tool_fs["tool-fs<br/>@deepseek-ai/dsh-tool-fs"]
|
||||
cfg --> plugin_dsh_base_tool_fs
|
||||
plugin_dsh_base_tool_fs_search["tool-fs-search<br/>@deepseek-ai/dsh-tool-fs-search"]
|
||||
cfg --> plugin_dsh_base_tool_fs_search
|
||||
plugin_dsh_base_workspace_context["workspace-context<br/>@deepseek-ai/dsh-workspace-context"]
|
||||
cfg --> plugin_dsh_base_workspace_context
|
||||
plugin_dsh_base_agent_instructions["agent-instructions<br/>@deepseek-ai/dsh-agent-instructions"]
|
||||
cfg --> plugin_dsh_base_agent_instructions
|
||||
plugin_dsh_base_skill["skill<br/>@deepseek-ai/dsh-skill"]
|
||||
cfg --> plugin_dsh_base_skill
|
||||
plugin_dsh_base_skill_local["skill-local<br/>@deepseek-ai/dsh-skill-local"]
|
||||
cfg --> plugin_dsh_base_skill_local
|
||||
plugin_dsh_base_skill_filesystem["skill-filesystem<br/>@deepseek-ai/dsh-skill-filesystem"]
|
||||
cfg --> plugin_dsh_base_skill_filesystem
|
||||
plugin_dsh_base_skill_badge["skill-badge<br/>@deepseek-ai/dsh-skill-badge"]
|
||||
cfg --> plugin_dsh_base_skill_badge
|
||||
plugin_dsh_base_tool_skill["tool-skill<br/>@deepseek-ai/dsh-tool-skill"]
|
||||
@@ -96,28 +96,24 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_command_feedback
|
||||
plugin_dsh_base_goal["goal<br/>@deepseek-ai/dsh-goal"]
|
||||
cfg --> plugin_dsh_base_goal
|
||||
plugin_dsh_base_goal_session["goal-session<br/>@deepseek-ai/dsh-goal-session"]
|
||||
cfg --> plugin_dsh_base_goal_session
|
||||
plugin_dsh_base_goal_round_driver["goal-round-driver<br/>@deepseek-ai/dsh-goal-round-driver"]
|
||||
cfg --> plugin_dsh_base_goal_round_driver
|
||||
plugin_dsh_base_command_goal["command-goal<br/>@deepseek-ai/dsh-command-goal"]
|
||||
cfg --> plugin_dsh_base_command_goal
|
||||
plugin_dsh_base_plan_mode["plan-mode<br/>@deepseek-ai/dsh-plan-mode"]
|
||||
cfg --> plugin_dsh_base_plan_mode
|
||||
plugin_dsh_base_token_meter["token-meter<br/>@deepseek-ai/dsh-token-meter"]
|
||||
cfg --> plugin_dsh_base_token_meter
|
||||
plugin_dsh_base_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"]
|
||||
cfg --> plugin_dsh_base_compact_basic
|
||||
plugin_dsh_base_compaction_basic["compaction-basic<br/>@deepseek-ai/dsh-compaction-basic"]
|
||||
cfg --> plugin_dsh_base_compaction_basic
|
||||
plugin_dsh_base_command_compact["command-compact<br/>@deepseek-ai/dsh-command-compact"]
|
||||
cfg --> plugin_dsh_base_command_compact
|
||||
plugin_dsh_base_subagent["subagent<br/>@deepseek-ai/dsh-subagent"]
|
||||
cfg --> plugin_dsh_base_subagent
|
||||
plugin_dsh_base_subagent_spawn["subagent-spawn<br/>@deepseek-ai/dsh-subagent-spawn"]
|
||||
cfg --> plugin_dsh_base_subagent_spawn
|
||||
plugin_dsh_base_subagent_fork["subagent-fork<br/>@deepseek-ai/dsh-subagent-fork"]
|
||||
cfg --> plugin_dsh_base_subagent_fork
|
||||
plugin_dsh_base_subagent_codex["subagent-codex<br/>@deepseek-ai/dsh-subagent-codex"]
|
||||
cfg --> plugin_dsh_base_subagent_codex
|
||||
plugin_dsh_base_subagent_claude_code["subagent-claude-code<br/>@deepseek-ai/dsh-subagent-claude-code"]
|
||||
cfg --> plugin_dsh_base_subagent_claude_code
|
||||
plugin_dsh_base_subagent_spawn_in_process["subagent-spawn-in-process<br/>@deepseek-ai/dsh-subagent-spawn-in-process"]
|
||||
cfg --> plugin_dsh_base_subagent_spawn_in_process
|
||||
plugin_dsh_base_subagent_fork_in_process["subagent-fork-in-process<br/>@deepseek-ai/dsh-subagent-fork-in-process"]
|
||||
cfg --> plugin_dsh_base_subagent_fork_in_process
|
||||
plugin_dsh_base_tool_subagent_control["tool-subagent-control<br/>@deepseek-ai/dsh-tool-subagent-control"]
|
||||
cfg --> plugin_dsh_base_tool_subagent_control
|
||||
plugin_dsh_base_tool_subagent_list_agents["tool-subagent-list-agents<br/>@deepseek-ai/dsh-tool-subagent-control/list-agents"]
|
||||
@@ -128,11 +124,11 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_tool_subagent_fork
|
||||
plugin_dsh_base_tool_subagent_report["tool-subagent-report<br/>@deepseek-ai/dsh-tool-subagent-report"]
|
||||
cfg --> plugin_dsh_base_tool_subagent_report
|
||||
plugin_dsh_base_workflow_workerthread["workflow-workerthread<br/>@deepseek-ai/dsh-workflow-workerthread"]
|
||||
cfg --> plugin_dsh_base_workflow_workerthread
|
||||
plugin_dsh_base_workflow_worker_thread["workflow-worker-thread<br/>@deepseek-ai/dsh-workflow-worker-thread"]
|
||||
cfg --> plugin_dsh_base_workflow_worker_thread
|
||||
plugin_dsh_base_tool_workflow["tool-workflow<br/>@deepseek-ai/dsh-tool-workflow"]
|
||||
cfg --> plugin_dsh_base_tool_workflow
|
||||
plugin_dsh_base_timeout_policy["timeout-policy<br/>@deepseek-ai/dsh-timeout-policy"]
|
||||
plugin_dsh_base_timeout_policy["timeout-policy<br/>@deepseek-ai/dsh-tool-call-timeout-policy"]
|
||||
cfg --> plugin_dsh_base_timeout_policy
|
||||
plugin_dsh_base_spill_local["spill-local<br/>@deepseek-ai/dsh-spill-local"]
|
||||
cfg --> plugin_dsh_base_spill_local
|
||||
@@ -140,8 +136,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_spill_policy
|
||||
plugin_dsh_base_session_checkpoint_policy["session-checkpoint-policy<br/>@deepseek-ai/dsh-session-checkpoint-policy"]
|
||||
cfg --> plugin_dsh_base_session_checkpoint_policy
|
||||
plugin_dsh_base_tool_result_prune["tool-result-prune<br/>@deepseek-ai/dsh-compact-tool-result-prune"]
|
||||
cfg --> plugin_dsh_base_tool_result_prune
|
||||
plugin_dsh_base_tool_result_pruner["tool-result-pruner<br/>@deepseek-ai/dsh-compaction-tool-result-pruner"]
|
||||
cfg --> plugin_dsh_base_tool_result_pruner
|
||||
plugin_dsh_base_tool_todo["tool-todo<br/>@deepseek-ai/dsh-tool-todo"]
|
||||
cfg --> plugin_dsh_base_tool_todo
|
||||
plugin_dsh_base_tool_goal["tool-goal<br/>@deepseek-ai/dsh-tool-goal"]
|
||||
@@ -150,8 +146,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_tool_ralph
|
||||
plugin_dsh_base_tool_str_replace_editor["tool-str-replace-editor<br/>@deepseek-ai/dsh-tool-str-replace-editor"]
|
||||
cfg --> plugin_dsh_base_tool_str_replace_editor
|
||||
plugin_dsh_base_repeat_tool_guard["repeat-tool-guard<br/>@deepseek-ai/dsh-repeat-tool-guard"]
|
||||
cfg --> plugin_dsh_base_repeat_tool_guard
|
||||
plugin_dsh_base_repeat_tool_reminder["repeat-tool-reminder<br/>@deepseek-ai/dsh-repeat-tool-reminder"]
|
||||
cfg --> plugin_dsh_base_repeat_tool_reminder
|
||||
plugin_dsh_base_web["web<br/>@deepseek-ai/dsh-web"]
|
||||
cfg --> plugin_dsh_base_web
|
||||
plugin_dsh_base_web_search_deepseek["web-search-deepseek<br/>@deepseek-ai/dsh-web-search-deepseek"]
|
||||
@@ -180,70 +176,68 @@ flowchart LR
|
||||
| `typert-loader` | `@deepseek-ai/dsh-typert-loader` |
|
||||
| `typert-gateway` | `@deepseek-ai/dsh-api-gateway` |
|
||||
| `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` |
|
||||
| `session-title-llm` | `@deepseek-ai/dsh-session-title-first-prompt-llm` |
|
||||
| `user-questions` | `@deepseek-ai/dsh-user-questions` |
|
||||
| `agent` | `@deepseek-ai/dsh-agent` |
|
||||
| `agent-default-model` | `@deepseek-ai/dsh-agent-default-model` |
|
||||
| `tasks` | `@deepseek-ai/dsh-tasks-local` |
|
||||
| `jobs` | `@deepseek-ai/dsh-jobs-local` |
|
||||
| `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
|
||||
| `settings` | `@deepseek-ai/dsh-settings-local` |
|
||||
| `settings` | `@deepseek-ai/dsh-settings-file` |
|
||||
| `credentials` | `@deepseek-ai/dsh-credentials-local` |
|
||||
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
|
||||
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
|
||||
| `attachment-local` | `@deepseek-ai/dsh-attachment-local` |
|
||||
| `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` |
|
||||
| `session-projection` | `@deepseek-ai/dsh-session-projection` |
|
||||
| `telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` |
|
||||
| `session-telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` |
|
||||
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
|
||||
| `sandbox` | `@deepseek-ai/dsh-sandbox-local` |
|
||||
| `sandbox-policy` | `@deepseek-ai/dsh-sandbox-policy` |
|
||||
| `bash-sandbox` | `@deepseek-ai/dsh-bash-sandbox` |
|
||||
| `pwsh-sandbox` | `@deepseek-ai/dsh-pwsh-sandbox` |
|
||||
| `approval` | `@deepseek-ai/dsh-user-approval` |
|
||||
| `permission` | `@deepseek-ai/dsh-permission` |
|
||||
| `bash-env` | `@deepseek-ai/dsh-bash-env` |
|
||||
| `permission` | `@deepseek-ai/dsh-permission-presets` |
|
||||
| `shell-env` | `@deepseek-ai/dsh-shell-env` |
|
||||
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
|
||||
| `tool-pwsh` | `@deepseek-ai/dsh-tool-pwsh` |
|
||||
| `tool-tasks` | `@deepseek-ai/dsh-tool-tasks` |
|
||||
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
|
||||
| `tool-jobs` | `@deepseek-ai/dsh-tool-jobs` |
|
||||
| `fs-observation-policy` | `@deepseek-ai/dsh-fs-observation-policy` |
|
||||
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |
|
||||
| `tool-fs-search` | `@deepseek-ai/dsh-tool-fs-search` |
|
||||
| `workspace-context` | `@deepseek-ai/dsh-workspace-context` |
|
||||
| `agent-instructions` | `@deepseek-ai/dsh-agent-instructions` |
|
||||
| `skill` | `@deepseek-ai/dsh-skill` |
|
||||
| `skill-local` | `@deepseek-ai/dsh-skill-local` |
|
||||
| `skill-filesystem` | `@deepseek-ai/dsh-skill-filesystem` |
|
||||
| `skill-badge` | `@deepseek-ai/dsh-skill-badge` |
|
||||
| `tool-skill` | `@deepseek-ai/dsh-tool-skill` |
|
||||
| `commands` | `@deepseek-ai/dsh-commands` |
|
||||
| `command-feedback` | `@deepseek-ai/dsh-command-feedback` |
|
||||
| `goal` | `@deepseek-ai/dsh-goal` |
|
||||
| `goal-session` | `@deepseek-ai/dsh-goal-session` |
|
||||
| `goal-round-driver` | `@deepseek-ai/dsh-goal-round-driver` |
|
||||
| `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` |
|
||||
| `compaction-basic` | `@deepseek-ai/dsh-compaction-basic` |
|
||||
| `command-compact` | `@deepseek-ai/dsh-command-compact` |
|
||||
| `subagent` | `@deepseek-ai/dsh-subagent` |
|
||||
| `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` |
|
||||
| `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` |
|
||||
| `subagent-codex` | `@deepseek-ai/dsh-subagent-codex` |
|
||||
| `subagent-claude-code` | `@deepseek-ai/dsh-subagent-claude-code` |
|
||||
| `subagent-spawn-in-process` | `@deepseek-ai/dsh-subagent-spawn-in-process` |
|
||||
| `subagent-fork-in-process` | `@deepseek-ai/dsh-subagent-fork-in-process` |
|
||||
| `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` |
|
||||
| `tool-subagent-list-agents` | `@deepseek-ai/dsh-tool-subagent-control/list-agents` |
|
||||
| `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` |
|
||||
| `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` |
|
||||
| `tool-subagent-report` | `@deepseek-ai/dsh-tool-subagent-report` |
|
||||
| `workflow-workerthread` | `@deepseek-ai/dsh-workflow-workerthread` |
|
||||
| `workflow-worker-thread` | `@deepseek-ai/dsh-workflow-worker-thread` |
|
||||
| `tool-workflow` | `@deepseek-ai/dsh-tool-workflow` |
|
||||
| `timeout-policy` | `@deepseek-ai/dsh-timeout-policy` |
|
||||
| `timeout-policy` | `@deepseek-ai/dsh-tool-call-timeout-policy` |
|
||||
| `spill-local` | `@deepseek-ai/dsh-spill-local` |
|
||||
| `spill-policy` | `@deepseek-ai/dsh-spill-policy` |
|
||||
| `session-checkpoint-policy` | `@deepseek-ai/dsh-session-checkpoint-policy` |
|
||||
| `tool-result-prune` | `@deepseek-ai/dsh-compact-tool-result-prune` |
|
||||
| `tool-result-pruner` | `@deepseek-ai/dsh-compaction-tool-result-pruner` |
|
||||
| `tool-todo` | `@deepseek-ai/dsh-tool-todo` |
|
||||
| `tool-goal` | `@deepseek-ai/dsh-tool-goal` |
|
||||
| `tool-ralph` | `@deepseek-ai/dsh-tool-ralph` |
|
||||
| `tool-str-replace-editor` | `@deepseek-ai/dsh-tool-str-replace-editor` |
|
||||
| `repeat-tool-guard` | `@deepseek-ai/dsh-repeat-tool-guard` |
|
||||
| `repeat-tool-reminder` | `@deepseek-ai/dsh-repeat-tool-reminder` |
|
||||
| `web` | `@deepseek-ai/dsh-web` |
|
||||
| `web-search-deepseek` | `@deepseek-ai/dsh-web-search-deepseek` |
|
||||
| `tool-web` | `@deepseek-ai/dsh-tool-web` |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The `code` agent preset: the standard coding agent, presented as Code Mode.
|
||||
#
|
||||
# Everything in `standard` is here unchanged. What is added is the `tool-mode`
|
||||
# Everything in `standard` is here unchanged. What is added is the `tool-presentation`
|
||||
# row: instead of one tool call per action, the model writes a TypeScript
|
||||
# program against a generated SDK and `run_code` executes it, so a sequence
|
||||
# that would be five round trips becomes one.
|
||||
@@ -34,14 +34,14 @@
|
||||
text: >-
|
||||
You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
|
||||
|
||||
- id: workspace-context
|
||||
name: '@deepseek-ai/dsh-workspace-context'
|
||||
- id: agent-instructions
|
||||
name: '@deepseek-ai/dsh-agent-instructions'
|
||||
config:
|
||||
maxBytes: 65536
|
||||
|
||||
# ── shell ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
||||
# `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
||||
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
@@ -68,27 +68,27 @@
|
||||
config:
|
||||
sampleOverCapGlobResults: false
|
||||
|
||||
# ── background tasks ────────────────────────────────────────────────────────
|
||||
# ── background jobs ────────────────────────────────────────────────────────
|
||||
|
||||
# Only the model-facing controls. The task REGISTRY stays on the host plane:
|
||||
# its producers sit outside any realm this file could put it in — `tool-bash`
|
||||
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
|
||||
# to every sibling row, so `run_in_background` would answer "background tasks
|
||||
# to every sibling row, so `run_in_background` would answer "background jobs
|
||||
# unavailable" while these controls sat in the catalog. The registry is keyed by
|
||||
# owning agent anyway, so one host instance serves every session. What a preset
|
||||
# chooses is whether its agent can collect and stop background work at all.
|
||||
- id: tool-tasks
|
||||
name: '@deepseek-ai/dsh-tool-tasks'
|
||||
- id: tool-jobs
|
||||
name: '@deepseek-ai/dsh-tool-jobs'
|
||||
|
||||
# ── skills ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# The skill REGISTRY lives in the host composition and is layered per scope:
|
||||
# these rows register into THIS preset's layer of it, so they need no realm.
|
||||
# `skill-local` contributes local-root discovery for agents on this preset, and
|
||||
# `skill-filesystem` contributes local-root discovery for agents on this preset, and
|
||||
# `tool-skill` gives them the catalog and loader; the merged catalog also
|
||||
# carries whatever the deployment registered globally (repository plugins).
|
||||
- id: skill-local
|
||||
name: '@deepseek-ai/dsh-skill-local'
|
||||
- id: skill-filesystem
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
@@ -132,7 +132,7 @@
|
||||
|
||||
# ── compaction ──────────────────────────────────────────────────────────────
|
||||
|
||||
# `compact-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
|
||||
# `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
|
||||
# share this realm rather than sit outside it.
|
||||
#
|
||||
# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
|
||||
@@ -140,22 +140,22 @@
|
||||
# keys every fold by Session, and owns the context-meter projection units the
|
||||
# browser reads for every session — behind a realm those units would come and go
|
||||
# with whichever presets happen to be mounted. What a preset chooses is whether
|
||||
# its agent compacts at all, which is `compact-basic` below.
|
||||
# its agent compacts at all, which is `compaction-basic` below.
|
||||
- id: compaction
|
||||
name: cordis:group
|
||||
group: true
|
||||
isolate:
|
||||
compact: true
|
||||
toolResultPrune: true
|
||||
compaction: true
|
||||
toolResultPruner: true
|
||||
config:
|
||||
- id: compact-basic
|
||||
name: '@deepseek-ai/dsh-compact-basic'
|
||||
- id: compaction-basic
|
||||
name: '@deepseek-ai/dsh-compaction-basic'
|
||||
|
||||
- id: command-compact
|
||||
name: '@deepseek-ai/dsh-command-compact'
|
||||
|
||||
- id: tool-result-prune
|
||||
name: '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
- id: tool-result-pruner
|
||||
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
||||
config:
|
||||
thresholdChars: 8192
|
||||
headChars: 4096
|
||||
@@ -176,7 +176,7 @@
|
||||
name: cordis:group
|
||||
group: true
|
||||
isolate:
|
||||
workflows: true
|
||||
workflowEngine: true
|
||||
config:
|
||||
- id: tool-subagent-control
|
||||
name: '@deepseek-ai/dsh-tool-subagent-control'
|
||||
@@ -219,8 +219,8 @@
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
|
||||
- id: workflow-workerthread
|
||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||
- id: workflow-worker-thread
|
||||
name: '@deepseek-ai/dsh-workflow-worker-thread'
|
||||
config:
|
||||
provider: spawn
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
# Code Mode for this agent alone. The row waits for the host's `codeRuntime`
|
||||
# rather than assuming it: a deployment that composes no TypeScript runtime
|
||||
# fails this preset at mount, naming this id, instead of at the first request.
|
||||
- id: tool-mode
|
||||
name: '@deepseek-ai/dsh-agent-tool-mode'
|
||||
- id: tool-presentation
|
||||
name: '@deepseek-ai/dsh-agent-tool-presentation'
|
||||
config:
|
||||
mode: code
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name: 代码模式
|
||||
name: PTC 模式
|
||||
description: 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
order: 2
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
|
||||
Load the `editing-cordis-compositions` skill before writing or changing a composition.
|
||||
|
||||
- id: workspace-context
|
||||
name: '@deepseek-ai/dsh-workspace-context'
|
||||
- id: agent-instructions
|
||||
name: '@deepseek-ai/dsh-agent-instructions'
|
||||
config:
|
||||
maxBytes: 65536
|
||||
|
||||
# ── shell ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
||||
# `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
||||
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
@@ -62,17 +62,17 @@
|
||||
config:
|
||||
sampleOverCapGlobResults: false
|
||||
|
||||
# ── background tasks ────────────────────────────────────────────────────────
|
||||
# ── background jobs ────────────────────────────────────────────────────────
|
||||
|
||||
# Only the model-facing controls. The task REGISTRY stays on the host plane:
|
||||
# its producers sit outside any realm this file could put it in — `tool-bash`
|
||||
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
|
||||
# to every sibling row, so `run_in_background` would answer "background tasks
|
||||
# to every sibling row, so `run_in_background` would answer "background jobs
|
||||
# unavailable" while these controls sat in the catalog. The registry is keyed by
|
||||
# owning agent anyway, so one host instance serves every session. What a preset
|
||||
# chooses is whether its agent can collect and stop background work at all.
|
||||
- id: tool-tasks
|
||||
name: '@deepseek-ai/dsh-tool-tasks'
|
||||
- id: tool-jobs
|
||||
name: '@deepseek-ai/dsh-tool-jobs'
|
||||
|
||||
# ── goals ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
|
||||
# ── compaction ──────────────────────────────────────────────────────────────
|
||||
|
||||
# `compact-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
|
||||
# `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
|
||||
# share this realm rather than sit outside it.
|
||||
#
|
||||
# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
|
||||
@@ -121,22 +121,22 @@
|
||||
# keys every fold by Session, and owns the context-meter projection units the
|
||||
# browser reads for every session — behind a realm those units would come and go
|
||||
# with whichever presets happen to be mounted. What a preset chooses is whether
|
||||
# its agent compacts at all, which is `compact-basic` below.
|
||||
# its agent compacts at all, which is `compaction-basic` below.
|
||||
- id: compaction
|
||||
name: cordis:group
|
||||
group: true
|
||||
isolate:
|
||||
compact: true
|
||||
toolResultPrune: true
|
||||
compaction: true
|
||||
toolResultPruner: true
|
||||
config:
|
||||
- id: compact-basic
|
||||
name: '@deepseek-ai/dsh-compact-basic'
|
||||
- id: compaction-basic
|
||||
name: '@deepseek-ai/dsh-compaction-basic'
|
||||
|
||||
- id: command-compact
|
||||
name: '@deepseek-ai/dsh-command-compact'
|
||||
|
||||
- id: tool-result-prune
|
||||
name: '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
- id: tool-result-pruner
|
||||
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
||||
config:
|
||||
thresholdChars: 8192
|
||||
headChars: 4096
|
||||
@@ -163,7 +163,7 @@
|
||||
name: cordis:group
|
||||
group: true
|
||||
isolate:
|
||||
workflows: true
|
||||
workflowEngine: true
|
||||
config:
|
||||
- id: tool-subagent-control
|
||||
name: '@deepseek-ai/dsh-tool-subagent-control'
|
||||
@@ -206,8 +206,8 @@
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
|
||||
- id: workflow-workerthread
|
||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||
- id: workflow-worker-thread
|
||||
name: '@deepseek-ai/dsh-workflow-worker-thread'
|
||||
config:
|
||||
provider: spawn
|
||||
|
||||
@@ -252,8 +252,8 @@
|
||||
# Both rows register into THIS preset's layer of the host skill registry, so
|
||||
# they need no realm; the agent's merged catalog also carries whatever the
|
||||
# deployment registered globally (repository plugins).
|
||||
- id: skill-local
|
||||
name: '@deepseek-ai/dsh-skill-local'
|
||||
- id: skill-filesystem
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
config:
|
||||
customSkillDirs:
|
||||
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
---
|
||||
name: cordis-plugin-development
|
||||
description: Create, modify, debug, or extend dynamic Cordis Plugins, including Host Services and Events, Client Slot and theme UI, Package-private Client-to-Host calls, dynamic Tools, version updates, approval failures, and runtime diagnostics. Use this Skill to route a user request to the correct platform and Inspect Provider, then define, run, repair, or roll back the Plugin.
|
||||
---
|
||||
|
||||
# Develop Dynamic Cordis Plugins
|
||||
|
||||
First determine whether a capability belongs on Host or Client, then query the real interface before writing code. Never infer a complete API from a Service name, Event payload, Slot props, theme token, or example.
|
||||
|
||||
## Standard workflow
|
||||
|
||||
1. Call `cordis_inspect_list` to obtain the Providers, methods, and schemas currently registered on Host and Client.
|
||||
2. Select the smallest set of `cordis_inspect_query` calls needed to read the exact Services, Events, Builtins, Slots, Theme tokens, or Tools that the implementation will use.
|
||||
3. For a new Plugin, design its first Package. To modify an existing Plugin, first use `cordis_inspect_self(pluginId, packageId)` to read the base source and diagnostics.
|
||||
4. Write plain JavaScript in `code.host`, `code.client`, or both, then call `cordis_define`.
|
||||
5. Call `cordis_run` with the final `pluginId` and `packageId` returned by define.
|
||||
6. Handle approval, waiting, Client loading, and render failures from the Run card, steering messages, or `cordis_inspect_self`.
|
||||
7. Use `cordis_stop` to disable the Plugin temporarily. Use `cordis_undefine` only when it is no longer needed.
|
||||
|
||||
Do not wait in the same turn for user approval or asynchronous browser results. After `cordis_run` returns `awaiting-approval` or `starting`, end the current Tool flow and wait for the system to report the final outcome through state updates and steering.
|
||||
|
||||
## Tool usage guidance
|
||||
|
||||
| Tool | Use it when | Do not |
|
||||
| --- | --- | --- |
|
||||
| `cordis_inspect_list` | Discover current Host/Client Providers and method schemas in one call; refresh after the runtime capability directory changes | Hard-code Provider names and skip list; treat a manifest as business data |
|
||||
| `cordis_inspect_query` | Confirm exact Service methods, Event modes, Builtins, Slots, tokens, or Tool schemas before writing code | Use it instead of calling a real Service from the Plugin; assume a Client query will finish without a responding page |
|
||||
| `cordis_inspect_self` | List current Plugins, inspect version pointers, or read exact Package source and runtime diagnostics | Fetch all source just to build a list; use it to modify or start a Plugin |
|
||||
| `cordis_define` | Create a Plugin's first version or append an immutable Package to an existing Plugin; let the user preview the code first | Expect define to execute `apply`, request approval, or update current |
|
||||
| `cordis_run` | Activate an exact Package; use `run` for first activation, restart, or rollback, and `update` to switch versions | Use `run` to switch versions implicitly; treat pending or starting as success |
|
||||
| `cordis_stop` | Pause current effects while preserving Packages, grants, and version pointers for later use | Use stop to mean permanent deletion |
|
||||
| `cordis_undefine` | Permanently remove a Plugin and all of its Packages and clear historical business views | Call it while rollback, inspection, or restart is still needed |
|
||||
|
||||
## Choose a platform
|
||||
|
||||
| Requirement | Preferred platform | Inspect first |
|
||||
| --- | --- | --- |
|
||||
| Files, commands, processes, or networking | Host | `fs`, `bash`, `subprocess`, `pty`, and `web` in `Service.listService` |
|
||||
| Agents, durable Session data, or Host lifecycle | Host | The relevant Service and `Event.listEvents` |
|
||||
| Register a dynamic Tool callable in the next model step | Host | `harness` in `Builtin.listBuiltins`, plus `Tool.listTools` |
|
||||
| Page theme, layout, or current page state | Client | `Theme.listTokens` and Client `Service.listService` |
|
||||
| Conversation Snapshot or session/workspace lists | Client | The target Slot's standard props and owner props |
|
||||
| Settings pages, sidebars, input areas, overlays, or Tool cards | Client | `Slots.listSubTree` |
|
||||
| Fetch on Host and display on Client | Both | Host Service + `harness.handle`; Client Slot + `host.call` |
|
||||
|
||||
Prefer the capability closest to the data owner. If Slot props already provide the Conversation Snapshot, do not fetch it again through Host. If only the Package's own styles need to change, do not override the global theme. If only a small entry point is needed, do not replace an entire product UI region.
|
||||
|
||||
## Provider navigation
|
||||
|
||||
Select methods from the actual `cordis_inspect_list` result. Common initial methods include:
|
||||
|
||||
- `Service.listService`: without `service`, returns every callable Service with its purpose and exact method signatures. Query the selected `service` again for access rules, structured method descriptions/parameters/returns, and only its referenced types.
|
||||
- `Event.listEvents`: without `event`, returns every Event with its purpose, dispatch mode, and exact listener signature. Query the selected `event` again for its structured listener contract and only its referenced types; a Waterfall listener must call `next()`.
|
||||
- `Builtin.listBuiltins`: returns evaluator-provided symbols and signatures that cannot be obtained through `ctx.get()`.
|
||||
- `Slots.listSubTree`: without `root`, returns compact live trees with each Slot's purpose, kind, scope, registration keys, replacement risk, and children. With an exact `root`, it also returns that selected Slot's full contract, props, and current occupants while keeping descendants compact.
|
||||
- `Theme.listTokens`: returns theme tokens that may currently be queried and overridden; it does not modify the theme.
|
||||
- `Tool.listTools`: returns Tool schemas actually visible to the current Agent, including dynamically registered Tools.
|
||||
|
||||
Provider names, methods, and inputs must come from the current list result. The Service/Event Catalog describes which interfaces this version permits; it does not guarantee that a Service is currently mounted. At runtime, use real Services and Events rather than caching or displaying Catalog query results.
|
||||
|
||||
## Execution environment
|
||||
|
||||
Both `code.host` and `code.client` are plain JavaScript function bodies that return a Cordis Plugin. They are not compiled by TypeScript, JSX, or a bundler.
|
||||
|
||||
Do not use:
|
||||
|
||||
- `import`, `require`, TypeScript types, `as`, decorators, or JSX;
|
||||
- globals not confirmed by `Builtin.listBuiltins`;
|
||||
- guessed access to `window`, `document`, `process`, `Buffer`, `fetch`, or native timers.
|
||||
|
||||
Client React code must use `React.createElement(...)`.
|
||||
|
||||
Correct:
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
const slots = ctx.get('slots')
|
||||
if (slots === undefined) return
|
||||
slots.inject('tool.view.cordis', () => slots.register(
|
||||
{ name: 'tool.view.cordis', key: 'self' },
|
||||
() => React.createElement('div', null, 'Hello'),
|
||||
))
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Incorrect:
|
||||
|
||||
```jsx
|
||||
return {
|
||||
apply(ctx) {
|
||||
return <div>Hello</div>
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
JSX is not the only problem in this example. `apply()` registers lifecycle contributions and cannot return a React Element as the Plugin result. UI must be registered in a queried Slot.
|
||||
|
||||
## Access Services
|
||||
|
||||
Read optional capabilities with `ctx.get(name)` by default and handle their absence:
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
const service = ctx.get('serviceName')
|
||||
if (service === undefined) return
|
||||
service.someMethod()
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Declare `inject` only when a Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears:
|
||||
|
||||
```js
|
||||
return {
|
||||
inject: ['requiredService'],
|
||||
apply(ctx) {
|
||||
ctx.requiredService.someMethod()
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Do not overuse `inject` merely to avoid an `undefined` check. Do not access `ctx.requiredService` without declaring the injection; the Guard rejects undeclared dependencies.
|
||||
|
||||
## Manage side effects
|
||||
|
||||
Every contribution must be removed after the Plugin is stopped, updated, or removed. Prefer Cordis lifecycle APIs:
|
||||
|
||||
- Use `ctx.on()` to register Event listeners.
|
||||
- Use `ctx.effect()` to own an external subscription that returns a disposer.
|
||||
- Retain disposers returned by Cordis Service, Tool, Slot, timer, and theme APIs.
|
||||
- Do not create process-wide or page-wide side effects at module scope or outside `apply()`.
|
||||
|
||||
Recommended:
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
const service = ctx.get('serviceName')
|
||||
if (service === undefined) return
|
||||
ctx.effect(() => service.subscribe((value) => {
|
||||
console.log(value)
|
||||
}))
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If `subscribe()` does not return a disposer, first query whether the Service provides a supported cleanup mechanism. Do not assume unload automatically removes arbitrary third-party callbacks.
|
||||
|
||||
## Host and Client timers
|
||||
|
||||
On both platforms, the timer is a Service named `timer` with the same interface; it is not a Builtin. Query `{ "service": "timer" }` through the corresponding platform's `Service.listService` before using it. Declare `inject: ['timer']` before using the timer mixin.
|
||||
|
||||
One-shot delay:
|
||||
|
||||
```js
|
||||
return {
|
||||
inject: ['timer'],
|
||||
apply(ctx) {
|
||||
const onClick = () => {
|
||||
ctx.timeout(() => console.log('done'), 300)
|
||||
}
|
||||
// Pass onClick to a queried Slot UI.
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Periodic work in a React component:
|
||||
|
||||
```js
|
||||
return {
|
||||
inject: ['timer'],
|
||||
apply(ctx) {
|
||||
function Clock() {
|
||||
React.useEffect(() => ctx.interval(() => console.log('tick'), 1000), [])
|
||||
return React.createElement('div', null, 'Running')
|
||||
}
|
||||
// Register Clock in a queried Slot.
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Incorrect:
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
ctx.timeout(() => console.log('invalid'), 300)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
setTimeout(() => console.log('invalid'), 300)
|
||||
```
|
||||
|
||||
The first example does not declare the timer hard dependency. The second uses a global timer that does not exist.
|
||||
|
||||
## Listen to Events
|
||||
|
||||
Query the Event Provider first to confirm the platform, parameter order, return value, and `mode`.
|
||||
|
||||
Ordinary emit Event:
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
ctx.on('some/event', (payload) => {
|
||||
console.log(payload)
|
||||
})
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The last parameter of a Waterfall Event is `next`. Unless the listener intentionally stops downstream processing, it must call and return it:
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
ctx.on('some/waterfall', (payload, next) => {
|
||||
console.log(payload)
|
||||
return next()
|
||||
})
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Register Client UI
|
||||
|
||||
Query `Slots.listSubTree` without `root` to choose a target from the compact purpose and topology tree, then query the exact Slot with `root` before writing its registration. The exact result determines:
|
||||
|
||||
- the Slot's purpose in the layout;
|
||||
- whether its registration protocol is `single`, `list`, `keyed`, or `chain`;
|
||||
- registration options;
|
||||
- scope standard props and business owner props;
|
||||
- current occupants, replacement risks, and descendant Slots.
|
||||
|
||||
Use `ctx.get('slots')` and handle its absence. Then use `slots.inject` to wait for the Slot declaration and call `slots.register` inside the callback:
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
const slots = ctx.get('slots')
|
||||
if (slots === undefined) return
|
||||
slots.inject('target.slot', () => slots.register(
|
||||
{ name: 'target.slot', id: 'my-view' },
|
||||
(props) => React.createElement('div', null, String(props.someValue)),
|
||||
))
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`ctx.get('slots')` does not require an injection. Do not rewrite it as `ctx.slots` unless `inject: ['slots']` is declared:
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
ctx.slots.register({ name: 'target.slot' }, () => null)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Do not guess an `id`, `key`, selector, or props before querying the Slot protocol. Do not default to root-level `root`, `sidebar`, `conversation`, or `details` Slots; replacing an entire occupant also removes the descendant Slots it declares.
|
||||
|
||||
### Settings pages
|
||||
|
||||
A full settings UI should usually register its own section through `settings.section` to obtain a complete content area. `settings.general.item` is only appropriate for one compact, general-purpose preference. Query the actual subtree, options, and props for both, then select the narrowest entry point that is still sufficient.
|
||||
|
||||
Dynamic Plugins are temporary and process-local, so their settings UI does not need persistent storage. Do not add durable settings or another persistence mechanism for it. Register the UI in the appropriate settings Slot and keep any transient interaction state in memory for the lifetime of the Plugin.
|
||||
|
||||
### Session and page data
|
||||
|
||||
A session-scoped Slot may provide `useSession`, `useSessions`, `useWorkspaces`, `useProjection`, input state, or actions through standard props. Follow the query result and prefer owner or standard props directly; do not add a Host RPC for data already present there.
|
||||
|
||||
Select only the fields that the UI actually needs. Do not copy or render an entire Conversation Snapshot, Session, Tool call, or Slot props object.
|
||||
|
||||
### Cordis Run-specific panel
|
||||
|
||||
To place interactive UI in the latest `cordis_run` card, register `tool.view.cordis` with `key: 'self'`:
|
||||
|
||||
When the feature needs user interaction tied to this Package's result, this region is often a good fit because it keeps the controls in the conversation flow beside the Run card. It is not the default target for every Client UI: settings, sidebars, message actions, and overlays should use their own queried Slots when those locations better match the feature.
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
const slots = ctx.get('slots')
|
||||
if (slots === undefined) return
|
||||
slots.inject('tool.view.cordis', () => slots.register(
|
||||
{ name: 'tool.view.cordis', key: 'self' },
|
||||
(props) => React.createElement('div', null, `Package ${props.packageId}`),
|
||||
))
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
At runtime, `self` binds to `pluginId + packageId`. Do not include `pluginRunId` in the key. When the same Package runs multiple times, the latest Run card hosts the UI and older cards automatically degrade.
|
||||
|
||||
### Ordinary Tool cards
|
||||
|
||||
To customize the call card for an ordinary model Tool, query `tool.call.toolview`. Its key is the Tool name; registering an existing key may replace the product's default card. When customizing only a newly added Tool, first verify its schema with `Tool.listTools`, then query the complete `ToolCallOwnerProps`.
|
||||
|
||||
### Overlays and local entry points
|
||||
|
||||
- For toasts, status notices, and frame-wide overlays, query `shell.overlay` first; observe its pointer-events and ordering rules.
|
||||
- When the selected target is a global overlay Slot, decide whether the UI should be draggable, how the user shows and hides it, and which existing layers it must cover or remain below.
|
||||
- For small sidebar actions, prefer additive inner Slots such as `sidebar.footer.action`; do not replace the entire sidebar.
|
||||
- For supplementary content after a conversation turn, query `conversation.chat.turnTail` and register according to its returned chain selector and fallback rules.
|
||||
|
||||
## Themes and styles
|
||||
|
||||
Determine the scope of the change first:
|
||||
|
||||
1. Global theme: first query `Theme.listTokens`, then query `{ "service": "theme" }` through Client `Service.listService`. Supply light and dark values for each override as required by the query, and retain the returned disposer.
|
||||
2. The Package's own components: use `styles.insert(css)` and prefer theme CSS variables for colors.
|
||||
3. New visible content: choose a Slot first, then decide between local CSS and global tokens.
|
||||
|
||||
Do not manipulate `document.body`, `window`, or hard-coded product DOM selectors. The theme Service changes tokens but does not create UI. Slots create UI but do not replace the theme system.
|
||||
|
||||
## Call Host from Client
|
||||
|
||||
Host registers a Package-private method with `harness.handle(method, handler)`, and Client invokes it with `host.call(method, args)`. This is Client→Host JSON RPC.
|
||||
|
||||
Host:
|
||||
|
||||
```js
|
||||
return {
|
||||
apply(ctx) {
|
||||
harness.handle('read-state', async (args) => {
|
||||
return { value: args.key }
|
||||
})
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Client:
|
||||
|
||||
```js
|
||||
return {
|
||||
async apply(ctx) {
|
||||
const result = await host.call('read-state', { key: 'demo' })
|
||||
console.log(result.value)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Arguments and return values must be lossless JSON. Do not pass functions, React elements, class instances, Contexts, Services, or other runtime objects; return `null` when there is no response data. Do not register a public Remote Service or use `ctx.remote` for Package-private communication.
|
||||
|
||||
## Register a dynamic model Tool
|
||||
|
||||
Host can use `harness` to register a Tool callable in the next model step. First query the current `harness` signature with Host `Builtin.listBuiltins`, then inspect existing Tool names and schemas with `Tool.listTools` to avoid conflicts.
|
||||
|
||||
Tool arguments and return values must be JSON-compatible. `execute` owns the business result; render and presentation own only what the model and native UI see. Tool registration must belong to the current Plugin Fiber so it is automatically removed after stop or update.
|
||||
|
||||
## Handle internal live data
|
||||
|
||||
Service instances, Event payloads, Slot props, Session and Conversation Snapshots, Tool state, and other DSH/Cordis objects are internal live data.
|
||||
|
||||
Do not:
|
||||
|
||||
- call `JSON.stringify` or `structuredClone` on these objects or their descendants;
|
||||
- recursively enumerate, fully copy, or display them as a whole;
|
||||
- place Host objects in the Package's long-lived state or RPC return values.
|
||||
|
||||
Read only the leaf fields required by the current feature. Extract the minimum strings, numbers, booleans, and other scalar values before constructing owned JSON.
|
||||
|
||||
## Versions, approval, and repair
|
||||
|
||||
- A Plugin is the stable instance identified by `pluginId`.
|
||||
- A Package is an immutable code version identified by `packageId`.
|
||||
- Every activation attempt has its own `pluginRunId`.
|
||||
- `currentPackageId` is the latest successful version; it does not imply that the Plugin is currently running.
|
||||
- `nextPackageId` is the target awaiting approval, activating, awaiting Client activation, or most recently failed.
|
||||
|
||||
Choose the `cordis_run` mode as follows:
|
||||
|
||||
| Current state | Target | mode |
|
||||
| --- | --- | --- |
|
||||
| No current | Any Package under the Plugin | `run` |
|
||||
| Has current | The same Package | `run` |
|
||||
| Has current | A different Package | `update` |
|
||||
| Update failed | `nextPackageId` | `update` to retry |
|
||||
| Update failed | `currentPackageId` | `run` to roll back |
|
||||
|
||||
An unauthorized Client Package returns `awaiting-approval`. A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains after a technical runtime failure. An authorized Package returns `starting` and completes asynchronously in the browser.
|
||||
|
||||
After a technical failure:
|
||||
|
||||
1. Use `cordis_inspect_self(pluginId, packageId)` to read the failed version's source and exact diagnostics.
|
||||
2. If the error involves an unknown capability, list and query the corresponding Provider again.
|
||||
3. Define a new Package under the same Plugin; do not overwrite the failed Package.
|
||||
4. Run again with the new `packageId` and the correct mode.
|
||||
|
||||
Do not retry automatically after the user rejects approval. A failed update does not automatically restore the old physical Run; explicitly run current when recovery is required.
|
||||
|
||||
## Modify @pluginId
|
||||
|
||||
When the user identifies a target with `@pluginId`, do not create another Plugin. The injected context contains only identity, version pointers, and the default base Package, not source code.
|
||||
|
||||
Modify it as follows:
|
||||
|
||||
1. Read the base Package with `cordis_inspect_self(pluginId, packageId)`.
|
||||
2. Preserve the Host or Client half that does not need to change and modify only the target code.
|
||||
3. Call `cordis_define` with `plugin.kind: 'existing'` and the original `pluginId`.
|
||||
4. Use the returned `packageId`; when current exists, activate the new version with `update` in the usual case.
|
||||
|
||||
If the reference is unavailable, explain that the Plugin was removed, belongs to another Session, or was lost on process restart. Do not create a same-named replacement.
|
||||
|
||||
## Common failure checks
|
||||
|
||||
| Failure | Check first |
|
||||
| --- | --- |
|
||||
| `service "x" is not declared` | Whether code uses `ctx.x` without declaring `inject: ['x']` on the Plugin object; switch to `ctx.get('x')` with an absence check or declare a true hard dependency |
|
||||
| `cannot get property "timer" without inject` | Query the timer Service and declare `inject: ['timer']` |
|
||||
| Client parse failure | Whether the code uses JSX, TypeScript, import, or an unavailable global |
|
||||
| Slot registration failure | Whether the live subtree was queried, the Slot exists, and options, key, or selector satisfy the returned protocol |
|
||||
| UI loads but the page reports an error | Inspect the `client-render` diagnostic and stack; the error belongs to an exact Run, so define a new Package to repair it |
|
||||
| `host.call` failure | The Host handler name, current `pluginRunId`, JSON arguments, and real Service dependencies inside the handler |
|
||||
| Update failure | Preserve current/next semantics; repair next and update, or run current to roll back |
|
||||
@@ -88,8 +88,8 @@ When a preset genuinely owns a service, wrap the provider **and every consumer t
|
||||
isolate:
|
||||
workflows: true
|
||||
config:
|
||||
- id: workflow-workerthread
|
||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||
- id: workflow-worker-thread
|
||||
name: '@deepseek-ai/dsh-workflow-worker-thread'
|
||||
config:
|
||||
provider: spawn
|
||||
- id: tool-workflow
|
||||
@@ -100,7 +100,7 @@ When a preset genuinely owns a service, wrap the provider **and every consumer t
|
||||
|
||||
A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.
|
||||
|
||||
Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-tasks`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.
|
||||
Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.
|
||||
|
||||
## Verifying a change
|
||||
|
||||
@@ -147,7 +147,7 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o
|
||||
maxDepth: provider-managed
|
||||
```
|
||||
|
||||
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Task id. Full presets already carry `tool-tasks`, while the host carries the task registry; retain both when making a custom composition so `task_output`, `task_list`, `task_kill`, cancellation, and completion notices stay available. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
|
||||
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the host carries the job registry; retain both when making a custom composition so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
|
||||
|
||||
## What not to move into a preset
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# The `minimal` agent preset: a fixed-prompt, two-tool coding-agent composition.
|
||||
#
|
||||
# The persona is the complete system prompt, so global identity, Web orientation,
|
||||
# tool guidance, and later assembly listeners cannot add prompt text. The model
|
||||
# composes only the persistent `bash` and `str_replace_editor` tools. Context
|
||||
# compaction is deliberately absent.
|
||||
# tool guidance, and later assembly listeners cannot add prompt text. Runtime
|
||||
# context snapshots are suppressed for this preset, and the model composes only
|
||||
# persistent `bash` and `str_replace_editor`. Context compaction is absent.
|
||||
|
||||
- id: persona
|
||||
name: '@deepseek-ai/dsh-persona'
|
||||
config:
|
||||
text: You are a helpful software engineer assistant.
|
||||
complete: true
|
||||
includeRuntimeContext: false
|
||||
|
||||
# The PTY registry is an agent-owned service, so it lives in an entry-local
|
||||
# realm. The backend still consumes the host sandbox policy and subprocess
|
||||
@@ -18,13 +19,13 @@
|
||||
name: cordis:group
|
||||
group: true
|
||||
isolate:
|
||||
pty: true
|
||||
terminals: true
|
||||
config:
|
||||
- id: pty
|
||||
name: '@deepseek-ai/dsh-pty'
|
||||
name: '@deepseek-ai/dsh-terminal'
|
||||
|
||||
- id: pty-local
|
||||
name: '@deepseek-ai/dsh-pty-local'
|
||||
- id: terminal-bash
|
||||
name: '@deepseek-ai/dsh-terminal-bash'
|
||||
config:
|
||||
timeoutMs: 300000
|
||||
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
text: >-
|
||||
You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
|
||||
|
||||
- id: workspace-context
|
||||
name: '@deepseek-ai/dsh-workspace-context'
|
||||
- id: agent-instructions
|
||||
name: '@deepseek-ai/dsh-agent-instructions'
|
||||
config:
|
||||
maxBytes: 65536
|
||||
|
||||
# ── shell ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
||||
# `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
||||
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
@@ -61,27 +61,27 @@
|
||||
config:
|
||||
sampleOverCapGlobResults: false
|
||||
|
||||
# ── background tasks ────────────────────────────────────────────────────────
|
||||
# ── background jobs ────────────────────────────────────────────────────────
|
||||
|
||||
# Only the model-facing controls. The task REGISTRY stays on the host plane:
|
||||
# its producers sit outside any realm this file could put it in — `tool-bash`
|
||||
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
|
||||
# to every sibling row, so `run_in_background` would answer "background tasks
|
||||
# to every sibling row, so `run_in_background` would answer "background jobs
|
||||
# unavailable" while these controls sat in the catalog. The registry is keyed by
|
||||
# owning agent anyway, so one host instance serves every session. What a preset
|
||||
# chooses is whether its agent can collect and stop background work at all.
|
||||
- id: tool-tasks
|
||||
name: '@deepseek-ai/dsh-tool-tasks'
|
||||
- id: tool-jobs
|
||||
name: '@deepseek-ai/dsh-tool-jobs'
|
||||
|
||||
# ── skills ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# The skill REGISTRY lives in the host composition and is layered per scope:
|
||||
# these rows register into THIS preset's layer of it, so they need no realm.
|
||||
# `skill-local` contributes local-root discovery for agents on this preset, and
|
||||
# `skill-filesystem` contributes local-root discovery for agents on this preset, and
|
||||
# `tool-skill` gives them the catalog and loader; the merged catalog also
|
||||
# carries whatever the deployment registered globally (repository plugins).
|
||||
- id: skill-local
|
||||
name: '@deepseek-ai/dsh-skill-local'
|
||||
- id: skill-filesystem
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
@@ -125,7 +125,7 @@
|
||||
|
||||
# ── compaction ──────────────────────────────────────────────────────────────
|
||||
|
||||
# `compact-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
|
||||
# `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
|
||||
# share this realm rather than sit outside it.
|
||||
#
|
||||
# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
|
||||
@@ -133,22 +133,22 @@
|
||||
# keys every fold by Session, and owns the context-meter projection units the
|
||||
# browser reads for every session — behind a realm those units would come and go
|
||||
# with whichever presets happen to be mounted. What a preset chooses is whether
|
||||
# its agent compacts at all, which is `compact-basic` below.
|
||||
# its agent compacts at all, which is `compaction-basic` below.
|
||||
- id: compaction
|
||||
name: cordis:group
|
||||
group: true
|
||||
isolate:
|
||||
compact: true
|
||||
toolResultPrune: true
|
||||
compaction: true
|
||||
toolResultPruner: true
|
||||
config:
|
||||
- id: compact-basic
|
||||
name: '@deepseek-ai/dsh-compact-basic'
|
||||
- id: compaction-basic
|
||||
name: '@deepseek-ai/dsh-compaction-basic'
|
||||
|
||||
- id: command-compact
|
||||
name: '@deepseek-ai/dsh-command-compact'
|
||||
|
||||
- id: tool-result-prune
|
||||
name: '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
- id: tool-result-pruner
|
||||
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
||||
config:
|
||||
thresholdChars: 8192
|
||||
headChars: 4096
|
||||
@@ -175,7 +175,7 @@
|
||||
name: cordis:group
|
||||
group: true
|
||||
isolate:
|
||||
workflows: true
|
||||
workflowEngine: true
|
||||
config:
|
||||
- id: tool-subagent-control
|
||||
name: '@deepseek-ai/dsh-tool-subagent-control'
|
||||
@@ -218,8 +218,8 @@
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
|
||||
- id: workflow-workerthread
|
||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||
- id: workflow-worker-thread
|
||||
name: '@deepseek-ai/dsh-workflow-worker-thread'
|
||||
config:
|
||||
provider: spawn
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
|
||||
"version": "0.0.1-rc.2",
|
||||
"version": "0.0.1-rc.5",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
@@ -18,39 +18,42 @@
|
||||
"lib/*.js",
|
||||
"config"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/cordis-plugin-hmr": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-include": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-timer": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-tool-mode": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-tool-presentation": "workspace:^",
|
||||
"@deepseek-ai/dsh-app-boot": "workspace:^",
|
||||
"@deepseek-ai/dsh-base": "workspace:^",
|
||||
"@deepseek-ai/dsh-cordis-client-runner": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-compact": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-compact-basic": "workspace:^",
|
||||
"@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^",
|
||||
"@deepseek-ai/dsh-compaction-basic": "workspace:^",
|
||||
"@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal-round-driver": "workspace:^",
|
||||
"@deepseek-ai/dsh-cmdline": "workspace:^",
|
||||
"@deepseek-ai/dsh-environment": "workspace:^",
|
||||
"@deepseek-ai/dsh-launch-environment": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-headless": "workspace:^",
|
||||
"@deepseek-ai/dsh-mcp-client": "workspace:^",
|
||||
"@deepseek-ai/dsh-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-persona": "workspace:^",
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
||||
"@deepseek-ai/dsh-pty": "workspace:^",
|
||||
"@deepseek-ai/dsh-pty-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-terminal": "workspace:^",
|
||||
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-pwsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-time-context": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-tasks-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
|
||||
"@deepseek-ai/dsh-jobs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-tmux-context": "workspace:^",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
|
||||
@@ -62,18 +65,18 @@
|
||||
"@deepseek-ai/dsh-tool-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-pwsh": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-schedule": "workspace:^",
|
||||
"@deepseek-ai/dsh-schedule": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-jobs": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-todo": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-web": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
|
||||
"@deepseek-ai/dsh-web-app": "workspace:^",
|
||||
"@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace-context": "workspace:^",
|
||||
"@deepseek-ai/dsh-workflow-worker-thread": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-instructions": "workspace:^",
|
||||
"commander": "^15.0.0",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"js-yaml": "^4.2.0",
|
||||
@@ -81,7 +84,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-frontend-static": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-frontend-static": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
|
||||
@@ -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/reference/README.md
|
||||
README.md: e8bc99f7b2b15f0679ec268a8e778e815758c00f
|
||||
README.zh.md: c75c487caa7e2ffd7ca5c4b16f3c6243f0a86f3e
|
||||
README.md: 0b23db293c304807b5959f5eae9a2faf868caa62
|
||||
README.zh.md: d812c7cd0d1efaa69b135925f3e922668539781b
|
||||
|
||||
@@ -73,12 +73,12 @@ New sessions default to the `workspace-write` permission preset. Bash and filesy
|
||||
|
||||
## Shared deployment behavior
|
||||
|
||||
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, and session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it.
|
||||
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, and disabled session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it.
|
||||
|
||||
Session events stream as OTLP/HTTP logs by default. `DSH_TELEMETRY_OTLP_URL` selects another collector. Any non-empty `DSH_TELEMETRY_DISABLED` disables the telemetry row before boot. The shipped base has no telemetry redaction rule, so exported records can contain message text, tool arguments and results, and workspace paths; the [telemetry Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md) owns that deployment decision.
|
||||
Session telemetry stays local by default. `DSH_TELEMETRY_MODE=FULL` streams every projected session event as OTLP/HTTP logs, while `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` uploads a session-log suffix only when feedback is recorded. `DSH_TELEMETRY_OTLP_URL` selects another collector, and any non-empty `DSH_TELEMETRY_DISABLED` remains an authoritative hard opt-out. The shipped base has no telemetry redaction rule, so explicitly enabled exports can contain message text, tool arguments and results, and workspace paths; the [default-off Agent Note](../../../.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md) owns that deployment decision.
|
||||
|
||||
Install external plugin bundles through `dsh plugin --profile <name> add <package-or-git-spec>`. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
|
||||
|
||||
## Source execution
|
||||
|
||||
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing TypeRT host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
|
||||
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing Typert host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
|
||||
|
||||
@@ -2,32 +2,32 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
本参考定义 profile、web 别名、插件管理和配置 dump 命令模式。参数由 [`src/args.ts`](../src/args.ts) 统一解析,[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。
|
||||
本参考定义 profile 启动、web 别名、插件管理和配置 dump 等命令模式。argv 由 [`src/args.ts`](../src/args.ts) 统一解析一次,[`src/bin.ts`](../src/bin.ts) 只会动态导入选中的运行器。
|
||||
|
||||
## Profile 启动
|
||||
|
||||
`dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树在空根节点之上按以下顺序逐层组合:profile manifest(元数据清单)的 `dsh.profile.bundles` 列表所列的各个组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(各 profile 共享的机器本地偏好,因此优先级高于逐 profile 的层)、以及按 argv 顺序的各个 `--patch <path>` overlay。后应用的层按行胜出;patch 替换目标行完整的 `config` 值,而不是深度合并各键,并且可以插入新行。配置解析、schema 校验、模块解析或插件启动失败会得到报告并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。
|
||||
`dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch <path>` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。
|
||||
|
||||
组合包名称先从 dsh 安装解析,再从 profile 目录解析。因此内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`)总是来自与正在运行的 `dsh` 相同的安装;树外组合包来自 profile 由 pnpm 管理的 `node_modules`。任何 patch 行中的裸插件 `name` 通过 profile 目录的 Node 父目录逐级查找解析,该查找可达到持续维护的安装后备目录 `$DSH_HOME/profiles/node_modules`(安装的应用和组合包所依赖的每个包对应一个符号链接,每次启动时修复)。
|
||||
组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。该目录为 dsh 安装中的应用和组合包所依赖的每个包各维护一个符号链接,并在每次启动时修复这些链接。
|
||||
|
||||
`web` 和 `headless` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app;`headless`:base + headless)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`。
|
||||
|
||||
### 应用参数
|
||||
|
||||
启动器自己的 flag 写在最前面,并在它不认识的第一个 token 处结束;从那里开始的一切都通过 `ctx.cmdlineArgs` 原样交给启动起来的 profile,任何注入它的应用插件都可以解析([`dsh-cmdline`](../../../packages/boot/cmdline/README.md))。因此 `dsh --profile web --port 8080` 到达的是 web 应用的 `--port`,`dsh --profile web --help` 打印的是该应用的 help 且什么也不启动,而 `dsh --help`(没有可以交付的 profile)打印的是启动器自己的 help。`-V`/`--version` 写在应用参数边界之前时会打印启动器的版本。
|
||||
启动器自身的 flag 必须写在最前面,并在遇到第一个无法识别的 token 时结束;从该 token 开始的所有内容都会通过 `ctx.cmdlineArgs` 原样交给已启动的 profile,注入该 profile 的任意应用插件都可以解析这些内容([`dsh-cmdline`](../../../packages/boot/cmdline/README.md))。因此,`dsh --profile web --port 8080` 会将 `--port` 交给 web 应用;`dsh --profile web --help` 只打印该应用的帮助信息,不启动应用;`dsh --help` 没有可供交付参数的 profile,因此会打印启动器自身的帮助信息。`-V`/`--version` 位于应用参数边界之前时,会打印启动器的版本。
|
||||
|
||||
一套组合只挂载一次。普通插件注入 `cmdlineArgs`、解析本应用参数,并把结果作为服务提供出去;由 flag 配置的每一行都会注入该服务,Loader 会等服务激活后再求值该行配置(`port: !!js ctx.webStartup.port ?? 3080`),因此 flag 胜过写在它旁边的值。该优先级要求配置行保留这一表达式;若用户 patch 用字面量替换整份 `config`,运行时读取也会随之消失。help 和被拒绝的参数会请求退出——拒绝时以非零状态,help 时以 0——且不会激活依赖提供方服务的行。在线编辑 `cordis.patch.yml` 会针对仍然在线的服务重新求值表达式,因此不会重置已在服务的端口。
|
||||
每套组合只会挂载一次。普通插件注入 `cmdlineArgs`,解析所属应用的参数,并将解析结果作为服务提供。每个从 flag 取值的配置行都会注入该服务;Loader 会等到服务激活后,再对该行的配置求值(`port: !!js ctx.webStartup.port ?? 3080`),因此 flag 的优先级高于配置行中写明的值。要维持这一优先级,配置行必须保留该表达式;如果用户 patch 用字面量替换整个 `config`,也会随之移除运行时读取。帮助参数和被拒绝的参数都会请求退出:参数被拒绝时以非零状态退出,显示帮助时以 0 退出;依赖该提供方服务的配置行不会激活。在线编辑 `cordis.patch.yml` 时,系统会根据仍在运行的服务重新计算表达式,因此不会重置当前正在使用的端口。
|
||||
|
||||
启动器的 flag 必须写在应用参数之前,且启动器的解析器会消耗掉一个 `--`:必须以字面量 `--` 送达应用的参数需要写成 `-- --`。如果应用的第一个参数恰好等于 `web` 或 `plugin`,会选择对应的子命令。`ctx.cmdlineArgs.get()` 是共享的不可变读取:多个插件可以解析同一份快照,没有读取方的 profile 则会忽略自己的应用参数。
|
||||
|
||||
随附的各应用持有这些命令行:
|
||||
随附的应用接受以下命令行参数:
|
||||
|
||||
| Profile | 参数 |
|
||||
|---|---|
|
||||
| `web` | `--host`、`--port`、可重复的 `--trusted-host` |
|
||||
| `headless` | 任务文本,作为位置参数 |
|
||||
|
||||
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对 Session 执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
|
||||
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对会话执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
|
||||
|
||||
可在不启动的情况下检查组合出的配置树:
|
||||
|
||||
@@ -36,11 +36,11 @@ dsh --profile web --dump-default-config
|
||||
dsh --profile web --patch ./extra.yml --dump-config
|
||||
```
|
||||
|
||||
`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml` 和 `--patch` overlay。两者都会打印注释,标明每行由哪个文件提供,以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。dump 从不运行应用命令行提供方,因此它展示的是任何应用参数被解析之前的组合配置树,并拒绝携带应用参数的调用。
|
||||
`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml` 和 `--patch` overlay。两者都会打印注释,标明每行由哪个文件提供,以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。dump 操作不会运行应用的命令行参数提供方,因此展示的是解析任何应用参数之前的组合配置树;如果调用中包含应用参数,dump 会拒绝该调用。
|
||||
|
||||
## 插件管理
|
||||
|
||||
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,`dsh.profile.bundles` 都会与已安装状态对齐:每个解析到 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的包的依赖加入层栈(因此让包获得该声明的 `update` 会将其激活),没有组合包声明的依赖保持为普通依赖并给出一次性警告,已移除的依赖则退出层栈。
|
||||
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。
|
||||
|
||||
```sh
|
||||
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
|
||||
@@ -48,7 +48,7 @@ dsh plugin --profile tui remove turtle-ui
|
||||
dsh --profile tui
|
||||
```
|
||||
|
||||
Git 托管、随附源码的插件在安装期间通过其 `prepare` 脚本构建,而 pnpm ≥10 在消费方允许之前会阻止该脚本:首次 `add` 会失败并给出 pnpm 的 `allowBuilds` 提示(以及 dsh 指向该 profile 的 `pnpm-workspace.yaml` 的指引);把打印出的键复制到那里并重新运行即可。安装已构建的 tarball 或本地 checkout 不需要任何允许。
|
||||
随源码发布的 Git 托管插件会在安装期间通过 `prepare` 脚本构建,而 pnpm ≥10 默认会阻止该脚本,直到使用方明确允许。首次运行 `add` 会失败,并显示 pnpm 的 `allowBuilds` 提示;dsh 还会提示应修改该 profile 的 `pnpm-workspace.yaml`。将输出的键复制到该文件后,重新运行命令即可。安装已经构建好的 tarball 或本地 checkout 时,无需加入 `allowBuilds`。
|
||||
|
||||
## Web 别名
|
||||
|
||||
@@ -61,11 +61,11 @@ dsh web --dump-config
|
||||
dsh web --help
|
||||
```
|
||||
|
||||
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
|
||||
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有网络接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
|
||||
|
||||
进程关闭时会给插件树最多 5 秒完成 dispose。第一次 `SIGINT`/`SIGTERM` 启动该优雅排空——`SIGTERM` 是监督进程的普通停止请求,在所有 surface 上以 0 退出,`SIGINT` 报告 130;第二次信号强制立即退出。如果一次性运行正常结束时已经卡在 dispose 中,第一次 `Ctrl+C` 就会升格并立即退出,而不会被吞掉。
|
||||
进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。
|
||||
|
||||
所有模式都将调用目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。每次 profile 启动都监视两个 `cordis.patch.yml` 层(profile 与 home)的有效编辑并以事务方式重新应用;一次性 surface 经由有界关闭退出,关闭会先 dispose 监视器。
|
||||
所有模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。每次启动 profile 时,系统都会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;一次性运行模式通过有界关闭流程退出,该流程会先 dispose 监视器。
|
||||
|
||||
新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。
|
||||
|
||||
@@ -73,12 +73,12 @@ dsh web --help
|
||||
|
||||
## 共享部署行为
|
||||
|
||||
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。
|
||||
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和已禁用的会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。
|
||||
|
||||
会话事件默认作为 OTLP/HTTP 日志流式发送。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。任何非空 `DSH_TELEMETRY_DISABLED` 都会在启动前禁用遥测配置行。随附基础配置没有遥测脱敏规则,因此导出的记录可能包含消息文本、工具参数与结果以及 workspace 路径;该部署决策由[遥测 Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md)负责。
|
||||
会话遥测默认留在本地。`DSH_TELEMETRY_MODE=FULL` 将每条已投影会话事件作为 OTLP/HTTP 日志流式发送,`DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 则仅在记录反馈时上传会话日志后缀。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。任何非空的 `DSH_TELEMETRY_DISABLED` 都是具有最终效力的遥测强制关闭开关。随附基础配置没有遥测脱敏规则,因此显式启用的导出可能包含消息文本、工具参数和结果,以及 workspace 路径;相关部署决策见[默认关闭 Agent Note](../../../.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md)。
|
||||
|
||||
通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。
|
||||
|
||||
## 源码执行
|
||||
|
||||
请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>`。`package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。TypeRT Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY` 和 `HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
|
||||
请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>`。`package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。Typert Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY` 和 `HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
|
||||
|
||||
@@ -29,12 +29,12 @@ import {
|
||||
watchUserPatches,
|
||||
type Profile,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
||||
|
||||
/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */
|
||||
const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', import.meta.url))
|
||||
|
||||
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import { DSH_LAUNCH_ENVIRONMENT_KEY, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
|
||||
|
||||
@@ -54,7 +54,7 @@ export function homePatchPath(): string {
|
||||
export const INSTALL_ANCHOR = fileURLToPath(new URL('../package.json', import.meta.url))
|
||||
|
||||
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */
|
||||
const TELEMETRY_ROW_ID = 'telemetry-otel'
|
||||
const TELEMETRY_ROW_ID = 'session-telemetry-otel'
|
||||
|
||||
/** The empty root entry list every profile tree patches over. */
|
||||
const PROFILE_ROOT_CONFIG = `# dsh profile root — an empty entry list. The tree is composed as patches:
|
||||
@@ -75,7 +75,7 @@ export const PROFILE_ROOT_FILENAME = 'cordis.yml'
|
||||
* switch set.
|
||||
* @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset).
|
||||
* @param hasRow - whether the composition carries the telemetry row.
|
||||
* @returns the disable patch, or `undefined` when telemetry stays enabled or is not mounted.
|
||||
* @returns the disable patch, or `undefined` when no hard-disable patch is required.
|
||||
*/
|
||||
export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: boolean): PatchOptions | undefined {
|
||||
if ((disabledEnv ?? '') === '' || !hasRow) return undefined
|
||||
@@ -173,7 +173,7 @@ function composeProfile(
|
||||
/** Options for {@link runProfile}. */
|
||||
export interface RunProfileOptions {
|
||||
/** This run's frozen environment snapshot, provided before any entry mounts. */
|
||||
environment: EnvironmentSnapshot
|
||||
environment: LaunchEnvironmentSnapshot
|
||||
/** The profile name to boot. */
|
||||
profile: string
|
||||
/** `--patch` overlay paths, in argv order. */
|
||||
@@ -249,7 +249,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
app.current = hostCtx
|
||||
// Before any config-tree entry mounts, so plugins resolve all launch-time
|
||||
// environment values from the same immutable provenance snapshot.
|
||||
hostCtx.provide(DSH_ENVIRONMENT_KEY, options.environment)
|
||||
hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment)
|
||||
// The command line and bounded exit request are launcher facts available
|
||||
// to every app plugin that injects the argument snapshot.
|
||||
provideCmdline(hostCtx, {
|
||||
|
||||
4
apps/cli/tests/fixtures/dsh-badge/cordis.yml
vendored
4
apps/cli/tests/fixtures/dsh-badge/cordis.yml
vendored
@@ -1,9 +1,9 @@
|
||||
- id: skill-badge
|
||||
disabled: false
|
||||
|
||||
- id: skill-local
|
||||
- id: skill-filesystem
|
||||
config:
|
||||
watch: false
|
||||
|
||||
- id: telemetry-otel
|
||||
- id: session-telemetry-otel
|
||||
disabled: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- id: skill-local
|
||||
- id: skill-filesystem
|
||||
config:
|
||||
watch: false
|
||||
|
||||
- id: telemetry-otel
|
||||
- id: session-telemetry-otel
|
||||
disabled: true
|
||||
|
||||
@@ -31,7 +31,7 @@ try {
|
||||
steer: () => {},
|
||||
inject: () => { throw new Error('dsh-badge snapshot must receive the catalog at the step boundary') },
|
||||
cancel: () => {},
|
||||
runMaintenance: task => task(new AbortController().signal),
|
||||
runMaintenance: job => job(new AbortController().signal),
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
const decision = await agentEvents(ctx, agent).waterfall(
|
||||
|
||||
@@ -21,15 +21,22 @@ 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 bundle's patch owns the session-query-sqlite lazy-open row.
|
||||
const configPath = join(repoRoot, 'packages/bundle/web-app/cordis.patch.yml')
|
||||
// Full-text session search ships off (`openAt: never` on both layers): the
|
||||
// base patch carries the default, and the web restatement must not re-enable it.
|
||||
const baseConfigPath = join(repoRoot, 'packages/bundle/base/cordis.patch.yml')
|
||||
const webConfigPath = join(repoRoot, 'packages/bundle/web-app/cordis.patch.yml')
|
||||
const requireBuiltArtifacts = process.env.DSH_REQUIRE_BUILT_CLI_SMOKE === '1'
|
||||
|
||||
interface ConfigRow {
|
||||
id?: string
|
||||
disabled?: unknown
|
||||
config?: { openAt?: unknown }
|
||||
}
|
||||
|
||||
interface PatchEntry extends ConfigRow {
|
||||
insert?: ConfigRow[]
|
||||
}
|
||||
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
construct: value => String(value),
|
||||
@@ -92,12 +99,20 @@ function runBuiltWeb(cwd: string): Promise<{ stdout: string; stderr: string; cod
|
||||
}
|
||||
|
||||
describe.skipIf(!requireBuiltArtifacts)('built CLI lazy-search startup', () => {
|
||||
it('boots and disposes the shipped composition without a SQLite startup warning', async () => {
|
||||
it('boots and disposes the shipped composition with full-text search off by default', 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 baseRows = (yaml.load(await readFile(baseConfigPath, 'utf8'), { schema: configSchema }) as PatchEntry[])
|
||||
.flatMap(entry => entry.insert ?? [entry])
|
||||
const webRows = (yaml.load(await readFile(webConfigPath, 'utf8'), { schema: configSchema }) as PatchEntry[])
|
||||
.flatMap(entry => entry.insert ?? [entry])
|
||||
const baseRow = baseRows.find(row => row.id === 'session-query-sqlite')
|
||||
const webRow = webRows.find(row => row.id === 'session-query-sqlite')
|
||||
expect(baseRow?.config?.openAt).toBe('never')
|
||||
expect(baseRow?.disabled).toBeUndefined()
|
||||
// The web restatement keeps the shipped default; opting in is a later layer's override.
|
||||
expect(webRow?.config?.openAt).toBe('never')
|
||||
expect(webRow?.disabled).toBeUndefined()
|
||||
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'dsh-cli-lazy-search-'))
|
||||
try {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import * as McpClient from '@deepseek-ai/dsh-mcp-client/src/index.ts'
|
||||
|
||||
interface ExampleContract {
|
||||
@@ -123,7 +123,7 @@ describe('third-party memory MCP example overlays', () => {
|
||||
(ctx) => {
|
||||
liveContexts.add(ctx)
|
||||
ctx.loader.builtins['memory-test-system-prompt'] = SystemPrompt
|
||||
ctx.loader.builtins['memory-test-tools'] = ToolRegistry
|
||||
ctx.loader.builtins['memory-test-tools'] = ToolRuntime
|
||||
ctx.loader.builtins['memory-test-mcp-client'] = McpClient
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,14 +2,14 @@ import { describe, expect, it } from 'vitest'
|
||||
import { resolveTelemetryPatch } from '../src/profile-boot.ts'
|
||||
|
||||
describe('resolveTelemetryPatch', () => {
|
||||
it('keeps telemetry enabled when the switch is unset or empty', () => {
|
||||
it('preserves the configured telemetry mode when the hard-disable 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 })
|
||||
expect(resolveTelemetryPatch(value, true)).toEqual({ id: 'session-telemetry-otel', disabled: true })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { resolveSessionPreset, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
|
||||
import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-compact-basic'
|
||||
import type {} from '@deepseek-ai/dsh-compaction-basic'
|
||||
import type {} from '@deepseek-ai/dsh-skill'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
// Type-only: resolves `ctx.get('sessionProjections')` and `ctx.get('tokenMeter')`.
|
||||
@@ -28,6 +28,7 @@ const BASE_PATCH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml')
|
||||
const WEB_PATCH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml')
|
||||
/** The installation anchor whose dependency surface the preset module fallback mirrors. */
|
||||
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
|
||||
const EXAMPLES_INSTALL_ANCHOR = join(REPO_ROOT, 'examples/package.json')
|
||||
const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.'
|
||||
const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
|
||||
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
|
||||
@@ -43,7 +44,11 @@ const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
|
||||
* touch the network, or write outside the test. Everything that decides an
|
||||
* agent's capabilities is the real thing, including both shipped presets.
|
||||
*/
|
||||
async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promise<Context> {
|
||||
async function bootWeb(
|
||||
settingsFile: string,
|
||||
extra: PatchOptions[] = [],
|
||||
extraInstallAnchor?: string,
|
||||
): Promise<Context> {
|
||||
const storageRoot = join(dirname(settingsFile), 'storages')
|
||||
const patches: PatchOptions[] = [
|
||||
...loadOverlayPatches('dsh-test', BASE_PATCH),
|
||||
@@ -67,12 +72,12 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
|
||||
// moved into the presets that a host row still waits for. The boot audit
|
||||
// is that assertion.
|
||||
{ id: 'webserver', disabled: true },
|
||||
// The web bundle's runtime row injects `httpServer`, so it cannot
|
||||
// The web bundle's runtime row injects `webServer`, so it cannot
|
||||
// activate without the bound port disabled above. It owns dist serving
|
||||
// and the URL prompt line — surface glue, not anything that decides an
|
||||
// agent's capabilities, which is all this file asserts.
|
||||
{ id: 'web-runtime', disabled: true },
|
||||
{ id: 'telemetry-otel', disabled: true },
|
||||
{ id: 'session-telemetry-otel', disabled: true },
|
||||
// A deployment-level skill on the host registry's GLOBAL layer — the same
|
||||
// registration shape a repository plugin's skill root uses. The layered
|
||||
// skills test below proves it reaches preset-composed agents.
|
||||
@@ -88,7 +93,7 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
|
||||
{ id: 'directory-picker', disabled: true },
|
||||
{ insert: [
|
||||
{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' },
|
||||
{ id: 'ui-directory-picker', name: '@deepseek-ai/dsh-client-ui-directory-picker' },
|
||||
{ id: 'ui-directory-picker-browse', name: '@deepseek-ai/dsh-client-ui-directory-picker-browse' },
|
||||
] },
|
||||
// The roster AppCLIEntry would patch in; only the shipped root, so a
|
||||
// developer's own `~/.dsh/.preset` cannot change this test's outcome.
|
||||
@@ -110,6 +115,7 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
|
||||
// them resolvable — the same mechanism, not a test-only shim.
|
||||
const home = dirname(settingsFile)
|
||||
healProfilesModuleFallback(INSTALL_ANCHOR, home)
|
||||
if (extraInstallAnchor !== undefined) healProfilesModuleFallback(extraInstallAnchor, home)
|
||||
const profileDir = join(home, 'profiles', 'spec')
|
||||
await mkdir(profileDir, { recursive: true })
|
||||
const rootConfig = join(profileDir, 'cordis.yml')
|
||||
@@ -209,9 +215,8 @@ describe('the shipped Web composition', () => {
|
||||
// depend on ripgrep being present on the machine.
|
||||
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
|
||||
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'get_goal', 'interrupt_agent', 'list_agents', 'ralph', 'read', 'read_image', 'send_message', 'skill',
|
||||
'subagent', 'subagent_fork', 'task_kill',
|
||||
'task_list', 'task_output', 'todo_write', 'update_goal', 'web_search',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'ralph', 'read', 'read_image', 'send_message', 'skill',
|
||||
'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_search',
|
||||
'workflow', 'write',
|
||||
])
|
||||
} finally {
|
||||
@@ -233,8 +238,8 @@ describe('the shipped Web composition', () => {
|
||||
expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)
|
||||
expect(JSON.stringify(assembly.tools.find(tool => tool.name === 'str_replace_editor')?.parameters))
|
||||
.toContain('Absolute path')
|
||||
expect(ctx.agentPresets.serviceFor(handle.agent, 'compact')).toBeUndefined()
|
||||
expect(handle.agent.ctx.get('compact')).toBeUndefined()
|
||||
expect(ctx.agentPresets.serviceFor(handle.agent, 'compaction')).toBeUndefined()
|
||||
expect(handle.agent.ctx.get('compaction')).toBeUndefined()
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -271,7 +276,10 @@ describe('the shipped Web composition', () => {
|
||||
try {
|
||||
const tools = toolNames(ctx, handle.agent)
|
||||
// The self-referential toolset is what distinguishes this preset.
|
||||
expect(tools).toEqual(expect.arrayContaining(['cordis_inspect', 'cordis_mount', 'cordis_unmount']))
|
||||
expect(tools).toEqual(expect.arrayContaining([
|
||||
'cordis_inspect_list', 'cordis_inspect_query', 'cordis_inspect_self',
|
||||
'cordis_define', 'cordis_run', 'cordis_stop', 'cordis_undefine',
|
||||
]))
|
||||
// And it keeps the standard agent's own tools rather than replacing them.
|
||||
expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
|
||||
expect(tools).not.toContain('str_replace_editor')
|
||||
@@ -325,7 +333,7 @@ describe('the shipped Web composition', () => {
|
||||
})
|
||||
try {
|
||||
// Editing the live runtime is opt-in per session, not ambient.
|
||||
expect(toolNames(ctx, handle.agent)).not.toContain('cordis_mount')
|
||||
expect(toolNames(ctx, handle.agent)).not.toContain('cordis_define')
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -362,7 +370,7 @@ describe('the shipped Web composition', () => {
|
||||
})
|
||||
try {
|
||||
// The host (global) view carries the deployment-level provider alone:
|
||||
// local discovery moved behind the presets with `skill-local`.
|
||||
// local discovery moved behind the presets with `skill-filesystem`.
|
||||
expect((await ctx.skills.list({ cwd: proj })).map(skill => skill.name)).toEqual(['dsh-badge'])
|
||||
|
||||
// The standard agent's view merges the global layer with its preset's
|
||||
@@ -448,17 +456,23 @@ describe('product subagent rows in user presets', () => {
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(join(directory, 'agent.cordis.yml'), composition)
|
||||
}
|
||||
productCtx = await bootWeb(settingsFile, [{
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
default: 'standard',
|
||||
roots: [
|
||||
{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
|
||||
{ path: userRoot, trust: 'user' },
|
||||
],
|
||||
includeUserRoot: false,
|
||||
productCtx = await bootWeb(settingsFile, [
|
||||
{ insert: [
|
||||
{ id: 'subagent-codex', name: '@deepseek-ai/dsh-subagent-codex' },
|
||||
{ id: 'subagent-claude-code', name: '@deepseek-ai/dsh-subagent-claude-code' },
|
||||
] },
|
||||
{
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
default: 'standard',
|
||||
roots: [
|
||||
{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
|
||||
{ path: userRoot, trust: 'user' },
|
||||
],
|
||||
includeUserRoot: false,
|
||||
},
|
||||
},
|
||||
}])
|
||||
], EXAMPLES_INSTALL_ANCHOR)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -485,7 +499,7 @@ describe('product subagent rows in user presets', () => {
|
||||
const tools = toolNames(productCtx, handle.agent)
|
||||
expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
|
||||
.toEqual(productTools)
|
||||
expect(tools).toEqual(expect.arrayContaining(['task_kill', 'task_list', 'task_output']))
|
||||
expect(tools).toEqual(expect.arrayContaining(['job_kill', 'job_list', 'job_output']))
|
||||
for (const productTool of productTools) {
|
||||
expect(toolParameterNames(productCtx, handle.agent, productTool)).toEqual([
|
||||
'description', 'prompt', 'run_in_background',
|
||||
|
||||
@@ -51,25 +51,25 @@
|
||||
"path": "../../packages/core/tools"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/util/environment"
|
||||
"path": "../../packages/util/launch-environment"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/util/paths"
|
||||
"path": "../../packages/util/home-paths"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/mcp/mcp-client"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/support/loader-smoke"
|
||||
"path": "../../packages/test-support/loader-smoke"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/session-query/session-query-sqlite"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/bash/bash-env"
|
||||
"path": "../../packages/shell/shell-env"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/bash/tool-bash"
|
||||
"path": "../../packages/shell/tool-bash"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-frontend",
|
||||
"name": "@deepseek-ai/dsh-web-frontend",
|
||||
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
|
||||
"version": "0.0.1-rc.2",
|
||||
"version": "0.0.1-rc.5",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
@@ -24,7 +24,7 @@
|
||||
"dev": "vite",
|
||||
"watch": "vite build --watch"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-client-web": "workspace:^",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -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/web/tests/README.md
|
||||
README.md: 68e5db5af5f816cc982bacb7989d996c859be204
|
||||
README.zh.md: f366c28024dab89d0243a60d93a706f220fa8fb8
|
||||
README.md: 4d7236ef69189845dbc2847289d497fc54c94ae9
|
||||
README.zh.md: 21c2f2ce7ccc8fe5314d766c06235453aa292031
|
||||
|
||||
@@ -29,9 +29,8 @@ artifact it produces itself.
|
||||
|
||||
When a scenario needs a Client-owned constant or pure function, mirror it here
|
||||
instead, next to the commented-out import that names the source module. A drift
|
||||
then surfaces as a missed selector or an unsuppressed notice — a loud failure,
|
||||
never a silent pass. `scaffold.ts` holds the mirrored welcome-notice values and
|
||||
exports them for the scenarios that assert on them.
|
||||
then surfaces as a missed selector or a stale mirrored value — a loud failure,
|
||||
never a silent pass.
|
||||
|
||||
Two kinds of Client import stand. `assembled-boot.ts` drives the shell itself, so
|
||||
it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest
|
||||
|
||||
@@ -23,8 +23,8 @@ Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/re
|
||||
于是 Host 构建阶段变成在等一个由它自己产出的产物。
|
||||
|
||||
当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的
|
||||
import 点明源模块。这样漂移会表现为选择器未命中或提示未被抑制——是响亮的失败,绝不会是静默
|
||||
通过。`scaffold.ts` 持有镜像的 welcome-notice 取值,并导出给断言它们的场景使用。
|
||||
import 点明源模块。这样漂移会表现为选择器未命中或镜像值过期——是响亮的失败,绝不会是静默
|
||||
通过。
|
||||
|
||||
有两类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从
|
||||
`@deepseek-ai/dsh-client-web` import `AppWebEntry`、从
|
||||
|
||||
@@ -34,7 +34,7 @@ const HEADER_EXPECTED = join(SNAPSHOT_DIR, 'header.expected.md')
|
||||
const SHIPPED_PRESETS = fileURLToPath(new URL('../../cli/config/agent-presets', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'agent-preset-selection-web-e2e'
|
||||
/** A project skill only a preset that mounts `skill-local` can discover. */
|
||||
/** A project skill only a preset that mounts `skill-filesystem` can discover. */
|
||||
const SKILL_NAME = 'preset-catalog-demo'
|
||||
|
||||
/**
|
||||
@@ -283,6 +283,7 @@ describe('web e2e: agent-preset selection', () => {
|
||||
expect(snapshot).toContain('Minimal mode')
|
||||
expect(snapshot).toContain('button "1 subagent"')
|
||||
expect(snapshot.indexOf('Minimal mode')).toBeLessThan(snapshot.indexOf('button "1 subagent"'))
|
||||
expect(snapshot.indexOf('button "1 subagent"')).toBeLessThan(snapshot.indexOf('button "Session log"'))
|
||||
// Static chrome, not a control: the header can only report a composition
|
||||
// the host would refuse to change.
|
||||
expect(snapshot).not.toContain('button "Minimal mode"')
|
||||
|
||||
@@ -43,6 +43,7 @@ const PLUGINS: readonly (WebBootEntry & { bundlePath: string })[] = [
|
||||
'@deepseek-ai/dsh-client-ui-sidebar',
|
||||
],
|
||||
},
|
||||
{ id: '@deepseek-ai/dsh-session-log-export', bundlePath: 'packages/session-query/session-log-export/lib/client.js', url: '/plugins/session-log-download.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-commands', '@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-trajectory', bundlePath: 'packages/client/ui-trajectory/lib/client.js', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
]
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Web e2e scenario: the session-header background-task list over the real
|
||||
// Web e2e scenario: the session-header background-job list over the real
|
||||
// host. No model call is involved — a genuine `run_in_background` bash call
|
||||
// registers with `ctx.tasks`, and the assertion chain is the whole delivery
|
||||
// path: registry change feed → api-proxy `session/tasks` frame → the client's
|
||||
// `tasksBySession` mirror → the header action.
|
||||
// registers with `ctx.jobs`, and the assertion chain is the whole delivery
|
||||
// path: registry change feed → api-proxy `session/jobs` frame → the client's
|
||||
// `jobsBySession` mirror → the header action.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
@@ -12,7 +12,7 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { TaskId } from '@deepseek-ai/dsh-tasks'
|
||||
import { JobId } from '@deepseek-ai/dsh-jobs'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/fresh-round-trip/session.jsonl', import.meta.url))
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/background-task-list', import.meta.url))
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/background-job-list', import.meta.url))
|
||||
const RUNNING_EXPECTED = join(SNAPSHOT_DIR, 'running.expected.md')
|
||||
const SETTLED_EXPECTED = join(SNAPSHOT_DIR, 'settled.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'background-task-list-web-e2e'
|
||||
const SEED_ID = 'background-job-list-web-e2e'
|
||||
// Long enough that the running assertions never race the process exiting on
|
||||
// their own; the test kills it explicitly to reach the settled state.
|
||||
const COMMAND = 'sleep 45'
|
||||
@@ -45,13 +45,13 @@ async function liveAgent(scaffold: WebScaffold, sessionId: SessionId): Promise<A
|
||||
}
|
||||
}
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: background task list', () => {
|
||||
describe.skipIf(MODE === 'record')('web e2e: background job list', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let agent: Agent
|
||||
let taskId: TaskId
|
||||
let jobId: JobId
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
@@ -70,7 +70,7 @@ describe.skipIf(MODE === 'record')('web e2e: background task list', () => {
|
||||
await sessionRow.click()
|
||||
|
||||
// Opening the session drives the Host's ordinary Agent resolution; the
|
||||
// task owner must be that exact live instance, never a second one.
|
||||
// job owner must be that exact live instance, never a second one.
|
||||
// `expect.poll` is test-scoped, so this hook polls by hand.
|
||||
agent = await liveAgent(scaffold, SessionId(SEED_ID))
|
||||
}, 120_000)
|
||||
@@ -80,29 +80,29 @@ describe.skipIf(MODE === 'record')('web e2e: background task list', () => {
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('shows a running background task in the session header without a refresh', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-running'))
|
||||
it('shows a running background job in the session header without a refresh', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-background-job-running'))
|
||||
// Point assertion, not a poll: `expect.poll` retries until a predicate
|
||||
// holds, so polling for zero passes at t=0 and proves nothing. The
|
||||
// "renders nothing without a task" branch is owned by the component suite.
|
||||
const trigger = page.getByRole('button', { name: '1 background task running' })
|
||||
const trigger = page.getByRole('button', { name: '1 background job running' })
|
||||
expect(await trigger.count()).toBe(0)
|
||||
|
||||
const started = await scaffold.ctx.tools.execute({
|
||||
signal: new AbortController().signal,
|
||||
callId: CallId('background-task-list-e2e'),
|
||||
callId: CallId('background-job-list-e2e'),
|
||||
name: 'bash',
|
||||
arguments: { command: COMMAND, description: 'Hold a background slot open', run_in_background: true },
|
||||
agent,
|
||||
})
|
||||
const reported = started.content.map(block => block.type === 'text' ? block.text : '').join('')
|
||||
const matched = /\bbash-\d+\b/.exec(reported)
|
||||
if (matched === null) throw new Error(`background bash reported no task id: ${reported}`)
|
||||
taskId = TaskId(matched[0])
|
||||
if (matched === null) throw new Error(`background bash reported no job id: ${reported}`)
|
||||
jobId = JobId(matched[0])
|
||||
|
||||
await trigger.waitFor({ timeout: 15_000 })
|
||||
await trigger.click()
|
||||
const row = page.getByRole('list', { name: 'Background tasks' }).getByRole('listitem').first()
|
||||
const row = page.getByRole('list', { name: 'Background jobs' }).getByRole('listitem').first()
|
||||
await row.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => row.textContent()).toContain(COMMAND)
|
||||
|
||||
@@ -113,12 +113,12 @@ describe.skipIf(MODE === 'record')('web e2e: background task list', () => {
|
||||
}, 60_000)
|
||||
|
||||
it('flips the open list to the cancelled outcome when the registry settles it', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-settled'))
|
||||
expect(scaffold.ctx.tasks.kill(taskId, agent, 'web e2e cancellation')).toBe('requested')
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-background-job-settled'))
|
||||
expect(scaffold.ctx.jobs.kill(jobId, agent, 'web e2e cancellation')).toBe('requested')
|
||||
|
||||
// The trigger drops its live count once the task leaves running/stopping,
|
||||
// which is also the proof that settlement reached the browser unprompted.
|
||||
const idle = page.getByRole('button', { name: '1 background task' })
|
||||
const idle = page.getByRole('button', { name: '1 background job' })
|
||||
await idle.waitFor({ timeout: 20_000 })
|
||||
|
||||
const snapshot = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd)
|
||||
@@ -250,13 +250,16 @@ function scrollGeometry(page: Page): Promise<ScrollGeometry> {
|
||||
}))
|
||||
}
|
||||
|
||||
async function conversationTurns(page: Page): Promise<number> {
|
||||
const stats = page.getByText(/\d+ turns · \d+ steps/, { exact: true }).last()
|
||||
await stats.waitFor({ timeout: 15_000 })
|
||||
const value = await stats.textContent()
|
||||
const match = value?.match(/^(\d+) turns · \d+ steps$/)
|
||||
if (match?.[1] === undefined) throw new Error(`unexpected conversation stats ${JSON.stringify(value)}`)
|
||||
return Number(match[1])
|
||||
/**
|
||||
* Rendered transcript rows in the loaded window. The stats strip cannot serve
|
||||
* as this probe: its turn/step counts ride the whole-log sessionStats
|
||||
* projection and stay fixed across paging by design, while the row count is
|
||||
* exactly what grows when an older page prepends or a live turn streams in.
|
||||
* @param page - the scenario page.
|
||||
* @returns the number of mounted chat flow rows.
|
||||
*/
|
||||
async function loadedFlowRows(page: Page): Promise<number> {
|
||||
return page.locator('[data-chat-flow-key]').count()
|
||||
}
|
||||
|
||||
async function openSeed(page: Page, fixture: ChatScrollFixture, tailMarker?: string): Promise<void> {
|
||||
@@ -265,7 +268,7 @@ async function openSeed(page: Page, fixture: ChatScrollFixture, tailMarker?: str
|
||||
if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
|
||||
const search = page.getByRole('textbox', { name: 'Search sessions...', exact: true })
|
||||
// Cold summaries initially show the temporary workspace basename, so the
|
||||
// persisted first-message marker is the stable user-facing identity. The
|
||||
// persisted first-prompt marker is the stable user-facing identity. The
|
||||
// query itself triggers lazy content-index reconciliation; no transient
|
||||
// empty-state paint is used as a barrier.
|
||||
await search.fill(fixture.markers.user(1))
|
||||
@@ -425,9 +428,9 @@ async function loadEarlierWithAnchor(page: Page): Promise<void> {
|
||||
const older = page.getByRole('button', { name: 'Load earlier', exact: true })
|
||||
await older.waitFor({ timeout: 10_000 })
|
||||
const anchor = await visibleFlowAnchor(page)
|
||||
const before = await conversationTurns(page)
|
||||
const before = await loadedFlowRows(page)
|
||||
await older.click()
|
||||
await expect.poll(() => conversationTurns(page), { timeout: 30_000 }).toBeGreaterThan(before)
|
||||
await expect.poll(() => loadedFlowRows(page), { timeout: 30_000 }).toBeGreaterThan(before)
|
||||
await nextPaint(page)
|
||||
await expectSameFlowTop(page, anchor)
|
||||
}
|
||||
@@ -498,7 +501,7 @@ describe('web e2e: long Chat scroll contract', () => {
|
||||
await world.page.getByRole('button', { name: 'Send message', exact: true }).click()
|
||||
await world.page.getByText(LIVE_TEXT_FIRST, { exact: false }).last().waitFor({ timeout: 15_000 })
|
||||
await wheelToHistoryStart(world.page)
|
||||
const beforeTurns = await conversationTurns(world.page)
|
||||
const beforeRows = await loadedFlowRows(world.page)
|
||||
await world.page.getByRole('button', { name: 'Load earlier', exact: true }).click()
|
||||
await expect.poll(() => held, { timeout: 10_000 }).toBe(true)
|
||||
|
||||
@@ -511,7 +514,7 @@ describe('web e2e: long Chat scroll contract', () => {
|
||||
).toBeGreaterThan(chunksAfterAnchor + 5)
|
||||
|
||||
releaseHistory()
|
||||
await expect.poll(() => conversationTurns(world.page), { timeout: 30_000 }).toBeGreaterThan(beforeTurns)
|
||||
await expect.poll(() => loadedFlowRows(world.page), { timeout: 30_000 }).toBeGreaterThan(beforeRows)
|
||||
await nextPaint(world.page)
|
||||
await expectSameFlowTop(world.page, readerAnchor)
|
||||
} finally {
|
||||
@@ -531,7 +534,11 @@ describe('web e2e: long Chat scroll contract', () => {
|
||||
additionalPages += 1
|
||||
}
|
||||
expect(additionalPages).toBeGreaterThan(0)
|
||||
expect(await conversationTurns(world.page)).toBe(HISTORY_FIXTURE.turns + 1)
|
||||
// The whole log is loaded: turn 1's unique marker renders in the
|
||||
// transcript (scoped: the sidebar search row also carries it) and no
|
||||
// page remains.
|
||||
expect(await world.page.locator('[data-conversation-scroll]')
|
||||
.getByText(HISTORY_FIXTURE.markers.user(1), { exact: false }).count()).toBe(1)
|
||||
expect(await world.page.getByRole('button', { name: 'Load earlier', exact: true }).count()).toBe(0)
|
||||
assertClean(world)
|
||||
})
|
||||
|
||||
@@ -797,12 +797,11 @@ async function stableCount(
|
||||
}
|
||||
|
||||
async function conversationTurns(page: Page): Promise<number> {
|
||||
const stats = page.getByText(/\d+ turns · \d+ steps/, { exact: true }).last()
|
||||
await stats.waitFor({ timeout: 15_000 })
|
||||
const value = await stats.textContent()
|
||||
const match = value?.match(/^(\d+) turns · \d+ steps$/)
|
||||
if (match?.[1] === undefined) throw new Error(`unexpected conversation stats ${JSON.stringify(value)}`)
|
||||
return Number(match[1])
|
||||
// Loaded-window turn count: one mounted turn-tail footer per settled turn in
|
||||
// the window (context keys are `${kind.length}:${kind}${id}`). The stats
|
||||
// strip cannot serve as this probe: its counts ride the whole-log
|
||||
// sessionStats projection and stay fixed across paging by design.
|
||||
return stableCount(page.locator('[data-chat-flow-key^="9:turn-tail"]'), count => count > 0)
|
||||
}
|
||||
|
||||
function retainedDelta(
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// Web e2e scenario for the opt-in Cordis tools. Record mode drives a real
|
||||
// model through inspect, mount, and unmount; replay pins the same shipped Web
|
||||
// composition, durable calls, generic rows, highlighted Plugin source, and
|
||||
// conversation accessibility tree.
|
||||
// model through inspect, define, run, and stop; replay pins the same shipped Web
|
||||
// composition, durable calls, Cordis-owned rows, the define card's own source view,
|
||||
// and conversation accessibility tree.
|
||||
//
|
||||
// The approval is never in the fixture. The fixture pins what the MODEL said;
|
||||
// tools execute for real, and this test answers the approval before starting the
|
||||
// stop turn. The package therefore carries a browser half whose only
|
||||
// job is to be visible (`[data-snapshot-probe]`): its absence before the answer
|
||||
// and presence after it is the v3 user gate, proven rather than described.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
@@ -17,12 +23,24 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
|
||||
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))
|
||||
const MODE = webSnapshotMode()
|
||||
const CORDIS_TOOLS = ['cordis_inspect', 'cordis_mount', 'cordis_unmount'] as const
|
||||
const MOUNT_CODE = 'return { name: "snapshot-noop", apply(ctx) {} }'
|
||||
const PROMPT = 'Use only Cordis tools. First call cordis_inspect with what "temporary". '
|
||||
+ `Then call cordis_mount with this exact code: ${JSON.stringify(MOUNT_CODE)}. `
|
||||
+ 'Read its returned id and call cordis_unmount with that exact id. '
|
||||
+ 'After all three calls succeed, reply exactly CORDIS_UI_DONE and stop.'
|
||||
const CORDIS_TOOLS = ['cordis_inspect_self', 'cordis_define', 'cordis_run', 'cordis_stop'] as const
|
||||
const PACKAGE_CODE = 'return { name: "snapshot-noop", apply(ctx) {} }'
|
||||
// The browser half is the PROBE this scenario turns on: it renders a marker into
|
||||
// the frame-wide overlay, so "did the plugin actually run in this page" becomes a
|
||||
// DOM fact. A host-only package would sidestep the approval round trip entirely
|
||||
// (the host runs those immediately), which would drop the v3 user gate out of
|
||||
// coverage — the one thing this scenario exists to prove.
|
||||
const CLIENT_CODE = 'return { inject: ["slots"], apply(ctx) { ctx.slots.register('
|
||||
+ '{ name: "shell.overlay", id: "snapshot-probe" }, '
|
||||
+ '() => React.createElement("div", { "data-snapshot-probe": "loaded" })) } }'
|
||||
const PROMPT = 'Use only Cordis tools. First call cordis_inspect_self with no arguments. '
|
||||
+ 'Then call cordis_define with plugin kind "new", idPrefix "snap", name "snapshot noop", '
|
||||
+ 'purpose "does nothing, for the snapshot", '
|
||||
+ `code.host exactly ${JSON.stringify(PACKAGE_CODE)} and code.client exactly ${JSON.stringify(CLIENT_CODE)}. `
|
||||
+ 'Read its returned pluginId and packageId, then call cordis_run with those exact IDs and mode "run". '
|
||||
+ 'After the run request returns, reply exactly CORDIS_UI_READY and stop.'
|
||||
const STOP_PROMPT = 'Use only Cordis tools. Call cordis_stop with pluginId "snap-1". '
|
||||
+ 'After it succeeds, reply exactly CORDIS_UI_DONE and stop.'
|
||||
|
||||
function assertCompleteCordisLifecycle(events: readonly SessionEvent[]): void {
|
||||
const turnEnd = events.findLast(
|
||||
@@ -46,7 +64,7 @@ function assertCompleteCordisLifecycle(events: readonly SessionEvent[]): void {
|
||||
expect(results.every(event => !event.data.message.content[0].isError)).toBe(true)
|
||||
}
|
||||
|
||||
describe('web e2e: Cordis tools use the generic row variants', () => {
|
||||
describe('web e2e: Cordis tools use their owned cards', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
@@ -75,14 +93,30 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
|
||||
it('drives the recorded Cordis lifecycle to a settled turn (all modes)', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-cordis-drive'))
|
||||
if (MODE !== 'record') {
|
||||
expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
|
||||
expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT, STOP_PROMPT])
|
||||
}
|
||||
const input = page.locator('textarea').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
const runTurnSettled = scaffold.whenTurnSettled()
|
||||
await input.fill(PROMPT)
|
||||
await input.press('Enter')
|
||||
const sessionId = await settled
|
||||
|
||||
// The approval is the TEST's action in every mode: the fixture pins what the
|
||||
// model said, and the gate is a real round trip through the real panel.
|
||||
const approve = page.locator('[data-cordis-approve]').first()
|
||||
await approve.waitFor({ timeout: 90_000 })
|
||||
// The one assertion this scenario cannot give up: the model asking to run is
|
||||
// NOT the plugin running. Until a person answers, the browser half has not
|
||||
// been fetched, evaluated, or mounted anywhere on this page.
|
||||
expect(await page.locator('[data-snapshot-probe]').count()).toBe(0)
|
||||
await approve.click()
|
||||
await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 30_000 }).toBe(1)
|
||||
|
||||
const sessionId = await runTurnSettled
|
||||
const stopTurnSettled = scaffold.whenTurnSettled()
|
||||
await input.fill(STOP_PROMPT)
|
||||
await input.press('Enter')
|
||||
await stopTurnSettled
|
||||
if (MODE === 'record') {
|
||||
assertCompleteCordisLifecycle(sessionEvents)
|
||||
await expect.poll(() => page.getByText('CORDIS_UI_DONE', { exact: true }).count(), { timeout: 15_000 })
|
||||
@@ -95,25 +129,36 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
|
||||
assertCompleteCordisLifecycle(sessionEvents)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('renders Cordis lifecycle titles over the generic row mechanics', async () => {
|
||||
it.skipIf(MODE === 'record')('renders localized Cordis lifecycle cards', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-cordis-rows'))
|
||||
await expect.poll(() => page.getByText('CORDIS_UI_DONE', { exact: true }).count(), { timeout: 15_000 })
|
||||
.toBeGreaterThanOrEqual(1)
|
||||
|
||||
const inspectRow = page.locator('[data-tool="cordis_inspect"]').filter({ hasText: 'Inspect' }).first()
|
||||
const inspectRow = page.locator('[data-tool="cordis_inspect_self"]').filter({ hasText: 'Inspect' }).first()
|
||||
await inspectRow.waitFor({ timeout: 10_000 })
|
||||
|
||||
const mountRow = page.locator('[data-tool="cordis_mount"]').filter({ hasText: 'Mount temporary Plugin' }).first()
|
||||
await mountRow.waitFor({ timeout: 10_000 })
|
||||
// cordis_define does NOT go through the generic row: ui-cordis registers a
|
||||
// keyed toolview for it, and a keyed hit replaces the generic card. So the
|
||||
// title here is the CARD's ("Cordis Plugin"), and the expanded body is the
|
||||
// card's own two code sections rather than a generic args dump.
|
||||
const defineRow = page.locator('[data-tool="cordis_define"]').filter({ hasText: 'Cordis Plugin' }).first()
|
||||
await defineRow.waitFor({ timeout: 10_000 })
|
||||
// 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)
|
||||
await defineRow.locator('[aria-expanded]').first().click()
|
||||
await expect.poll(() => defineRow.textContent(), { timeout: 10_000 }).toContain('data-snapshot-probe')
|
||||
await defineRow.getByRole('tab', { name: 'Host' }).click()
|
||||
await expect.poll(() => defineRow.textContent()).toContain(PACKAGE_CODE)
|
||||
|
||||
const unmountRow = page.locator('[data-tool="cordis_unmount"]').filter({ hasText: 'Unmount temporary Plugin' }).first()
|
||||
await unmountRow.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => unmountRow.textContent()).toContain('dyn-')
|
||||
await expect(unmountRow.getAttribute('data-state')).resolves.toBe('ok')
|
||||
const runRow = page.locator('[data-tool="cordis_run"]').filter({ hasText: 'Run Cordis Plugin' }).first()
|
||||
await runRow.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => runRow.textContent()).toContain('snap-')
|
||||
|
||||
const stopRow = page.locator('[data-tool="cordis_stop"]').filter({ hasText: 'Stop Cordis Plugin' }).first()
|
||||
await stopRow.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => stopRow.textContent()).toContain('snap-')
|
||||
await expect(stopRow.getAttribute('data-state')).resolves.toBe('ok')
|
||||
// Stopping withdraws the browser half from every page, probe included.
|
||||
await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 15_000 }).toBe(0)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('matches the conversation aria golden', async () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('web e2e: goal bar clear convergence', () => {
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, welcomeNoticePending: true })
|
||||
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# The client-side FixtureApiClient intentionally rejects settings writes, so
|
||||
# this goal-only scenario omits the durable welcome step that would otherwise
|
||||
# cover the page. Onboarding owns separate assembled-browser coverage.
|
||||
# The client-side FixtureApiClient intentionally rejects settings traffic, so
|
||||
# this goal-only scenario omits the settings shell and the onboarding steps it
|
||||
# would mount. Onboarding owns separate assembled-browser coverage.
|
||||
- id: ui-settings-general
|
||||
disabled: true
|
||||
|
||||
@@ -39,11 +39,11 @@ const PACKAGE_FILES: Readonly<Record<string, string>> = {
|
||||
'packages/llm/token-meter/package.json': '{"name":"@deepseek-ai/dsh-token-meter"}\n',
|
||||
'packages/llm/token-meter/src/index.ts': 'export {}\n',
|
||||
'packages/llm/token-meter/tests/token-meter.spec.ts': 'export {}\n',
|
||||
'packages/skill/skill-local/README.md': '# Local skill provider\n',
|
||||
'packages/skill/skill-local/package.json': '{"name":"@deepseek-ai/dsh-skill-local"}\n',
|
||||
'packages/skill/skill-local/src/index.ts': 'export {}\n',
|
||||
'packages/skill/skill-local/src/invariant.ts': 'export {}\n',
|
||||
'packages/skill/skill-local/tests/skill-local.spec.ts': 'export {}\n',
|
||||
'packages/skill/skill-filesystem/README.md': '# Local skill provider\n',
|
||||
'packages/skill/skill-filesystem/package.json': '{"name":"@deepseek-ai/dsh-skill-filesystem"}\n',
|
||||
'packages/skill/skill-filesystem/src/index.ts': 'export {}\n',
|
||||
'packages/skill/skill-filesystem/src/invariant.ts': 'export {}\n',
|
||||
'packages/skill/skill-filesystem/tests/skill-filesystem.spec.ts': 'export {}\n',
|
||||
}
|
||||
|
||||
/** Materialize a stable package inventory inside the isolated session workspace. */
|
||||
|
||||
@@ -8,7 +8,7 @@ import { chromium } from 'playwright'
|
||||
import { expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import type { Fiber } from '@deepseek-ai/cordis'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import { REPO_ROOT } from './support.ts'
|
||||
|
||||
@@ -88,7 +88,7 @@ it('hot-reloads a real client-plugin source edit without refreshing the page', a
|
||||
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined
|
||||
const failures: unknown[] = []
|
||||
try {
|
||||
subprocessFiber = await subprocessCtx.plugin(LocalSubprocessService)
|
||||
subprocessFiber = await subprocessCtx.plugin(LocalSubprocessRuntime)
|
||||
watcher = subprocessCtx.subprocess.spawn(spawnSpec(['pnpm', 'run', 'dev:web'], REPO_ROOT))
|
||||
await waitForOutput(watcher, /dev-web: watching/, 'pnpm run dev:web')
|
||||
host = subprocessCtx.subprocess.spawn(spawnSpec(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
// Multimodal image surfaces over the BUILT client graph (the code-mode-fixture
|
||||
// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
|
||||
// Opens the fixture history session whose turn 72 carries an image in BOTH a
|
||||
// Opens the fixture history session whose turn 73 carries an image in BOTH a
|
||||
// user message and an assistant message, and pins the product surfaces: the
|
||||
// history ImageGallery loading real fixture bytes through the authorized
|
||||
// sessions.attachment route, the single-click ImageLightbox, and the composer
|
||||
@@ -143,8 +143,55 @@ it('accepts pasted images into the composer rail in order and removes them', asy
|
||||
},
|
||||
})
|
||||
const toast = await screen.findByRole('alert')
|
||||
expect(toast.textContent).toContain('Unsupported image format: text/plain')
|
||||
expect(toast.textContent).toContain('Only PNG, JPG, WebP, and GIF images are supported')
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
}, { timeout: 6_000 })
|
||||
})
|
||||
|
||||
it('accepts a whole-page drop under the limits-labeled overlay and refuses an over-limit batch at intake', async () => {
|
||||
mountAssembledApp()
|
||||
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="New session in fixture"]')
|
||||
if (start === null) throw new Error('fixture Workspace new-session action missing')
|
||||
fireEvent.click(start)
|
||||
const textarea = await screen.findByPlaceholderText('Describe what you want to build', {}, { timeout: 10_000 })
|
||||
|
||||
// A file drag anywhere over the page raises the full-viewport overlay whose
|
||||
// desc line carries the projected limits — copy that can only render after
|
||||
// the imageLimits projection crossed the real fixture transport.
|
||||
const image = new File([new Uint8Array([137, 80, 78, 71])], 'dropped.png', { type: 'image/png' })
|
||||
const dataTransfer = { types: ['Files'], files: [image], dropEffect: 'none' }
|
||||
fireEvent.dragEnter(document.body, { dataTransfer })
|
||||
const overlay = await screen.findByRole('status')
|
||||
expect(overlay.textContent).toContain('Drag images here to add them')
|
||||
await waitFor(() => {
|
||||
expect(overlay.textContent).toContain('Up to 20 images, 5MB each')
|
||||
})
|
||||
|
||||
// Dropping on the transcript area (not the composer card) lands in the rail.
|
||||
fireEvent.drop(document.body, { dataTransfer })
|
||||
await waitFor(() => {
|
||||
const rail = document.querySelector('[role="group"][aria-label="Pending images"]')
|
||||
if (rail === null) throw new Error('attachment rail missing after page drop')
|
||||
expect([...rail.querySelectorAll('img')].map(img => img.getAttribute('alt'))).toEqual(['dropped.png'])
|
||||
}, { timeout: 5_000 })
|
||||
expect(screen.queryByRole('status')).toBeNull()
|
||||
|
||||
// An intake that would exceed the projected per-message count is refused as
|
||||
// a whole batch at add time: the banner names the limit and the rail keeps
|
||||
// only the previously accepted thumbnail — no submit-time rollback.
|
||||
const batch = Array.from({ length: 20 }, (_, i) =>
|
||||
new File([new Uint8Array([137, 80, 78, 71])], `bulk-${String(i)}.png`, { type: 'image/png' }))
|
||||
fireEvent.paste(textarea, {
|
||||
clipboardData: {
|
||||
items: batch.map(file => ({ kind: 'file', type: 'image/png', getAsFile: () => file })),
|
||||
getData: () => '',
|
||||
},
|
||||
})
|
||||
const banner = await screen.findByRole('alert')
|
||||
expect(banner.textContent).toContain('A message can include up to 20 images')
|
||||
const rail = document.querySelector('[role="group"][aria-label="Pending images"]')
|
||||
expect([...(rail?.querySelectorAll('img') ?? [])]).toHaveLength(1)
|
||||
})
|
||||
|
||||
@@ -238,7 +238,7 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
|
||||
|
||||
it.skipIf(MODE === 'record')('cascades the dark theme from the body attribute to painted surfaces', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-lifecycle-dark'))
|
||||
// This scenario pins the ThemeService's DOM contract directly (the
|
||||
// This scenario pins the ThemeRuntime's DOM contract directly (the
|
||||
// body[data-ds-dark-theme] attribute -> stylesheet cascade); the REAL
|
||||
// user gesture above it (Settings -> Appearance cubes) is owned by
|
||||
// settings-chrome.e2e.ts. Driving the attribute here keeps the cascade
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('web e2e: settled Markdown math rendering', () => {
|
||||
await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2)
|
||||
expect(await page.locator('.katex-error').count()).toBe(0)
|
||||
await expect.poll(
|
||||
() => page.getByText('Input 0 tok · Output 0 tok', { exact: false }).count(),
|
||||
() => page.getByText('1 turns · 1 steps', { exact: false }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(1)
|
||||
|
||||
|
||||
56
apps/web/tests/max-tokens-notice.snapshot.ts
Normal file
56
apps/web/tests/max-tokens-notice.snapshot.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// @vitest-environment jsdom
|
||||
// Assembled max-tokens snapshot: boots the real built `packages/client/*/lib/
|
||||
// client.js` bundles through AppWebEntry's ModuleLoader path against the
|
||||
// keyless FixtureApiClient transport, opens the fixture session, and pins the
|
||||
// surface its max-tokens turn (72) reaches — the turn-end notice row that a
|
||||
// provider output-cap truncation must render instead of ending silently.
|
||||
//
|
||||
// The dot state is pinned beside the copy on purpose: `dot=warning` is what
|
||||
// distinguishes this notice from the error row, so a regression that routes
|
||||
// max-tokens through the turn-error presentation changes this file even when
|
||||
// its own copy still renders.
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { hasClass, installAssembledBootEnv, mountAssembledApp, REFRESHING_GOLDEN } from './assembled-boot.ts'
|
||||
|
||||
const EXPECTED = join(process.cwd(), 'apps/web/tests/snapshots/max-tokens-notice/history-turn.expected.txt')
|
||||
|
||||
installAssembledBootEnv()
|
||||
|
||||
/** Normalize the notice row to stable fields: its dot state, title, and hint. */
|
||||
function noticeShape(row: Element): string {
|
||||
const first = (name: string): string =>
|
||||
[...row.querySelectorAll('*')].filter(el => hasClass(el, name))[0]?.textContent?.trim() ?? '<absent>'
|
||||
return [
|
||||
`dot=${row.querySelector('[data-state]')?.getAttribute('data-state') ?? '<absent>'}`,
|
||||
`title=${first('maxTokensTitle')}`,
|
||||
`hint=${first('turnErrorMessage')}`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
describe('assembled max-tokens turn-end notice', () => {
|
||||
it('renders the localized truncation notice after the cut-off answer instead of ending silently', async () => {
|
||||
mountAssembledApp()
|
||||
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
fireEvent.click(await within(tree).findByText('Fixture 历史会话'))
|
||||
// The truncated answer itself stays in the flow: the notice supplements the
|
||||
// partial output, it never replaces it.
|
||||
await screen.findByText(/条目 3:这一条写到一半被/, undefined, { timeout: 10_000 })
|
||||
const row = await waitFor(() => {
|
||||
const found = [...document.querySelectorAll('[role="status"]')]
|
||||
.find(candidate => [...candidate.querySelectorAll('*')].some(el => hasClass(el, 'maxTokensTitle')))
|
||||
expect(found).not.toBeUndefined()
|
||||
return found!
|
||||
}, { timeout: 10_000 })
|
||||
|
||||
const shape = noticeShape(row)
|
||||
if (REFRESHING_GOLDEN) {
|
||||
mkdirSync(dirname(EXPECTED), { recursive: true })
|
||||
writeFileSync(EXPECTED, shape)
|
||||
}
|
||||
await expect(shape).toMatchFileSnapshot(EXPECTED)
|
||||
})
|
||||
})
|
||||
121
apps/web/tests/message-feedback.e2e.ts
Normal file
121
apps/web/tests/message-feedback.e2e.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
// Keyless browser regression for durable per-message feedback. Cold-seeds a
|
||||
// settled two-turn transcript (zero model calls), rates one assistant message,
|
||||
// attaches a note, proves both survive a full page reload from the Host's
|
||||
// message-feedback sidecar, then retracts the rating.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
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 {
|
||||
acknowledgeReloadConnectionLoss, launchWebScaffold,
|
||||
seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
// Borrowed read-only: this scenario needs any settled assistant message to
|
||||
// address, not a new recording (message-actions / sidebar-scrollbar pattern).
|
||||
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'message-feedback-web-e2e'
|
||||
const NOTE = 'Read both files before answering.'
|
||||
|
||||
describe('web e2e: durable per-message feedback', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
await seedSession(scaffold, await readFile(SEED, 'utf8'), SEED_ID)
|
||||
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 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
/**
|
||||
* Open the seeded transcript. The first treeitem is the collapsible group
|
||||
* row; the session itself is the row beneath it. The group is already
|
||||
* expanded on a fresh load, so clicking it unconditionally would collapse it
|
||||
* and hide the session row.
|
||||
*/
|
||||
async function openSeededSession(): Promise<void> {
|
||||
const groupRow = page.locator('[role="treeitem"]').first()
|
||||
await groupRow.waitFor({ timeout: 15_000 })
|
||||
if (await groupRow.getAttribute('aria-expanded') !== 'true') await groupRow.click()
|
||||
const sessionRow = page.locator('[role="treeitem"]').nth(1)
|
||||
await sessionRow.waitFor({ timeout: 15_000 })
|
||||
await sessionRow.click()
|
||||
}
|
||||
|
||||
it.skipIf(MODE === 'record')('persists a rating and its note across a reload, then retracts', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-feedback'))
|
||||
await openSeededSession()
|
||||
|
||||
// The controls live in the assistant message's IconActions row, which the
|
||||
// transcript reveals on hover/focus like copy and branch. Wait for the
|
||||
// settled closing text first: the strip mounts with that turn's tail.
|
||||
await page.getByText('DONE', { exact: true }).waitFor({ timeout: 30_000 })
|
||||
const like = page.getByRole('button', { name: 'Good response' }).first()
|
||||
await like.waitFor({ timeout: 30_000 })
|
||||
await like.scrollIntoViewIfNeeded()
|
||||
await like.hover()
|
||||
await like.click()
|
||||
// A recorded rating relabels the button to what the next click would do,
|
||||
// so the pressed control is addressed by the retract label from here on.
|
||||
const rated = page.getByRole('button', { name: 'Remove rating' }).first()
|
||||
await expect.poll(() => rated.getAttribute('aria-pressed'), { timeout: 10_000 }).toBe('true')
|
||||
|
||||
// A rated message offers the note editor; an unrated one does not.
|
||||
await page.getByRole('button', { name: 'Add a note' }).first().click()
|
||||
const editor = page.getByRole('textbox', { name: 'Feedback note' })
|
||||
await editor.fill(NOTE)
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click()
|
||||
await expect.poll(() => editor.count(), { timeout: 10_000 }).toBe(0)
|
||||
await page.getByText(NOTE, { exact: true }).waitFor({ timeout: 10_000 })
|
||||
|
||||
// The durable assertion: a cold browser re-reads the sidecar over the wire.
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await openSeededSession()
|
||||
await page.getByText('DONE', { exact: true }).waitFor({ timeout: 30_000 })
|
||||
|
||||
// The controller defers its list read to the first hover or focus, so a
|
||||
// cold reload shows the unrated label until the strip is touched. Hovering
|
||||
// the unrated control is what triggers the authoritative re-read.
|
||||
const cold = page.getByRole('button', { name: 'Good response' }).first()
|
||||
await cold.waitFor({ timeout: 30_000 })
|
||||
await cold.scrollIntoViewIfNeeded()
|
||||
await cold.hover()
|
||||
|
||||
const restored = page.getByRole('button', { name: 'Remove rating' }).first()
|
||||
await restored.waitFor({ timeout: 30_000 })
|
||||
await restored.scrollIntoViewIfNeeded()
|
||||
await restored.hover()
|
||||
await expect.poll(() => restored.getAttribute('aria-pressed'), { timeout: 15_000 }).toBe('true')
|
||||
await page.getByText(NOTE, { exact: true }).waitFor({ timeout: 10_000 })
|
||||
|
||||
// Re-clicking the active rating retracts it, and the note goes with it.
|
||||
await restored.click()
|
||||
await expect.poll(
|
||||
() => page.getByRole('button', { name: 'Good response' }).first().getAttribute('aria-pressed'),
|
||||
{ timeout: 10_000 },
|
||||
).toBe('false')
|
||||
await expect.poll(() => page.getByText(NOTE, { exact: true }).count(), { timeout: 10_000 }).toBe(0)
|
||||
}, 90_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('kept the console clean', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -55,10 +55,13 @@ describe('minimal agent preset', () => {
|
||||
|
||||
const requestHeader = agentHandle.agent.session.requestHeader()
|
||||
if (requestHeader === undefined) throw new Error('the minimal agent issued no model request')
|
||||
expect(agentHandle.agent.session.events.some(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === '@deepseek-ai/dsh-system-prompt')).toBe(false)
|
||||
const presetFileSystem = scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'fs')
|
||||
expect(presetFileSystem).toBeDefined()
|
||||
expect(presetFileSystem?.sandboxMode).toBeUndefined()
|
||||
expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'compact')).toBeUndefined()
|
||||
expect(scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'compaction')).toBeUndefined()
|
||||
|
||||
const stateDir = join(scaffold.workspaceCwd, 'persistent-state')
|
||||
await mkdir(stateDir)
|
||||
|
||||
@@ -52,6 +52,11 @@ async function assertBaselineSucceeded(response: Response, method: string): Prom
|
||||
}
|
||||
|
||||
async function ensureSeedOpen(page: Page): Promise<void> {
|
||||
const welcome = page.locator('[class*="onboardingOverlay"]')
|
||||
if (await welcome.count() > 0) {
|
||||
await welcome.getByRole('button').click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
}
|
||||
const chat = page.getByRole('tab', { name: 'Chat', exact: true })
|
||||
// Search is a collapsed header action; expand it so the input is actionable.
|
||||
const searchButton = page.getByRole('button', { name: 'Search sessions' })
|
||||
@@ -283,14 +288,30 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
await details.getByRole('button', { name: 'Close details' }).click()
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('downloads the session-log ZIP from the trajectory toolbar', async () => {
|
||||
it.skipIf(MODE === 'record')('downloads through the Session Header and /export with one dialog', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-export'))
|
||||
await ensureSeedOpen(page)
|
||||
await page.getByRole('tab', { name: 'Trajectory' }).click()
|
||||
const exportButton = page.getByRole('button', { name: 'Session log' })
|
||||
expect(await exportButton.isDisabled()).toBe(false)
|
||||
const header = exportButton.locator('xpath=ancestor::header[1]')
|
||||
const [buttonBox, headerBox] = await Promise.all([
|
||||
exportButton.boundingBox(), header.boundingBox(),
|
||||
])
|
||||
if (buttonBox === null || headerBox === null) {
|
||||
throw new Error('Session Header export geometry is unavailable')
|
||||
}
|
||||
expect(headerBox.x + headerBox.width - (buttonBox.x + buttonBox.width)).toBeLessThanOrEqual(32)
|
||||
const responsePromise = page.waitForResponse(response =>
|
||||
response.request().method() === 'HEAD'
|
||||
&& new URL(response.url()).pathname === '/api/session.export', { timeout: 30_000 })
|
||||
const downloadPromise = page.waitForEvent('download', { timeout: 30_000 })
|
||||
await page.getByRole('button', { name: 'Export session log' }).click()
|
||||
await exportButton.click()
|
||||
const response = await responsePromise
|
||||
expect(response.status()).toBe(200)
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toMatch(/^dsh-session-.+\.zip$/)
|
||||
const dialog = page.getByRole('dialog', { name: 'Session download started' })
|
||||
await dialog.waitFor({ timeout: 30_000 })
|
||||
// The real host streamed the ZIP; its root entry is the persisted log
|
||||
// text verbatim (the assembled seam: real route, real persistence read).
|
||||
const files = unzipSync(await readFile(await download.path()))
|
||||
@@ -298,7 +319,63 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
const content = strFromU8(files['session.jsonl'] as Uint8Array)
|
||||
expect(content.split('\n')[0]).toContain(SEED_ID)
|
||||
expect(content).toContain('FIRST_DONE')
|
||||
}, 60_000)
|
||||
await dialog.getByText('Close', { exact: true }).click()
|
||||
|
||||
const observer = await newEnglishPage(browser)
|
||||
const observerTripwire = watchConsole(observer)
|
||||
const observerSlotErrors: string[] = []
|
||||
let observerDownloads = 0
|
||||
observer.on('download', () => { observerDownloads += 1 })
|
||||
observer.on('console', (message) => {
|
||||
if (message.type() === 'error' && /slot entry crashed/i.test(message.text())) {
|
||||
observerSlotErrors.push(message.text())
|
||||
}
|
||||
})
|
||||
const observerSessionBaseline = baselineResponse(observer, 'session.list')
|
||||
const observerWorkspaceBaseline = baselineResponse(observer, 'workspace.list')
|
||||
const [, observerSessionResponse, observerWorkspaceResponse] = await Promise.all([
|
||||
observer.goto(scaffold.baseUrl, { waitUntil: 'load' }),
|
||||
observerSessionBaseline,
|
||||
observerWorkspaceBaseline,
|
||||
])
|
||||
await Promise.all([
|
||||
assertBaselineSucceeded(observerSessionResponse, 'observer session.list'),
|
||||
assertBaselineSucceeded(observerWorkspaceResponse, 'observer workspace.list'),
|
||||
])
|
||||
await observer.getByText('Ungrouped', { exact: true }).waitFor({ timeout: 30_000 })
|
||||
await ensureSeedOpen(observer)
|
||||
|
||||
try {
|
||||
const input = page.locator('textarea').first()
|
||||
const slashDownloadPromise = page.waitForEvent('download', { timeout: 30_000 })
|
||||
await input.fill('/export')
|
||||
await page.getByRole('option', { name: /export/u }).waitFor({ timeout: 10_000 })
|
||||
await input.press('Enter')
|
||||
const slashDownload = await slashDownloadPromise
|
||||
expect(slashDownload.suggestedFilename()).toBe(download.suggestedFilename())
|
||||
const slashFiles = unzipSync(await readFile(await slashDownload.path()))
|
||||
const slashContent = strFromU8(slashFiles['session.jsonl'] as Uint8Array)
|
||||
const slashEvents = parseSessionLog(slashContent)
|
||||
const exportRun = slashEvents.findLast(event => event.type === 'command/run' && event.data.name === 'export')
|
||||
if (exportRun?.type !== 'command/run') throw new Error('slash ZIP has no export command/run')
|
||||
const exportDone = slashEvents.find(event =>
|
||||
event.type === 'command/done' && event.data.commandId === exportRun.data.commandId)
|
||||
expect(exportDone?.type).toBe('command/done')
|
||||
await page.getByRole('dialog', { name: 'Session download started' }).waitFor({ timeout: 30_000 })
|
||||
await page.getByRole('dialog', { name: 'Session download started' })
|
||||
.getByText('Close', { exact: true }).click()
|
||||
await observer.getByText('Session log download requested.', { exact: true }).waitFor({ timeout: 30_000 })
|
||||
expect(observerDownloads).toBe(0)
|
||||
expect(await observer.getByRole('dialog', { name: 'Session download started' }).count()).toBe(0)
|
||||
expect({
|
||||
pageErrors: observerTripwire.pageErrors,
|
||||
slotErrors: observerSlotErrors,
|
||||
warnings: observerTripwire.warnings,
|
||||
}).toEqual({ pageErrors: [], slotErrors: [], warnings: [] })
|
||||
} finally {
|
||||
await observer.close()
|
||||
}
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('focuses the ledger by dragging an overview interval', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-timeline'))
|
||||
|
||||
@@ -10,15 +10,11 @@ import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_ACK_FIELD,
|
||||
WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/onboarding-deepseek-config', import.meta.url))
|
||||
const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md')
|
||||
const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md')
|
||||
const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -31,7 +27,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
const browserConsole: string[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, welcomeNoticePending: true })
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
|
||||
browser = await chromium.launch()
|
||||
// The scenario asserts the shipped Chinese copy, so the browser asks for it.
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE })
|
||||
@@ -48,13 +44,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
|
||||
it('stores a key write-only and observes configured state without restarting', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-config'))
|
||||
const welcome = page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title })
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
const credentialStep = page.getByRole('region', { name: '添加一个 API Key 开始使用' })
|
||||
await credentialStep.waitFor({ timeout: 15_000 })
|
||||
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true)
|
||||
const welcomeAria = await captureStableAria(page, '[role="region"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(WELCOME_EXPECTED, welcomeAria, MODE)
|
||||
expect(await welcome.getByRole('button').allTextContents()).toEqual([WELCOME_NOTICE_COPY.zh.continueLabel])
|
||||
expect(await welcome.locator('button').count()).toBe(1)
|
||||
|
||||
const mask = page.locator('[class*="onboardingMask"]')
|
||||
expect(await mask.count()).toBe(1)
|
||||
@@ -83,17 +75,6 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
rect: { left: 0, top: 80, right: 1440, bottom: 960 },
|
||||
})
|
||||
|
||||
// Closing the process/page before acknowledgement writes nothing, so the
|
||||
// same durable profile presents the notice again after reload.
|
||||
const firstReloadWarnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, firstReloadWarnings)
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
|
||||
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
const credentialStep = page.getByRole('region', { name: '添加一个 API Key 开始使用' })
|
||||
await credentialStep.waitFor({ timeout: 15_000 })
|
||||
expect(await credentialStep.getByRole('textbox').count()).toBe(0)
|
||||
const initial = await captureStableAria(page, '[role="region"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE)
|
||||
@@ -129,27 +110,10 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
{ timeout: 10_000 },
|
||||
).toBe('已配置——输入新值可替换')
|
||||
|
||||
const acknowledgedSettings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
|
||||
expect(acknowledgedSettings).toContain(`${WELCOME_NOTICE_ACK_FIELD}: ${WELCOME_NOTICE_VERSION}`)
|
||||
|
||||
const secondReloadWarnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, secondReloadWarnings)
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 15_000 })
|
||||
expect(await page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title }).count()).toBe(0)
|
||||
expect(await page.getByRole('region', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
|
||||
|
||||
// A different stored copy version represents an intentional version bump:
|
||||
// the welcome step returns even though the credential is already ready.
|
||||
await scaffold.ctx.settings.mutate(settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), [{
|
||||
op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: 'previous-copy-version',
|
||||
}])
|
||||
const thirdReloadWarnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, thirdReloadWarnings)
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
expect(await page.getByRole('region', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
|
||||
|
||||
expect((await page.content()).includes(secret)).toBe(false)
|
||||
@@ -161,8 +125,8 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
|
||||
it('never paints the takeover chrome on a configured reload, even with the settings join held open', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-configured-reload'))
|
||||
// Regression pin for the reload white flash: both steps are satisfied
|
||||
// (welcome acknowledged, credential configured), yet each must LOAD its
|
||||
// Regression pin for the reload white flash: the credential step is
|
||||
// satisfied (credential configured), yet it must LOAD its
|
||||
// private join before it can decide not to show. The chrome lives inside
|
||||
// the step (OnboardingSurface), so the deciding window paints and blocks
|
||||
// nothing. Holding settings.describe widens that window from loopback
|
||||
@@ -214,7 +178,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
it('configures arbitrary DeepSeek models and prompts after the selected model is removed', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-models'))
|
||||
// Opened here rather than inherited: the credential test reloads the page
|
||||
// to exercise the welcome step, so nothing carries an open dialog across.
|
||||
// after configuring the key, so nothing carries an open dialog across.
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
@@ -264,7 +228,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
it('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(
|
||||
SNAPSHOT_DIR,
|
||||
['missing.expected.md', 'models.expected.md', 'welcome.expected.md'],
|
||||
['missing.expected.md', 'models.expected.md'],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
- insert:
|
||||
- id: directory-picker-browse
|
||||
name: '@deepseek-ai/dsh-host-directory-picker-browse'
|
||||
- id: ui-directory-picker
|
||||
name: '@deepseek-ai/dsh-client-ui-directory-picker'
|
||||
- id: ui-directory-picker-browse
|
||||
name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Web e2e scenario: the Plugins settings section — the cards a deployment's
|
||||
// exposed host-plane namespaces produce, one field edited through the real
|
||||
// Web e2e scenario: the configurable tab in Plugins settings — the cards a
|
||||
// deployment's exposed host-plane namespaces produce, one field edited through the real
|
||||
// wire down to `$DSH_HOME/settings.yaml`, and the override badge and reset
|
||||
// that layering produces. Zero model calls: everything is client state plus
|
||||
// the settings document on a blank frame, so there is no fixture and a stray
|
||||
@@ -56,9 +56,12 @@ describe('web e2e: plugin configuration section', () => {
|
||||
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: '插件配置', exact: true }).click()
|
||||
await dialog.getByRole('button', { name: '插件', exact: true }).click()
|
||||
await expect
|
||||
.poll(() => dialog.getByRole('button', { name: '插件配置', exact: true }).getAttribute('aria-current'), { timeout: 5_000 })
|
||||
.poll(() => dialog.getByRole('button', { name: '插件', exact: true }).getAttribute('aria-current'), { timeout: 5_000 })
|
||||
.toBe('true')
|
||||
await expect
|
||||
.poll(() => dialog.getByRole('tab', { name: '插件配置', exact: true }).getAttribute('aria-selected'), { timeout: 5_000 })
|
||||
.toBe('true')
|
||||
return dialog
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
name: '@deepseek-ai/dsh-bash-sandbox'
|
||||
disabled: true
|
||||
- id: permission
|
||||
name: '@deepseek-ai/dsh-permission'
|
||||
name: '@deepseek-ai/dsh-permission-presets'
|
||||
disabled: true
|
||||
- insert:
|
||||
- id: pwsh-local
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Web e2e scenario: the resident question composer. The shipped composition
|
||||
// already exposes ask_user_question (the ui-question row's node half mounts
|
||||
// already exposes ask_user_question (the ui-user-questions row's node half mounts
|
||||
// the tool), so a recorded turn where the model asks blocks mid-turn on the
|
||||
// real userInteraction seam: the composer renders in the browser, the test
|
||||
// answers through it, and the turn completes with the answer in the log.
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// Trusted non-loopback Web access must not wedge on the loopback-only
|
||||
// settings API while the mandatory product notice owns the viewport.
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, launchWebScaffold, watchConsole, webSnapshotMode,
|
||||
WELCOME_NOTICE_COPY,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: remote welcome notice', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ remoteAuthority: 'remote.localhost', welcomeNoticePending: true })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('#root', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('advances process-locally and presents the notice again after reload', async () => {
|
||||
const welcome = page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title })
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true)
|
||||
|
||||
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
await expect.poll(
|
||||
() => page.locator('#root').evaluate(root => (root as HTMLElement).inert),
|
||||
{ timeout: 15_000 },
|
||||
).toBe(false)
|
||||
const reloadWarnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, reloadWarnings)
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -14,7 +14,7 @@
|
||||
// Composition divergences from `dsh web`, all deliberate, all via include
|
||||
// patches after the shipped bundle layers, over the SAME tree (never a
|
||||
// second yml): temp persistenceRoot; host-level skill roots confined to the
|
||||
// temp workspace while project skill discovery remains real; workspace-context
|
||||
// temp workspace while project skill discovery remains real; agent-instructions
|
||||
// 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
|
||||
@@ -40,21 +40,7 @@ import {
|
||||
healProfilesModuleFallback,
|
||||
loadOverlayPatches,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { dshHomePath } from '@deepseek-ai/dsh-paths'
|
||||
// Client packages must not be imported here: these e2e type-check in the Host
|
||||
// aggregate, so a Client import pulls that package's whole project — and every
|
||||
// project it references — into the Host build graph. Mirrored from
|
||||
// packages/client/ui-settings-general/src/onboarding-copy.ts; a drift makes the
|
||||
// pre-acknowledgement stop suppressing the notice, which fails loudly.
|
||||
// import {
|
||||
// WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY,
|
||||
// } from '@deepseek-ai/dsh-client-ui-settings-general'
|
||||
export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-onboarding'
|
||||
export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion'
|
||||
export const WELCOME_NOTICE_VERSION = '2026-07-30.7'
|
||||
export const WELCOME_NOTICE_COPY = { zh: { title: '内测声明', continueLabel: '继续' } } as const
|
||||
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type {
|
||||
LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk,
|
||||
@@ -69,9 +55,8 @@ import SessionStore, {
|
||||
type SessionEvent,
|
||||
type SessionHeader,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
// Empty type imports carry the httpServer/agents/sessionPersistence Context merges.
|
||||
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
// Empty type imports carry the webServer/agents/sessionPersistence Context merges.
|
||||
import type {} from '@deepseek-ai/dsh-host-webserver'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
@@ -101,7 +86,7 @@ const SHIPPED_PRESET_DIR = join(REPO_ROOT, 'apps/cli/config/agent-presets')
|
||||
|
||||
// Replay publishes the provider catalog the gateway routes to (providers
|
||||
// mode, never catch-all: with llm-deepseek disabled no adapter exists, so a
|
||||
// catch-all would leave resolveModelInfo unroutable and compact-basic's
|
||||
// catch-all would leave resolveModelInfo unroutable and compaction-basic's
|
||||
// post-step pressure check would warn every step). The published
|
||||
// contextWindow keeps that pressure path provably inert for small fixtures.
|
||||
const REPLAY_PROVIDERS = [{
|
||||
@@ -166,7 +151,7 @@ export interface WebScaffold {
|
||||
baseUrl: string
|
||||
/** Settled root context (the in-process readiness barrier; headless event subscription is its sanctioned use). */
|
||||
ctx: Context
|
||||
/** Temp project directory sessions run in (bash/fs tool cwd). */
|
||||
/** Temp project directory sessions run in (shell/fs tool cwd). */
|
||||
workspaceCwd: string
|
||||
/** Temp persistence root (seeded sessions land here through the real API). */
|
||||
persistenceRoot: string
|
||||
@@ -217,7 +202,7 @@ export interface LaunchOptions {
|
||||
*/
|
||||
toolsMode?: 'native' | 'code' | 'both'
|
||||
/**
|
||||
* Insert the opt-in self-referential Cordis tools into the shipped tree.
|
||||
* Insert the opt-in model-facing Cordis tool provider into the shipped tree.
|
||||
* Record and replay use the same tool surface, so captured request headers
|
||||
* remain reconstructable without making the tools a product default.
|
||||
*/
|
||||
@@ -253,8 +238,6 @@ export interface LaunchOptions {
|
||||
/** The preset a session that names none is composed from. */
|
||||
default: string
|
||||
}
|
||||
/** Leave the current welcome notice unacknowledged; ordinary scenarios publish it as complete before browser boot. */
|
||||
welcomeNoticePending?: boolean
|
||||
/**
|
||||
* Mount the shipped telemetry row in FULL mode against this exporter URL
|
||||
* instead of disabling it. Used to pin a real backend disclosure in
|
||||
@@ -392,6 +375,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
},
|
||||
},
|
||||
{ id: 'session-persistence-jsonl', config: { root: persistenceRoot } },
|
||||
// Content search is enabled here although the shipped bundles default it
|
||||
// off (`openAt: never`, pinned by apps/cli/tests/lazy-search-startup):
|
||||
// the seeded-session scenarios navigate by content search, and these e2e
|
||||
// runs are the assembled coverage for the opt-in search path.
|
||||
{ id: 'session-query-sqlite', config: { path: ':memory:', openAt: 'first-search' } },
|
||||
// storage-json's yml root is anchored to the real $DSH_HOME; pin the row
|
||||
// to an absolute temp root (removed with the workspace at close) so tests
|
||||
@@ -402,7 +389,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// cannot change replay requests or conversation goldens. Project roots stay
|
||||
// enabled against the same empty temp workspace, preserving the real seam.
|
||||
{
|
||||
id: 'skill-local',
|
||||
id: 'skill-filesystem',
|
||||
config: {
|
||||
dshHome: join(workspaceCwd, '.dsh-home'),
|
||||
agentsHome: join(workspaceCwd, '.agents-home'),
|
||||
@@ -413,21 +400,28 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// 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: 'agent-instructions', 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). A scenario that pins a real backend
|
||||
// disclosure passes a local dead endpoint instead of disabling the row.
|
||||
options.telemetryUrl === undefined
|
||||
? { id: 'telemetry-otel', disabled: true }
|
||||
: { id: 'telemetry-otel', config: { exporter: { url: options.telemetryUrl }, shutdownTimeoutMillis: 1_000 } },
|
||||
? { id: 'session-telemetry-otel', disabled: true }
|
||||
: {
|
||||
id: 'session-telemetry-otel',
|
||||
config: {
|
||||
mode: 'FULL',
|
||||
exporter: { url: options.telemetryUrl },
|
||||
shutdownTimeoutMillis: 1_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'webserver',
|
||||
config: { host: '127.0.0.1', port: 0 },
|
||||
},
|
||||
// The bundle's web-runtime row resolves the same built dist under test
|
||||
// (apps/web IS @deepseek-ai/dsh-frontend); only the URL line is silenced.
|
||||
// (apps/web IS @deepseek-ai/dsh-web-frontend); only the URL line is silenced.
|
||||
// Preserve the composed surface-context choice because a patch replaces
|
||||
// the row's complete config.
|
||||
{ id: 'web-runtime', config: { printUrl: false, surfaceContext } },
|
||||
@@ -445,7 +439,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
{ id: 'directory-picker', disabled: true },
|
||||
{ insert: [
|
||||
{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' },
|
||||
{ id: 'ui-directory-picker', name: '@deepseek-ai/dsh-client-ui-directory-picker' },
|
||||
{ id: 'ui-directory-picker-browse', name: '@deepseek-ai/dsh-client-ui-directory-picker-browse' },
|
||||
] },
|
||||
...options.agentPresets === undefined
|
||||
? []
|
||||
@@ -453,8 +447,12 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// be able to change a golden, whatever roots a scenario asks for.
|
||||
: [{ id: 'agent-presets', config: { ...options.agentPresets, includeUserRoot: false } }],
|
||||
...options.toolsMode === undefined ? [] : [{ id: 'tools', config: { mode: options.toolsMode } }],
|
||||
// The shipped Web bundle already owns both runners and the Cordis UI. This
|
||||
// scenario adds only the model-facing tools that exercise those services.
|
||||
...options.cordisTools === true
|
||||
? [{ insert: [{ id: 'tool-cordis', name: 'cordis:tool-cordis' }] }]
|
||||
? [{ insert: [
|
||||
{ id: 'tool-cordis', name: '@deepseek-ai/dsh-tool-cordis' },
|
||||
] }]
|
||||
: [],
|
||||
...options.deepSeekSearch === undefined
|
||||
? []
|
||||
@@ -506,23 +504,15 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// and a preset resolving package names from its own directory cannot reach
|
||||
// `@deepseek-ai/cordis-plugin-group` by name.
|
||||
ctx.loader.builtins.group = Group
|
||||
// The shipped CLI deliberately has no dependency on this opt-in package.
|
||||
// Keep the Loader row real without broadening the product installation.
|
||||
if (options.cordisTools === true) ctx.loader.builtins['tool-cordis'] = ToolCordis
|
||||
await ctx.loader.create({
|
||||
name: 'cordis:include',
|
||||
config: { path: pathToFileURL(rootConfig).href, patches },
|
||||
})
|
||||
await ctx.loader.await()
|
||||
assertEntriesLoaded(ctx, 'web e2e scaffold')
|
||||
if (options.welcomeNoticePending !== true) {
|
||||
await ctx.settings.mutate(settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), [{
|
||||
op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION,
|
||||
}])
|
||||
}
|
||||
const boundPort = ctx.get('httpServer')?.port
|
||||
const boundPort = ctx.get('webServer')?.port
|
||||
if (boundPort === undefined) {
|
||||
throw new Error('web e2e scaffold: httpServer service missing after settled boot')
|
||||
throw new Error('web e2e scaffold: webServer service missing after settled boot')
|
||||
}
|
||||
port = boundPort
|
||||
|
||||
@@ -718,7 +708,7 @@ export async function seedSession(
|
||||
await seeder.plugin(SessionStore)
|
||||
// Same root as the booted tree with the plugin's own default compression,
|
||||
// so the host's directory-scan list() sees one consistent encoding.
|
||||
await seeder.plugin(SessionPersistenceJsonl, { root: scaffold.persistenceRoot })
|
||||
await seeder.plugin(JsonlSessionPersistence, { root: scaffold.persistenceRoot })
|
||||
await seeder.sessionPersistence.create(meta)
|
||||
await seeder.sessionPersistence.append(meta.id, events)
|
||||
// Deterministic sidebar order: cold summaries take updatedAt from mtime.
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
foldScheduleEvents,
|
||||
resolveEveryOccurrence,
|
||||
type EveryScheduleRecord,
|
||||
} from '@deepseek-ai/dsh-tool-schedule'
|
||||
} from '@deepseek-ai/dsh-schedule'
|
||||
import {
|
||||
assertFixtureInventory,
|
||||
captureStableAria,
|
||||
@@ -167,7 +167,7 @@ function requestText(options: GenerateOptions): string {
|
||||
/** Require one assembled request to preserve the reminder-content trust boundary. */
|
||||
function expectReminderFraming(options: GenerateOptions): void {
|
||||
const reminder = options.messages.find(message => (
|
||||
message.source.kind === 'plugin' && message.source.plugin === 'tool-schedule'
|
||||
message.source.kind === 'plugin' && message.source.plugin === 'schedule'
|
||||
))
|
||||
expect(reminder?.role).toBe('user')
|
||||
const text = reminder?.content.find(block => block.type === 'text')?.text
|
||||
@@ -242,7 +242,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
|
||||
.toBe(AT_BROWSER_ZONE)
|
||||
|
||||
const cwd = join(scaffold.workspaceCwd, 'workspace')
|
||||
const workspace = await scaffold.ctx.workspace.resolveByPath(cwd)
|
||||
const workspace = await scaffold.ctx.workspaceRegistry.resolveByPath(cwd)
|
||||
if (workspace === undefined) throw new Error('connected Web workspace was not registered')
|
||||
|
||||
afterHandle = await scaffold.ctx.agents.create({
|
||||
@@ -421,7 +421,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
|
||||
const batch = everyHandle.agent.session.events.find(event => (
|
||||
event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === 'tool-schedule'
|
||||
&& event.data.source.plugin === 'schedule'
|
||||
&& event.data.content.some(block => block.type === 'text'
|
||||
&& block.text.startsWith('[SCHEDULE REMINDER BATCH]'))
|
||||
))
|
||||
|
||||
@@ -19,7 +19,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm'
|
||||
import { deriveEventMessage, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { TokenMeterService } from '@deepseek-ai/dsh-token-meter'
|
||||
import type { TokenMeter } from '@deepseek-ai/dsh-token-meter'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
@@ -46,13 +46,13 @@ const PROMPT = 'Use the read tool twice in one assistant message: read a.txt and
|
||||
* presentation through the real host and browser.
|
||||
* @param raw - the seed fixture text, already realized (placeholder-free) so
|
||||
* the shadow price below is computed from the exact strings the host folds.
|
||||
* @param meter - the composed token meter; the appended `compact/summary`'s
|
||||
* @param meter - the composed token meter; the appended `compaction/summary`'s
|
||||
* shadow price must be the exact heuristic price of the shadowed nodes, the
|
||||
* way compact-basic derives it, because the token-meter projections subtract
|
||||
* way compaction-basic derives it, because the token-meter projections subtract
|
||||
* it verbatim.
|
||||
* @returns the fixture with a manual compaction lifecycle appended.
|
||||
*/
|
||||
function withCompaction(raw: string, meter: TokenMeterService): string {
|
||||
function withCompaction(raw: string, meter: TokenMeter): string {
|
||||
const lines = raw.trimEnd().split('\n')
|
||||
const events = lines.slice(1).map(line => JSON.parse(line) as {
|
||||
type: string
|
||||
@@ -96,7 +96,7 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
|
||||
data: { commandId, name: 'compact', args: '', source: { kind: 'user' } },
|
||||
})
|
||||
const startSeq = at({
|
||||
type: 'compact/start',
|
||||
type: 'compaction/start',
|
||||
data: { compactionId, sourceCommandId: commandId, turn: null },
|
||||
})
|
||||
// Load-bearing exactness: the projections subtract this count verbatim, so
|
||||
@@ -125,7 +125,7 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
|
||||
return total + priceRow(event)
|
||||
}, 0)
|
||||
const summarySeq = at({
|
||||
type: 'compact/summary',
|
||||
type: 'compaction/summary',
|
||||
data: {
|
||||
compactionId,
|
||||
sourceCommandId: commandId,
|
||||
@@ -155,7 +155,7 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
|
||||
sourceEventSeqs: [startSeq, summarySeq, ...surfaceSeqs],
|
||||
})
|
||||
at({
|
||||
type: 'compact/end',
|
||||
type: 'compaction/end',
|
||||
data: { compactionId, sourceCommandId: commandId, turn: null },
|
||||
})
|
||||
at({
|
||||
@@ -256,6 +256,12 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
// client's "omitted key = capability absent → clear the row" rule from
|
||||
// wiping preset-owned projections on cold reads.
|
||||
expect(projections?.values).toHaveProperty('todos', null)
|
||||
// The session-stats unit is a shipped web-app bundle row: whole-log
|
||||
// turn/step counts ride the same tail block (the stats strip's source).
|
||||
const sessionStats = projections?.values.sessionStats as { turns: number; steps: number } | undefined
|
||||
expect(sessionStats).toBeDefined()
|
||||
expect(sessionStats?.turns).toBeGreaterThanOrEqual(1)
|
||||
expect(sessionStats?.steps).toBeGreaterThanOrEqual(sessionStats?.turns ?? 0)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('lists the seeded session cold and renders its history from the log', async () => {
|
||||
@@ -296,7 +302,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
+ '\n</system-reminder>',
|
||||
}],
|
||||
source: {
|
||||
kind: 'workspace-instructions',
|
||||
kind: 'agent-instructions',
|
||||
form: 'instructions',
|
||||
baseline: true,
|
||||
changes: [{
|
||||
@@ -457,9 +463,9 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
if (done?.type !== 'command/done') throw new Error('feedback command did not settle')
|
||||
const [sessionLine, userLine, extraLine] = done.data.text?.split('\n') ?? []
|
||||
expect(sessionLine).toBe(`Feedback recorded for session ${SEED_ID}`)
|
||||
expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\./i)
|
||||
expect(userLine).toMatch(/^Anonymous user: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\./i)
|
||||
expect(extraLine).toBeUndefined()
|
||||
const userId = userLine?.match(/^User: ([0-9a-f-]+)/i)?.[1]
|
||||
const userId = userLine?.match(/^Anonymous user: ([0-9a-f-]+)/i)?.[1]
|
||||
if (userId === undefined) throw new Error('feedback command omitted the user id')
|
||||
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Web e2e scenarios: the settings surface — the modal shell (trigger, nav,
|
||||
// section switching, both close paths), the Appearance preference row (the
|
||||
// real theme gesture — click 深色 and the whole cascade runs: ThemeService preference -> Host settings
|
||||
// real theme gesture — click 深色 and the whole cascade runs: ThemeRuntime preference -> Host settings
|
||||
// -> theme/change -> ui-layout's presenter -> body attribute -> alias token +
|
||||
// browser theme-color metadata)
|
||||
// the Language row and busy-state Enter preference (both Host-backed), plus
|
||||
@@ -99,6 +99,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
// an unrelated plugin does not rewrite this surface's golden.
|
||||
await dialog.getByRole('button', { name: '插件', exact: true }).click()
|
||||
await dialog.getByRole('heading', { name: '插件', exact: true }).waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('tab', { name: '插件列表', exact: true }).click()
|
||||
const pluginRow = dialog.locator(PLUGIN_ROW_SELECTOR)
|
||||
await pluginRow.waitFor({ timeout: 10_000 })
|
||||
const expectedPluginCount = [...scaffold.ctx.loader.entries()]
|
||||
@@ -109,6 +110,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(await dialog.locator('[data-plugin-count]').getAttribute('data-plugin-count'))
|
||||
.toBe(String(expectedPluginCount))
|
||||
expect(await dialog.getByRole('button', { name: '插件', exact: true }).getAttribute('aria-current')).toBe('true')
|
||||
expect(await dialog.getByRole('tab', { name: '插件列表', exact: true }).getAttribute('aria-selected')).toBe('true')
|
||||
expect(await dialog.getByRole('button', { name: '模型' }).getAttribute('aria-current')).toBeNull()
|
||||
const pluginsSnapshot = await captureStableAria(
|
||||
page,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Boots the shipped Web composition over the built dist this lane already uses
|
||||
// and asserts what that composition produces: the model-visible tool catalog
|
||||
// and the sandbox/approval knobs it ships with. No browser and no model call —
|
||||
// these are composition facts, and the browser scenarios in this lane cover the
|
||||
// surface itself.
|
||||
// and file-reference guidance plus the sandbox/approval knobs it ships with.
|
||||
// No browser and no model call — these are composition facts, and the browser
|
||||
// scenarios in this lane cover the surface itself.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
|
||||
@@ -12,17 +14,22 @@ import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import type {} from '@deepseek-ai/dsh-user-approval'
|
||||
import type {} from '@deepseek-ai/dsh-permission'
|
||||
import type {} from '@deepseek-ai/dsh-permission-presets'
|
||||
import type {} from '@deepseek-ai/dsh-agent-presets'
|
||||
import type {} from '@deepseek-ai/dsh-commands'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
|
||||
|
||||
const FILE_REFERENCE_PROMPT = fileURLToPath(new URL(
|
||||
'./snapshots/web-runtime-context/file-reference-prompt.expected.md', import.meta.url,
|
||||
))
|
||||
|
||||
/**
|
||||
* The catalog the shipped Web composition puts in front of the model, minus the
|
||||
* ripgrep-dependent pair below. The absences are deliberate, not incidental
|
||||
* gaps: the `cordis_*` toolset executes model-written JavaScript that no
|
||||
* sandbox row confines, `web_fetch` chooses its own request target, and
|
||||
* `mcp_*` servers spawn outside `ctx.bash`. The composition Agent Note owns the
|
||||
* `mcp_*` servers spawn outside `ctx.shell`. The composition Agent Note owns the
|
||||
* rationale and its sources.
|
||||
*/
|
||||
const EXPECTED_TOOLS = [
|
||||
@@ -33,6 +40,9 @@ const EXPECTED_TOOLS = [
|
||||
'exit_plan_mode',
|
||||
'get_goal',
|
||||
'interrupt_agent',
|
||||
'job_kill',
|
||||
'job_list',
|
||||
'job_output',
|
||||
'list_agents',
|
||||
'ralph',
|
||||
'read',
|
||||
@@ -41,9 +51,6 @@ const EXPECTED_TOOLS = [
|
||||
'skill',
|
||||
'subagent',
|
||||
'subagent_fork',
|
||||
'task_kill',
|
||||
'task_list',
|
||||
'task_output',
|
||||
'todo_write',
|
||||
'update_goal',
|
||||
'web_search',
|
||||
@@ -66,7 +73,7 @@ afterEach(async () => {
|
||||
scaffold = undefined
|
||||
})
|
||||
|
||||
it('assembles the shipped Web catalog with the confined access default', async () => {
|
||||
it('assembles the shipped Web catalog, file-reference guidance, and confined access default', async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
const ctx = scaffold.ctx
|
||||
// The catalog belongs to an AGENT, not to the process: every model-facing row
|
||||
@@ -85,6 +92,9 @@ it('assembles the shipped Web catalog with the confined access default', async (
|
||||
// The packaged ripgrep binary ships with the dependency, so the pair is a
|
||||
// fixed roster member on every host.
|
||||
expect(names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS)
|
||||
const fileReferenceSection = (await ctx.systemPrompt.assemble({ scope: handle.agent })).sections
|
||||
.find(section => section.name === 'ui:deliverable-file-references')
|
||||
expect(fileReferenceSection?.text).toBe(readFileSync(FILE_REFERENCE_PROMPT, 'utf8').trimEnd())
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
@@ -98,7 +108,7 @@ it('assembles the shipped Web catalog with the confined access default', async (
|
||||
)
|
||||
expect(scaffold.ctx.sandboxPolicy.defaultMode).toBe('workspace-write')
|
||||
expect(scaffold.ctx.approval.config.policy).toBe('ask')
|
||||
expect(scaffold.ctx.permission.defaultPreset).toBe('workspace-write')
|
||||
expect(scaffold.ctx.permissionPresets.defaultPreset).toBe('workspace-write')
|
||||
|
||||
const commandHandle = await scaffold.ctx.agents.create({
|
||||
sessionId: SessionId('shipped-command-catalog'),
|
||||
@@ -116,11 +126,11 @@ it('assembles the shipped Web catalog with the confined access default', async (
|
||||
}
|
||||
}, 120_000)
|
||||
|
||||
it('lets a preset producer reach the background-task registry', async () => {
|
||||
it('lets a preset producer reach the background-job registry', async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
const ctx = scaffold.ctx
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId('shipped-background-task'),
|
||||
sessionId: SessionId('shipped-background-job'),
|
||||
meta: { cwd: scaffold.workspaceCwd },
|
||||
setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
|
||||
})
|
||||
@@ -142,7 +152,7 @@ it('lets a preset producer reach the background-task registry', async () => {
|
||||
})
|
||||
expect({ isError: started.isError, content: started.content }).toEqual({
|
||||
isError: false,
|
||||
content: [{ type: 'text', text: 'started background task bash-1' }],
|
||||
content: [{ type: 'text', text: 'started background job bash-1' }],
|
||||
})
|
||||
|
||||
// The controller reads what the producer started: same registry, one
|
||||
@@ -150,7 +160,7 @@ it('lets a preset producer reach the background-task registry', async () => {
|
||||
const listed = await ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('shipped-task-list'),
|
||||
name: 'task_list',
|
||||
name: 'job_list',
|
||||
arguments: {},
|
||||
agent: handle.agent,
|
||||
})
|
||||
@@ -164,8 +174,8 @@ it('lets a preset producer reach the background-task registry', async () => {
|
||||
const collected = await ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('shipped-task-output'),
|
||||
name: 'task_output',
|
||||
arguments: { task_id: 'bash-1', wait: true },
|
||||
name: 'job_output',
|
||||
arguments: { job_id: 'bash-1', wait: true },
|
||||
agent: handle.agent,
|
||||
})
|
||||
expect(collected.isError).toBe(false)
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('web e2e: sidebar subagent activity', () => {
|
||||
agentOptions: { provider: HOLD_PROVIDER, model: HOLD_MODEL },
|
||||
})
|
||||
parentHandle.agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: 'Delegate a background task.' }],
|
||||
content: [{ type: 'text', text: 'Delegate a background job.' }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
await parentHandle.agent.whenIdle()
|
||||
@@ -114,7 +114,7 @@ describe('web e2e: sidebar subagent activity', () => {
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
const workspace = await scaffold.ctx.workspace.resolveByPath(cwd)
|
||||
const workspace = await scaffold.ctx.workspaceRegistry.resolveByPath(cwd)
|
||||
if (workspace === undefined) throw new Error('connected Web workspace was not registered')
|
||||
await workspace.attachSession(parentHandle.agent.session.id)
|
||||
}, 60_000)
|
||||
@@ -136,7 +136,7 @@ describe('web e2e: sidebar subagent activity', () => {
|
||||
it('pins a running descendant on its visible idle owner row', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-subagent-activity'))
|
||||
const sidebar = page.getByRole('tree', { name: 'Sessions' })
|
||||
const ownerRow = sidebar.getByRole('treeitem', { name: /1 subagent running Delegate a background task/ })
|
||||
const ownerRow = sidebar.getByRole('treeitem', { name: /1 subagent running Delegate a background job/ })
|
||||
await ownerRow.waitFor({ timeout: 10_000 })
|
||||
expect(parentHandle.agent.status).toBe('idle')
|
||||
await compareOrRefreshGolden(
|
||||
|
||||
@@ -138,13 +138,13 @@ async function detailsTrack(page: Page): Promise<number> {
|
||||
return Number(cols.split(' ').pop()!.replace('px', ''))
|
||||
}
|
||||
|
||||
// Readiness gate: `dsh web` serves all ten production manifest plugins; until every UI
|
||||
// Readiness gate: `dsh web` serves every production manifest plugin; until every UI
|
||||
// plugin's client bundle exists and exports apply, the loader fail-louds and
|
||||
// the frame never appears.
|
||||
const UI_PLUGIN_DIRS = [
|
||||
'connection', 'runtime', 'ui-theme', 'locale', 'ui-layout', 'ui-sidebar',
|
||||
'ui-settings', 'ui-settings-general', 'ui-models', 'ui-conversation',
|
||||
'ui-model', 'ui-question', 'ui-trajectory',
|
||||
'ui-settings', 'ui-settings-general', 'ui-settings-models', 'ui-conversation',
|
||||
'ui-model-selection', 'ui-user-questions', 'ui-trajectory', '../session-query/session-log-export',
|
||||
]
|
||||
const ROUND_DONE_MARKER = 'WEB_ROUND_DONE'
|
||||
const notReady = UI_PLUGIN_DIRS.filter((dir) => {
|
||||
@@ -533,16 +533,9 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
|
||||
it('empty-state first send completes a real model round', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'w5-first-round'))
|
||||
// This scenario spawns its own server against a fresh $DSH_HOME, so the
|
||||
// first-run welcome notice is unacknowledged and its overlay owns pointer
|
||||
// events (the shared scaffold acknowledges it before boot instead). The
|
||||
// notice is anchored structurally, not by its copy: this spec sits in the
|
||||
// client TypeScript program, which does not reference the package that
|
||||
// owns the strings.
|
||||
const welcome = page.locator('[class*="onboardingOverlay"]')
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
await welcome.getByRole('button').click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
// This scenario spawns its own server against a fresh $DSH_HOME with the
|
||||
// DeepSeek credential inherited from the environment, so no onboarding
|
||||
// step mounts and the page is immediately interactive.
|
||||
// Fresh world: connect a Workspace so the composer starts live.
|
||||
await connectFreshWorkspace(page, sessionsDir)
|
||||
const input = page.locator('textarea').first()
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
@@ -35,13 +32,13 @@
|
||||
- img
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 代码模式"':
|
||||
- text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- 'button "设为默认: PTC 模式"':
|
||||
- text: PTC 模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- code: code
|
||||
- 'button "查看: 代码模式"':
|
||||
- 'button "查看: PTC 模式"':
|
||||
- img
|
||||
- text: 查看
|
||||
- 'button "复制: 代码模式"':
|
||||
- 'button "复制: PTC 模式"':
|
||||
- img
|
||||
- text: 复制
|
||||
- listitem:
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
@@ -35,13 +32,13 @@
|
||||
- img
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 代码模式"':
|
||||
- text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- 'button "设为默认: PTC 模式"':
|
||||
- text: PTC 模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- code: code
|
||||
- 'button "查看: 代码模式"':
|
||||
- 'button "查看: PTC 模式"':
|
||||
- img
|
||||
- text: 查看
|
||||
- 'button "复制: 代码模式"':
|
||||
- 'button "复制: PTC 模式"':
|
||||
- img
|
||||
- text: 复制
|
||||
- listitem:
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
@@ -35,13 +32,13 @@
|
||||
- img
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 代码模式"':
|
||||
- text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- 'button "设为默认: PTC 模式"':
|
||||
- text: PTC 模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- code: code
|
||||
- 'button "查看: 代码模式"':
|
||||
- 'button "查看: PTC 模式"':
|
||||
- img
|
||||
- text: 查看
|
||||
- 'button "复制: 代码模式"':
|
||||
- 'button "复制: PTC 模式"':
|
||||
- img
|
||||
- text: 复制
|
||||
- listitem:
|
||||
|
||||
@@ -5,3 +5,6 @@
|
||||
- button "1 subagent":
|
||||
- text: 1 subagent
|
||||
- img
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
- list "Background tasks":
|
||||
- list "Background jobs":
|
||||
- listitem: bash sleep 45 running {{duration}}
|
||||
@@ -1,2 +1,2 @@
|
||||
- list "Background tasks":
|
||||
- list "Background jobs":
|
||||
- listitem: "bash sleep 45 signal: SIGTERM {{duration}}"
|
||||
@@ -1,6 +1,9 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Run two shell commands: wait" [disabled]'
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- 'button "Using ONE run_code program: run" [disabled]'
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -33,6 +36,10 @@
|
||||
- paragraph: DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
{"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}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":1,"time":1785157562882,"data":{"content":[{"type":"text","text":"Use only Cordis tools. First call cordis_inspect_self with no arguments. Then call cordis_define with plugin kind \"new\", idPrefix \"snap\", name \"snapshot noop\", purpose \"does nothing, for the snapshot\", code.host exactly \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\" and code.client exactly \"return { inject: [\\\"slots\\\"], apply(ctx) { ctx.slots.register({ name: \\\"shell.overlay\\\", id: \\\"snapshot-probe\\\" }, () => React.createElement(\\\"div\\\", { \\\"data-snapshot-probe\\\": \\\"loaded\\\" })) } }\". Read its returned pluginId and packageId, then call cordis_run with those exact IDs and mode \"run\". After the run request returns, reply exactly CORDIS_UI_READY 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-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"}}}
|
||||
{"type":"tool-call-chunks","seq0":88,"time0":1785157565360,"data":{"turn":1,"step":1,"index":1,"dt":[15,2,0,0,25,2,0,0,27,1],"id":"call_00_KZk918WtlKan9pHMULIT8794","name":"cordis_inspect","args":["","{","\"","what","\"",": ","\"","t","emporary","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":99,"time":1785157565490,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"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":"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":99,"time":1785157565490,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"I will inspect the current Session's dynamic Cordis Plugins before defining the snapshot Package."}}}}
|
||||
{"type":"assistant/chunk","seq":100,"time":1785157565491,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"cordis-inspect-self","name":"cordis_inspect_self","arguments":"{}"}}}}
|
||||
{"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,"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"}
|
||||
@@ -18,11 +16,9 @@
|
||||
{"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"}}}
|
||||
{"type":"reasoning-chunks","seq0":109,"time0":1785157566525,"data":{"turn":1,"step":2,"index":0,"dt":[105,30,2,0,0,24,2,0,0,27,2,1,0,25,0,0,0,1,0,39,1],"texts":["Good",","," no"," temporary"," plugins"," running","."," Now"," step"," ","2",":"," call"," cord","is","_m","ount"," with"," the"," exact"," code","."]}}
|
||||
{"type":"assistant/chunk","seq":131,"time":1785157566845,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":132,"time0":1785157566845,"data":{"turn":1,"step":2,"index":1,"dt":[42,4,1,0,0,15,2,0,0,0,16,0,0,0,0,1,23,2,0,0,25,7,18],"id":"call_00_OyrlxqqpnvzTe11sN5Oy2361","name":"cordis_mount","args":["","{","\"","code","\"",": ","\"","return"," {"," name",":"," \\\"","sn","apshot","-no","op","\\\","," apply","(ctx",")"," {}"," }","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":156,"time":1785157567041,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."}}}}
|
||||
{"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":156,"time":1785157567041,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"No dynamic Plugins are present, so I will define the requested Host and Client Package."}}}}
|
||||
{"type":"assistant/chunk","seq":157,"time":1785157567041,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"cordis-define","name":"cordis_define","arguments":"{\"plugin\":{\"kind\":\"new\",\"idPrefix\":\"snap\"},\"name\":\"snapshot noop\",\"purpose\":\"does nothing, for the snapshot\",\"code\":{\"host\":\"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\",\"client\":\"return { inject: [\\\"slots\\\"], apply(ctx) { ctx.slots.register({ name: \\\"shell.overlay\\\", id: \\\"snapshot-probe\\\" }, () => React.createElement(\\\"div\\\", { \\\"data-snapshot-probe\\\": \\\"loaded\\\" })) } }\"}}"}}}}
|
||||
{"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,"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"}
|
||||
@@ -31,11 +27,9 @@
|
||||
{"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"}}}
|
||||
{"type":"reasoning-chunks","seq0":166,"time0":1785157567836,"data":{"turn":1,"step":3,"index":0,"dt":[83,25,2,0,0,26,1,1,0,0,0,41,2,0,0,0,0,8,2,0,0,0],"texts":["The"," id"," is"," \"","d","yn","-","1","\"."," Now"," step"," ","3",":"," call"," cord","is","_un","mount"," with"," that"," id","."]}}
|
||||
{"type":"assistant/chunk","seq":189,"time":1785157568117,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":190,"time0":1785157568118,"data":{"turn":1,"step":3,"index":1,"dt":[17,2,1,24,2,0,1,25,0,0,0,29],"id":"call_00_e38S6zeYdZGvbhecUCil6659","name":"cordis_unmount","args":["","{","\"","id","\"",": ","\"","d","yn","-","1","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":203,"time":1785157568278,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."}}}}
|
||||
{"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":203,"time":1785157568278,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The Host returned snap-1/pkg-1, so I will request its first activation."}}}}
|
||||
{"type":"assistant/chunk","seq":204,"time":1785157568279,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"cordis-run","name":"cordis_run","arguments":"{\"pluginId\":\"snap-1\",\"packageId\":\"pkg-1\",\"mode\":\"run\"}"}}}}
|
||||
{"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,"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"}
|
||||
@@ -44,13 +38,20 @@
|
||||
{"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"}}}
|
||||
{"type":"reasoning-chunks","seq0":213,"time0":1785157569186,"data":{"turn":1,"step":4,"index":0,"dt":[165,26,26,1,0,27,9,17,2,1,0,0,0,44,1,0,0,1,0,6,3],"texts":["All"," three"," calls"," succeeded","."," I"," should"," now"," reply"," exactly"," \"","C","ORD","IS","_","UI","_D","ONE","\""," and"," stop","."]}}
|
||||
{"type":"assistant/chunk","seq":235,"time":1785157569515,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"text-chunks","seq0":236,"time0":1785157569515,"data":{"turn":1,"step":4,"index":1,"dt":[0,0,35,1,0,0],"texts":["C","ORD","IS","_","UI","_D","ONE"]}}
|
||||
{"type":"assistant/chunk","seq":243,"time":1785157569551,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"All three calls succeeded. I should now reply exactly \"CORDIS_UI_DONE\" and stop."}}}}
|
||||
{"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":243,"time":1785157569551,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The activation request has been submitted, so I will return the requested readiness marker."}}}}
|
||||
{"type":"assistant/chunk","seq":244,"time":1785157569551,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CORDIS_UI_READY"}}}}
|
||||
{"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,"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"}}}
|
||||
{"type":"user/message","seq":250,"time":1785157569555,"data":{"content":[{"type":"text","text":"Use only Cordis tools. Call cordis_stop with pluginId \"snap-1\". After it succeeds, reply exactly CORDIS_UI_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":251,"time":1785157569556,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":252,"time":1785157569557,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"cordis-stop","name":"cordis_stop","arguments":"{\"pluginId\":\"snap-1\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":253,"time":1785157569558,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":254,"time":1785157569559,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/chunk","seq":255,"time":1785157569560,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":256,"time":1785157569561,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORDIS_UI_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":257,"time":1785157569562,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":258,"time":1785157569563,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
|
||||
@@ -3,54 +3,90 @@
|
||||
- button "Use only Cordis tools. First" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- 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}}"
|
||||
- text: "Use only Cordis tools. First call cordis_inspect_self with no arguments. Then call cordis_define with plugin kind \"new\", idPrefix \"snap\", name \"snapshot noop\", purpose \"does nothing, for the snapshot\", code.host exactly \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\" and code.client exactly \"return { inject: [\\\"slots\\\"], apply(ctx) { ctx.slots.register({ name: \\\"shell.overlay\\\", id: \\\"snapshot-probe\\\" }, () => React.createElement(\\\"div\\\", { \\\"data-snapshot-probe\\\": \\\"loaded\\\" })) } }\". Read its returned pluginId and packageId, then call cordis_run with those exact IDs and mode \"run\". After the run request returns, reply exactly CORDIS_UI_READY and stop. {{clock}}"
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Context injection @deepseek-ai/dsh-system-prompt":
|
||||
- img
|
||||
- img
|
||||
- text: Context injection @deepseek-ai/dsh-system-prompt
|
||||
- button "Think The user wants me to:":
|
||||
- button "Think I will inspect the current Session's dynamic Cordis Plugins before defining the snapshot Package.":
|
||||
- img
|
||||
- img
|
||||
- text: "Think The user wants me to:"
|
||||
- button "Inspect temporary":
|
||||
- text: Think I will inspect the current Session's dynamic Cordis Plugins before defining the snapshot Package.
|
||||
- 'button "Tool call cordis_inspect_self · {}"':
|
||||
- img
|
||||
- img
|
||||
- text: Inspect temporary
|
||||
- 'button "Think Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."':
|
||||
- text: "Tool call cordis_inspect_self · {}"
|
||||
- button "Think No dynamic Plugins are present, so I will define the requested Host and Client Package.":
|
||||
- img
|
||||
- img
|
||||
- text: "Think Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."
|
||||
- 'button "Mount temporary Plugin return { name: \"snapshot-noop\", apply(ctx) {} }" [expanded]':
|
||||
- text: Think No dynamic Plugins are present, so I will define the requested Host and Client Package.
|
||||
- button "Register Cordis Plugin snapshot noop does nothing, for the snapshot Ready" [expanded]:
|
||||
- img
|
||||
- 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).
|
||||
- text: Register Cordis Plugin snapshot noop does nothing, for the snapshot Ready
|
||||
- tablist "Plugin source":
|
||||
- tab "Client"
|
||||
- tab "Host" [selected]
|
||||
- tabpanel "Host":
|
||||
- text: javascript
|
||||
- button "Copy"
|
||||
- code: "return { name: \"snapshot-noop\", apply(ctx) {} }"
|
||||
- text: Result Defined snap-1/pkg-1 (snapshot noop); it is not running yet. Use cordis_run to activate this Package. Run controls live in the Cordis panel above Settings
|
||||
- button "Inspect"
|
||||
- 'button "Think The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."':
|
||||
- button "Think The Host returned snap-1/pkg-1, so I will request its first activation.":
|
||||
- img
|
||||
- img
|
||||
- text: "Think The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."
|
||||
- button "Unmount temporary Plugin dyn-1":
|
||||
- text: Think The Host returned snap-1/pkg-1, so I will request its first activation.
|
||||
- img
|
||||
- text: Run Cordis Plugin snap-1 · pkg-1 Ready
|
||||
- button "Inspect"
|
||||
- text: snap-1/pkg-1 is awaiting user approval (run-1).
|
||||
- button "Think The activation request has been submitted, so I will return the requested readiness marker.":
|
||||
- img
|
||||
- img
|
||||
- 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
|
||||
- text: Think The activation request has been submitted, so I will return the requested readiness marker.
|
||||
- paragraph: CORDIS_UI_READY
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
- button "Context injection cordis-host-runner":
|
||||
- img
|
||||
- img
|
||||
- text: Context injection cordis-host-runner
|
||||
- img
|
||||
- text: Stop Cordis Plugin snap-1
|
||||
- button "Inspect"
|
||||
- text: Dynamic Plugin snap-1 is stopped; its definition and versions remain.
|
||||
- paragraph: CORDIS_UI_DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- status:
|
||||
- text: "This turn failedllm-replay: script exhausted — session requested model call #7 but its script has only 6; re-record the scenario"
|
||||
- code: UNKNOWN
|
||||
- button "Back to bottom":
|
||||
- img
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
@@ -58,6 +94,6 @@
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "13% of context used"
|
||||
- button "0% of context used"
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 4 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 77% Input 66.5K tok · Output 312 tok
|
||||
- text: 3 turns · 7 steps LLM {{duration}} · Tool call {{duration}} Cache hit 77% Input 66.5K tok · Output 318 tok
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "Reply with the single word" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -20,13 +23,17 @@
|
||||
- paragraph: LIGHTHOUSE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
- 'button "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."':
|
||||
- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled."':
|
||||
- img
|
||||
- img
|
||||
- text: "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."
|
||||
- text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled."
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
You are an AI agent powered by the DeepSeek Harness SDK.
|
||||
You are an AI agent powered by DeepSeek Harness.
|
||||
|
||||
The DeepSeek Harness implementation checkout is at {{sourceRoot}}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.
|
||||
|
||||
You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while `pnpm run dev:web` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.
|
||||
You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while `pnpm run dev:web` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background job and verify its exact URL.
|
||||
|
||||
You are a coding agent powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "Use the bash tool to" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -28,6 +31,10 @@
|
||||
- paragraph: DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "workspace" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"kind": "chunks",
|
||||
"chunks": [
|
||||
{ "type": "block-start", "index": 0, "blockType": "reasoning" },
|
||||
{ "type": "reasoning-delta", "index": 0, "text": "There are 4 packages:\n- client/ui-conversation\n- context/session-reference\n- llm/token-meter\n- skill/skill-local\n\nFor this turn (turn 1 of 2), I should randomly pick one package and output its file structure. Let me pick randomly — I'll use $RANDOM or just pick one. Let me use a quick random selection." },
|
||||
{ "type": "block-end", "index": 0, "block": { "type": "reasoning", "text": "There are 4 packages:\n- client/ui-conversation\n- context/session-reference\n- llm/token-meter\n- skill/skill-local\n\nFor this turn (turn 1 of 2), I should randomly pick one package and output its file structure. Let me pick randomly — I'll use $RANDOM or just pick one. Let me use a quick random selection." } },
|
||||
{ "type": "reasoning-delta", "index": 0, "text": "There are 4 packages:\n- client/ui-conversation\n- context/session-reference\n- llm/token-meter\n- skill/skill-filesystem\n\nFor this turn (turn 1 of 2), I should randomly pick one package and output its file structure. Let me pick randomly — I'll use $RANDOM or just pick one. Let me use a quick random selection." },
|
||||
{ "type": "block-end", "index": 0, "block": { "type": "reasoning", "text": "There are 4 packages:\n- client/ui-conversation\n- context/session-reference\n- llm/token-meter\n- skill/skill-filesystem\n\nFor this turn (turn 1 of 2), I should randomly pick one package and output its file structure. Let me pick randomly — I'll use $RANDOM or just pick one. Let me use a quick random selection." } },
|
||||
{ "type": "block-start", "index": 1, "blockType": "tool-call" },
|
||||
{ "type": "tool-call-delta", "index": 1, "id": "call_00_dAM4qfVp4LLtRCiJi7d71418", "name": "bash", "argumentsDelta": "{\"command\": \"find packages -maxdepth 2 -mindepth 2 -type d | sort | awk '{print NR\\\": \\\"$0}' && echo \\\"---random pick---\\\" && find packages -maxdepth 2 -mindepth 2 -type d | sort | PATH=/__dsh_fixture_missing__ shuf -n 1\", \"description\": \"Randomly pick one package directory\"}" },
|
||||
{ "type": "block-end", "index": 1, "block": { "type": "tool-call", "id": "call_00_dAM4qfVp4LLtRCiJi7d71418", "name": "bash", "arguments": "{\"command\": \"find packages -maxdepth 2 -mindepth 2 -type d | sort | awk '{print NR\\\": \\\"$0}' && echo \\\"---random pick---\\\" && find packages -maxdepth 2 -mindepth 2 -type d | sort | PATH=/__dsh_fixture_missing__ shuf -n 1\", \"description\": \"Randomly pick one package directory\"}" } },
|
||||
|
||||
@@ -42,20 +42,20 @@
|
||||
{"type":"assistant/chunk","seq":335,"time":1786073822633,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":336,"time":1786073822633,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"There's a `packages` directory. Let me look inside to see what packages are available."},{"type":"tool-call","id":"call_00_XlYaTHoKielffCybfcXb9253","name":"bash","arguments":"{\"command\": \"ls packages && echo \\\"---\\\" && find packages -maxdepth 2 -type d\", \"description\": \"List packages directory contents\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4f03ca00-aa2d-4a69-9675-58833e8d1b30"},"usage":{"inputTokens":363,"outputTokens":99,"cacheReadTokens":8064,"reasoningTokens":20}},"sourceEventSeqs":[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],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":337,"time":1786073822634,"data":{"turn":1,"step":2,"callId":"call_00_XlYaTHoKielffCybfcXb9253","name":"bash","arguments":"{\"command\": \"ls packages && echo \\\"---\\\" && find packages -maxdepth 2 -type d\", \"description\": \"List packages directory contents\"}"}}
|
||||
{"type":"tool/result","seq":338,"time":1786073822659,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_XlYaTHoKielffCybfcXb9253"},"content":[{"type":"tool-result","toolCallId":"call_00_XlYaTHoKielffCybfcXb9253","content":[{"type":"text","text":"client\ncontext\nllm\nskill\n---\npackages\npackages/llm\npackages/llm/token-meter\npackages/context\npackages/context/session-reference\npackages/skill\npackages/skill/skill-local\npackages/client\npackages/client/ui-conversation\n"}],"isError":false}],"role":"user","id":"8a53ae07-7f7a-424f-8095-96f216746bc4"}},"sourceEventSeqs":[337],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":338,"time":1786073822659,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_00_XlYaTHoKielffCybfcXb9253"},"content":[{"type":"tool-result","toolCallId":"call_00_XlYaTHoKielffCybfcXb9253","content":[{"type":"text","text":"client\ncontext\nllm\nskill\n---\npackages\npackages/llm\npackages/llm/token-meter\npackages/context\npackages/context/session-reference\npackages/skill\npackages/skill/skill-filesystem\npackages/client\npackages/client/ui-conversation\n"}],"isError":false}],"role":"user","id":"8a53ae07-7f7a-424f-8095-96f216746bc4"}},"sourceEventSeqs":[337],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":339,"time":1786073822659,"data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","seq":340,"time":1786073822668,"data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/chunk","seq":341,"time":1786073823725,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":342,"time0":1786073823726,"data":{"turn":1,"step":3,"index":0,"dt":[82,26,0,0,27,0,0,0,23,0,0,0,0,0,21,0,0,0,0,0,21,1,0,0,24,0,1,0,0,0,23,0,0,21,1,0,23,0,0,0,0,0,22,1,0,0,0,0,24,0,0,0,0,0,24,0,0,1,24,1,0,0,17,24,1,0,0,0,29,0,0,16,1,0,31,0,0,18,0],"texts":["There"," are"," ","4"," packages",":\n","-"," client","/ui","-con","vers","ation","\n","-"," context","/s","ession","-reference","\n","-"," ll","m","/t","oken","-meter","\n","-"," skill","/s","kill","-local","\n\n","For"," this"," turn"," (","turn"," ","1"," of"," ","2","),"," I"," should"," randomly"," pick"," one"," package"," and"," output"," its"," file"," structure","."," Let"," me"," pick"," randomly"," —"," I","'ll"," use"," $","R","AND","OM"," or"," just"," pick"," one","."," Let"," me"," use"," a"," quick"," random"," selection","."]}}
|
||||
{"type":"assistant/chunk","seq":422,"time":1786073824320,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":423,"time0":1786073824321,"data":{"turn":1,"step":3,"index":1,"dt":[24,1,1,0,0,29,1,14,1,0,0,0,24,0,1,21,0,0,41,1,0,0,0,0,11,0,0,20,0,0,26,0,0,16,1,0,28,1,23,0,1,0,28,0,0,19,0,0,0,0,1,13,1,0,0,0,0,27,1,0,28,0,0,0,28,1,26,1,0,0,19,1,0,0,22,1,0,18,0,23],"id":"call_00_dAM4qfVp4LLtRCiJi7d71418","name":"bash","args":["","{","\"","command","\"",": ","\"","find"," packages"," -","max","depth"," ","2"," -","mind","epth"," ","2"," -","type"," d"," |"," sort"," |"," awk"," '{","print"," NR","\\\":"," \\\"$","0","}'"," &&"," echo"," \\\"","---","random"," pick","---","\\\""," &&"," find"," packages"," -","max","depth"," ","2"," -","mind","epth"," ","2"," -","type"," d"," |"," sort"," |"," sh","uf"," -","n"," ","1","\"",", ","\"","description","\"",": ","\"","Random","ly"," pick"," one"," package"," directory","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":504,"time":1786073824907,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"There are 4 packages:\n- client/ui-conversation\n- context/session-reference\n- llm/token-meter\n- skill/skill-local\n\nFor this turn (turn 1 of 2), I should randomly pick one package and output its file structure. Let me pick randomly — I'll use $RANDOM or just pick one. Let me use a quick random selection."}}}}
|
||||
{"type":"assistant/chunk","seq":504,"time":1786073824907,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"There are 4 packages:\n- client/ui-conversation\n- context/session-reference\n- llm/token-meter\n- skill/skill-filesystem\n\nFor this turn (turn 1 of 2), I should randomly pick one package and output its file structure. Let me pick randomly — I'll use $RANDOM or just pick one. Let me use a quick random selection."}}}}
|
||||
{"type":"assistant/chunk","seq":505,"time":1786073824908,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_dAM4qfVp4LLtRCiJi7d71418","name":"bash","arguments":"{\"command\": \"find packages -maxdepth 2 -mindepth 2 -type d | sort | awk '{print NR\\\": \\\"$0}' && echo \\\"---random pick---\\\" && find packages -maxdepth 2 -mindepth 2 -type d | sort | shuf -n 1\", \"description\": \"Randomly pick one package directory\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":506,"time":1786073824908,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":156,"outputTokens":202,"cacheReadTokens":8448,"reasoningTokens":80}}}}
|
||||
{"type":"assistant/chunk","seq":507,"time":1786073824908,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":508,"time":1786073824909,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"There are 4 packages:\n- client/ui-conversation\n- context/session-reference\n- llm/token-meter\n- skill/skill-local\n\nFor this turn (turn 1 of 2), I should randomly pick one package and output its file structure. Let me pick randomly — I'll use $RANDOM or just pick one. Let me use a quick random selection."},{"type":"tool-call","id":"call_00_dAM4qfVp4LLtRCiJi7d71418","name":"bash","arguments":"{\"command\": \"find packages -maxdepth 2 -mindepth 2 -type d | sort | awk '{print NR\\\": \\\"$0}' && echo \\\"---random pick---\\\" && find packages -maxdepth 2 -mindepth 2 -type d | sort | shuf -n 1\", \"description\": \"Randomly pick one package directory\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"9e27c6d1-afa2-464c-b4c9-2ca7a44035f6"},"usage":{"inputTokens":156,"outputTokens":202,"cacheReadTokens":8448,"reasoningTokens":80}},"sourceEventSeqs":[341,342,343,344,345,346,347,348,349,350,351,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,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":508,"time":1786073824909,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"reasoning","text":"There are 4 packages:\n- client/ui-conversation\n- context/session-reference\n- llm/token-meter\n- skill/skill-filesystem\n\nFor this turn (turn 1 of 2), I should randomly pick one package and output its file structure. Let me pick randomly — I'll use $RANDOM or just pick one. Let me use a quick random selection."},{"type":"tool-call","id":"call_00_dAM4qfVp4LLtRCiJi7d71418","name":"bash","arguments":"{\"command\": \"find packages -maxdepth 2 -mindepth 2 -type d | sort | awk '{print NR\\\": \\\"$0}' && echo \\\"---random pick---\\\" && find packages -maxdepth 2 -mindepth 2 -type d | sort | shuf -n 1\", \"description\": \"Randomly pick one package directory\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"9e27c6d1-afa2-464c-b4c9-2ca7a44035f6"},"usage":{"inputTokens":156,"outputTokens":202,"cacheReadTokens":8448,"reasoningTokens":80}},"sourceEventSeqs":[341,342,343,344,345,346,347,348,349,350,351,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,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":509,"time":1786073824910,"data":{"turn":1,"step":3,"callId":"call_00_dAM4qfVp4LLtRCiJi7d71418","name":"bash","arguments":"{\"command\": \"find packages -maxdepth 2 -mindepth 2 -type d | sort | awk '{print NR\\\": \\\"$0}' && echo \\\"---random pick---\\\" && find packages -maxdepth 2 -mindepth 2 -type d | sort | shuf -n 1\", \"description\": \"Randomly pick one package directory\"}"}}
|
||||
{"type":"tool/result","seq":510,"time":1786073824937,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_00_dAM4qfVp4LLtRCiJi7d71418"},"content":[{"type":"tool-result","toolCallId":"call_00_dAM4qfVp4LLtRCiJi7d71418","content":[{"type":"text","text":"1: packages/client/ui-conversation\n2: packages/context/session-reference\n3: packages/llm/token-meter\n4: packages/skill/skill-local\n---random pick---\n[stderr]\nbash: shuf: command not found\n[exit code: 127]"}],"isError":false}],"role":"user","id":"3227ba71-d0f0-4b4e-82e0-b01c6cd1ded1"}},"sourceEventSeqs":[509],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":510,"time":1786073824937,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_00_dAM4qfVp4LLtRCiJi7d71418"},"content":[{"type":"tool-result","toolCallId":"call_00_dAM4qfVp4LLtRCiJi7d71418","content":[{"type":"text","text":"1: packages/client/ui-conversation\n2: packages/context/session-reference\n3: packages/llm/token-meter\n4: packages/skill/skill-filesystem\n---random pick---\n[stderr]\nbash: shuf: command not found\n[exit code: 127]"}],"isError":false}],"role":"user","id":"3227ba71-d0f0-4b4e-82e0-b01c6cd1ded1"}},"sourceEventSeqs":[509],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":511,"time":1786073824938,"data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","seq":512,"time":1786073824947,"data":{"turn":1,"step":4}}
|
||||
{"type":"assistant/chunk","seq":513,"time":1786073826168,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
@@ -175,9 +175,9 @@
|
||||
{"type":"user/message","seq":1749,"time":1786073844598,"data":{"content":[{"type":"text","text":"<goal_complete>\nObjective: \"做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的\"\nThe goal is marked complete and this autonomous run is ending. Write the closing message to the user now: state the outcome, summarize what was done and how it was verified, and point to the concrete results (files, commits, or other artifacts). Report only what earlier rounds and tool results in this session actually establish; when a detail is not in the session, say so instead of inventing it. Note anything the user should review or do next. Address the user directly. Do not call any more tools in this run; further work waits for the user's next instruction.\n</goal_complete>"}],"source":{"kind":"plugin","plugin":"tool-goal","form":"notice","summary":"complete: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的"},"role":"user","id":"34261fc8-220c-40c0-819d-c9c0332bf965"},"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":1750,"time":1786073845862,"data":{"turn":2,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"text-chunks","seq0":1751,"time0":1786073845863,"data":{"turn":2,"step":6,"index":0,"dt":[22,1,0,0,22,1,0,0,0,22,0,0,0,0,0,24,0,25,0,0,0,22,1,0,19,1,0,0,23,0,0,27,0,20,0,0,25,0,1,0,0,0,21,0,1,0,0,23,0,0,0,0,0,24,0,0,0,0,0,0,24,0,0,0,0,0,20,0,1,0,0,27,0,0,0,1,20,0,0,1,0,24,0,1,0,0,0,19,0,0,0,0,0,24,0,0,0,0,0,24,1,0,0,0,0,20,0,0,0,0,24,0,0,0,0,24,0,0,1,0,0,23,0,0,0,0,0,23,1,0,0,0,0,22,0,0,1,0,0,22,1,0,0,0,24,1,0,0,0,0,26,0,0,0,1,21,0,0,0,0,24,0,0,0,0,0,26,0,0,0,0,0,20,20,24,23,1,24,0,22,0,45,3,1,0,25,1,23,0,0,0,0,25,0,0,1,17,0,23,26,1,0,0,23,0,0,23,0,22,28,1,0,0,0,0,23,0,0,0,0,1,23,0,1,0,0,0,21,0,1,0,22,0,0,0,23,0,0,24,22,25,21,38,1,0,10,24,27,1,0,0,26,0,0,0,1,0,19,21,27,0,0,29,0,0,0,0,0,23,20,0,21,25,1,54,0,0,30,0,0,0,0,22,24,1,0,25,0,1,19,24,0,24,1,0,23,1,22,0,0,22,1,25,21,0,22,23,28,0,23,1,0,0,0,22,0,0,0,0,0,24,0,1,0,0,0,18,24,26,1,0,20,23,24,1,26,0,1,0,39],"texts":["两个"," turn"," ","都","完成了"," ✅","\n\n","**","总结",":","**\n\n","-"," **","Turn"," ","1","**"," —"," ","随机","选中"," `","pack","ages","/","context","/s","ession","-reference","`",",","输","出了","文件","结构",":\n"," "," ```\n"," "," session","-reference","/\n"," "," ├","──"," READ","ME",".md","\n"," "," ├","──"," package",".json","\n"," "," ├","──"," src","/\n"," "," │"," "," ├","──"," index",".ts","\n"," "," │"," "," └","──"," uri",".ts","\n"," "," └","──"," tests","/\n"," "," └","──"," session","-reference",".s","pec",".ts","\n"," "," ```\n","-"," **","Turn"," ","2","**"," —"," ","随机","选中"," `","pack","ages","/","ll","m","/t","oken","-meter","`",",","输","出了","文件","结构",":\n"," "," ```\n"," "," token","-meter","/\n"," "," ├","──"," READ","ME",".md","\n"," "," ├","──"," package",".json","\n"," "," ├","──"," src","/\n"," "," │"," "," └","──"," index",".ts","\n"," "," └","──"," tests","/\n"," "," └","──"," token","-meter",".s","pec",".ts","\n"," "," ```\n\n","**","验证","方式",":","**"," ","两个","包的","随机","选择","都","通过"," `","python","3"," random",".choice","`"," ","从","工作","区的"," `","pack","ages","/","`"," ","目录","下","实际","枚举","的"," ","4"," ","个","候选","包","中","抽取","(","`","ui","-con","vers","ation","`","、","`","session","-reference","`","、","`","token","-meter","`","、","`","skill","-local","`","),","文件","结构","则","用"," `","find","`"," ","实际","列出","文件","后","整理","输出",",","均已","排除"," `","node","_modules","`"," ","和"," `.","git","`","。","第二次","随机","抽","到了","与"," turn"," ","1"," ","相同的","包",",","为避免","重复","换","抽","了","另一个","。\n\n","**","说明",":","**"," ","本轮","没有","产生","任何","文件","修改","或","提交","——","任务","本身就是","\"","输出","文件","结构","\",","所有","产物","都在","上面的","消息","文本","里","。\n\n","如果你","还想","看","剩下","两个","包","(","`","ui","-con","vers","ation","`","、","`","skill","-local","`",")","的结构",",","或者其他","格式","的输出","(","比如","带","文件","大小的","树","形","图","),","随时","告诉我","。"]}}
|
||||
{"type":"assistant/chunk","seq":2092,"time":1786073848306,"data":{"turn":2,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"两个 turn 都完成了 ✅\n\n**总结:**\n\n- **Turn 1** — 随机选中 `packages/context/session-reference`,输出了文件结构:\n ```\n session-reference/\n ├── README.md\n ├── package.json\n ├── src/\n │ ├── index.ts\n │ └── uri.ts\n └── tests/\n └── session-reference.spec.ts\n ```\n- **Turn 2** — 随机选中 `packages/llm/token-meter`,输出了文件结构:\n ```\n token-meter/\n ├── README.md\n ├── package.json\n ├── src/\n │ └── index.ts\n └── tests/\n └── token-meter.spec.ts\n ```\n\n**验证方式:** 两个包的随机选择都通过 `python3 random.choice` 从工作区的 `packages/` 目录下实际枚举的 4 个候选包中抽取(`ui-conversation`、`session-reference`、`token-meter`、`skill-local`),文件结构则用 `find` 实际列出文件后整理输出,均已排除 `node_modules` 和 `.git`。第二次随机抽到了与 turn 1 相同的包,为避免重复换抽了另一个。\n\n**说明:** 本轮没有产生任何文件修改或提交——任务本身就是\"输出文件结构\",所有产物都在上面的消息文本里。\n\n如果你还想看剩下两个包(`ui-conversation`、`skill-local`)的结构,或者其他格式的输出(比如带文件大小的树形图),随时告诉我。"}}}}
|
||||
{"type":"assistant/chunk","seq":2092,"time":1786073848306,"data":{"turn":2,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"两个 turn 都完成了 ✅\n\n**总结:**\n\n- **Turn 1** — 随机选中 `packages/context/session-reference`,输出了文件结构:\n ```\n session-reference/\n ├── README.md\n ├── package.json\n ├── src/\n │ ├── index.ts\n │ └── uri.ts\n └── tests/\n └── session-reference.spec.ts\n ```\n- **Turn 2** — 随机选中 `packages/llm/token-meter`,输出了文件结构:\n ```\n token-meter/\n ├── README.md\n ├── package.json\n ├── src/\n │ └── index.ts\n └── tests/\n └── token-meter.spec.ts\n ```\n\n**验证方式:** 两个包的随机选择都通过 `python3 random.choice` 从工作区的 `packages/` 目录下实际枚举的 4 个候选包中抽取(`ui-conversation`、`session-reference`、`token-meter`、`skill-filesystem`),文件结构则用 `find` 实际列出文件后整理输出,均已排除 `node_modules` 和 `.git`。第二次随机抽到了与 turn 1 相同的包,为避免重复换抽了另一个。\n\n**说明:** 本轮没有产生任何文件修改或提交——任务本身就是\"输出文件结构\",所有产物都在上面的消息文本里。\n\n如果你还想看剩下两个包(`ui-conversation`、`skill-filesystem`)的结构,或者其他格式的输出(比如带文件大小的树形图),随时告诉我。"}}}}
|
||||
{"type":"assistant/chunk","seq":2093,"time":1786073848307,"data":{"turn":2,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":311,"outputTokens":348,"cacheReadTokens":10624,"reasoningTokens":0}}}}
|
||||
{"type":"assistant/chunk","seq":2094,"time":1786073848307,"data":{"turn":2,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":2095,"time":1786073848307,"data":{"turn":2,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"两个 turn 都完成了 ✅\n\n**总结:**\n\n- **Turn 1** — 随机选中 `packages/context/session-reference`,输出了文件结构:\n ```\n session-reference/\n ├── README.md\n ├── package.json\n ├── src/\n │ ├── index.ts\n │ └── uri.ts\n └── tests/\n └── session-reference.spec.ts\n ```\n- **Turn 2** — 随机选中 `packages/llm/token-meter`,输出了文件结构:\n ```\n token-meter/\n ├── README.md\n ├── package.json\n ├── src/\n │ └── index.ts\n └── tests/\n └── token-meter.spec.ts\n ```\n\n**验证方式:** 两个包的随机选择都通过 `python3 random.choice` 从工作区的 `packages/` 目录下实际枚举的 4 个候选包中抽取(`ui-conversation`、`session-reference`、`token-meter`、`skill-local`),文件结构则用 `find` 实际列出文件后整理输出,均已排除 `node_modules` 和 `.git`。第二次随机抽到了与 turn 1 相同的包,为避免重复换抽了另一个。\n\n**说明:** 本轮没有产生任何文件修改或提交——任务本身就是\"输出文件结构\",所有产物都在上面的消息文本里。\n\n如果你还想看剩下两个包(`ui-conversation`、`skill-local`)的结构,或者其他格式的输出(比如带文件大小的树形图),随时告诉我。"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5a1daadd-6f23-49ea-980b-6f96f67e3f52"},"usage":{"inputTokens":311,"outputTokens":348,"cacheReadTokens":10624,"reasoningTokens":0}},"sourceEventSeqs":[1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090,2091,2092,2093,2094],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":2095,"time":1786073848307,"data":{"turn":2,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"两个 turn 都完成了 ✅\n\n**总结:**\n\n- **Turn 1** — 随机选中 `packages/context/session-reference`,输出了文件结构:\n ```\n session-reference/\n ├── README.md\n ├── package.json\n ├── src/\n │ ├── index.ts\n │ └── uri.ts\n └── tests/\n └── session-reference.spec.ts\n ```\n- **Turn 2** — 随机选中 `packages/llm/token-meter`,输出了文件结构:\n ```\n token-meter/\n ├── README.md\n ├── package.json\n ├── src/\n │ └── index.ts\n └── tests/\n └── token-meter.spec.ts\n ```\n\n**验证方式:** 两个包的随机选择都通过 `python3 random.choice` 从工作区的 `packages/` 目录下实际枚举的 4 个候选包中抽取(`ui-conversation`、`session-reference`、`token-meter`、`skill-filesystem`),文件结构则用 `find` 实际列出文件后整理输出,均已排除 `node_modules` 和 `.git`。第二次随机抽到了与 turn 1 相同的包,为避免重复换抽了另一个。\n\n**说明:** 本轮没有产生任何文件修改或提交——任务本身就是\"输出文件结构\",所有产物都在上面的消息文本里。\n\n如果你还想看剩下两个包(`ui-conversation`、`skill-filesystem`)的结构,或者其他格式的输出(比如带文件大小的树形图),随时告诉我。"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"5a1daadd-6f23-49ea-980b-6f96f67e3f52"},"usage":{"inputTokens":311,"outputTokens":348,"cacheReadTokens":10624,"reasoningTokens":0}},"sourceEventSeqs":[1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090,2091,2092,2093,2094],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":2096,"time":1786073848308,"data":{"turn":2,"step":6}}
|
||||
{"type":"turn/end","seq":2097,"time":1786073848308,"data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "workspace" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -78,6 +81,10 @@
|
||||
- paragraph: 这是一个很典型的轻量 TypeScript 包结构:入口 + 实现 + 测试。这一轮到此结束,等系统开启下一个 turn。
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
@@ -168,7 +175,7 @@
|
||||
- text: 、
|
||||
- code: token-meter
|
||||
- text: 、
|
||||
- code: skill-local
|
||||
- code: skill-filesystem
|
||||
- text: ),文件结构则用
|
||||
- code: find
|
||||
- text: 实际列出文件后整理输出,均已排除
|
||||
@@ -183,10 +190,14 @@
|
||||
- text: 如果你还想看剩下两个包(
|
||||
- code: ui-conversation
|
||||
- text: 、
|
||||
- code: skill-local
|
||||
- code: skill-filesystem
|
||||
- text: )的结构,或者其他格式的输出(比如带文件大小的树形图),随时告诉我。
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- tooltip "Branch into a new conversation"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
- listbox "Trigger suggestions":
|
||||
- text: Commands
|
||||
- option "compact Compact older conversation history" [selected]
|
||||
- option "export Download this Session log as a ZIP archive"
|
||||
- option "feedback record feedback about this session"
|
||||
- option "goal set or view the goal for a long-running task"
|
||||
- option "permission Switch the permission preset (sandbox mode + approval policy)"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "Reply with the single word" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -20,6 +23,10 @@
|
||||
- paragraph: LIGHTHOUSE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -28,4 +31,4 @@
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps Input 0 tok · Output 0 tok
|
||||
- text: 1 turns · 1 steps
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -24,3 +27,4 @@
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -22,6 +25,10 @@
|
||||
- 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 "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "CJK strong emphasis" [disabled]
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -35,6 +38,10 @@
|
||||
- paragraph: CJK_STRONG_DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
@@ -46,4 +53,4 @@
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok
|
||||
- text: 1 turns · 1 steps LLM {{duration}}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Markdown image policy" [disabled]
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -14,6 +17,10 @@
|
||||
- paragraph: REMOTE_IMAGE_DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
@@ -25,4 +32,4 @@
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok
|
||||
- text: 1 turns · 1 steps LLM {{duration}}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Inline code links" [disabled]
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -26,6 +29,10 @@
|
||||
- paragraph: INLINE_CODE_LINK_DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
@@ -37,4 +44,4 @@
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok
|
||||
- text: 1 turns · 1 steps LLM {{duration}}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Math rendering" [disabled]
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -30,6 +33,10 @@
|
||||
- paragraph: MATH_RENDERING_DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
@@ -41,4 +48,4 @@
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok
|
||||
- text: 1 turns · 1 steps LLM {{duration}}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
dot=warning
|
||||
title=Output token limit reached
|
||||
hint=The reply was cut off; earlier output is preserved in the conversation. Send "continue" to let the model resume.
|
||||
@@ -1,6 +1,9 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the read tool twice" [disabled]
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -15,6 +18,10 @@
|
||||
- paragraph: I will read both files before answering.
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation" [disabled]:
|
||||
- img
|
||||
- text: Available only on the last message of a completed turn 7/25 {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
@@ -38,6 +45,10 @@
|
||||
- paragraph: DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}}
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
@@ -47,7 +44,6 @@
|
||||
- option "moonshotai-cn"
|
||||
- option "nvidia"
|
||||
- option "openai"
|
||||
- option "openai-codex"
|
||||
- option "opencode"
|
||||
- option "opencode-go"
|
||||
- option "openrouter"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
- button "Use actual duration": Duration
|
||||
- button "Collapse turns": Turns
|
||||
- button "Collapse calls": Calls
|
||||
- button "Export session log": Export
|
||||
- img
|
||||
- searchbox "Search trajectory"
|
||||
- region "Trajectory timeline":
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
- region "内测声明":
|
||||
- heading "内测声明" [level=2]
|
||||
- paragraph: 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完善,体验难免有些粗糙。
|
||||
- blockquote: “如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。
|
||||
- paragraph:
|
||||
- text: 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session Log;如需关闭,可以设置环境变量 DSH_TELEMETRY_DISABLED=1。另外,
|
||||
- strong: 如果您有任何反馈与建议,请在企业微信群中留言告诉我们
|
||||
- text: 。每一条反馈,都会帮助我们把它打磨得更好。
|
||||
- button "继续"
|
||||
@@ -13,9 +13,6 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
@@ -51,7 +48,6 @@
|
||||
- option "moonshotai-cn"
|
||||
- option "nvidia"
|
||||
- option "openai"
|
||||
- option "openai-codex"
|
||||
- option "opencode"
|
||||
- option "opencode-go"
|
||||
- option "openrouter"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,9 @@
|
||||
- 'button "Plan a small change: add" [disabled]'
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -33,6 +36,10 @@
|
||||
- paragraph: DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
|
||||
@@ -13,25 +13,26 @@
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "插件配置":
|
||||
- img
|
||||
- text: 插件配置
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
- text: 关闭
|
||||
- heading "插件配置" [level=2]
|
||||
- paragraph: 配置本部署已安装的插件。
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "展开设置: 终端"':
|
||||
- text: 终端 限制 agent 运行的每一条命令。
|
||||
- img
|
||||
- listitem:
|
||||
- 'button "展开设置: Agent 循环"':
|
||||
- text: Agent 循环 Agent 如何派发工具调用。
|
||||
- img
|
||||
- listitem:
|
||||
- 'button "展开设置: 网页搜索"':
|
||||
- text: 网页搜索 DeepSeek 搜索提供方。
|
||||
- img
|
||||
- heading "插件" [level=2]
|
||||
- paragraph: 配置和查看本部署已安装的插件。
|
||||
- tablist "插件视图":
|
||||
- tab "插件配置" [selected]
|
||||
- tab "插件列表"
|
||||
- tabpanel "插件配置":
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "展开设置: 终端"':
|
||||
- text: 终端 限制 agent 运行的每一条命令。
|
||||
- img
|
||||
- listitem:
|
||||
- 'button "展开设置: Agent 循环"':
|
||||
- text: Agent 循环 Agent 如何派发工具调用。
|
||||
- img
|
||||
- listitem:
|
||||
- 'button "展开设置: 网页搜索"':
|
||||
- text: 网页搜索 DeepSeek 搜索提供方。
|
||||
- img
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -28,6 +31,10 @@
|
||||
- paragraph: DONE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Good response":
|
||||
- img
|
||||
- button "Bad response":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "Session log":
|
||||
- text: Session log
|
||||
- img
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user