dsh-loop-engine 1.0.0-rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -0
- package/README.zh.md +93 -0
- package/lib/client.js +403 -0
- package/lib/index.js +4310 -0
- package/lib/invariant.js +83 -0
- package/lib/types/client/LoopEngineBadge.d.ts +34 -0
- package/lib/types/client/LoopEngineSection.d.ts +34 -0
- package/lib/types/client/index.d.ts +28 -0
- package/lib/types/client/locales.d.ts +40 -0
- package/lib/types/client/store.d.ts +45 -0
- package/lib/types/commands.d.ts +32 -0
- package/lib/types/driver-core/ownership.d.ts +41 -0
- package/lib/types/driver-core/permission-knobs.d.ts +26 -0
- package/lib/types/driver-core/prompt.d.ts +23 -0
- package/lib/types/driver-core/skill-inject.d.ts +59 -0
- package/lib/types/engine-claude/agent.d.ts +102 -0
- package/lib/types/engine-claude/loop.d.ts +89 -0
- package/lib/types/engine-claude/mapping.d.ts +83 -0
- package/lib/types/engine-claude/permission.d.ts +41 -0
- package/lib/types/engine-claude/process.d.ts +59 -0
- package/lib/types/engine-claude/sdk.d.ts +57 -0
- package/lib/types/engine-claude/types.d.ts +18 -0
- package/lib/types/engine-codex/agent.d.ts +109 -0
- package/lib/types/engine-codex/appserver/client.d.ts +49 -0
- package/lib/types/engine-codex/appserver/mapping.d.ts +67 -0
- package/lib/types/engine-codex/appserver/thread.d.ts +66 -0
- package/lib/types/engine-codex/appserver/types.d.ts +215 -0
- package/lib/types/engine-codex/loop.d.ts +92 -0
- package/lib/types/engine-codex/permission.d.ts +32 -0
- package/lib/types/engine-codex/skills.d.ts +26 -0
- package/lib/types/engine-codex/types.d.ts +19 -0
- package/lib/types/engine-pi/agent.d.ts +125 -0
- package/lib/types/engine-pi/loop.d.ts +96 -0
- package/lib/types/engine-pi/permission.d.ts +43 -0
- package/lib/types/engine-pi/rpc/client.d.ts +105 -0
- package/lib/types/engine-pi/rpc/mapping.d.ts +37 -0
- package/lib/types/engine-pi/rpc/types.d.ts +235 -0
- package/lib/types/engine-pi/skills.d.ts +26 -0
- package/lib/types/engine-pi/types.d.ts +27 -0
- package/lib/types/index.d.ts +96 -0
- package/lib/types/invariant.d.ts +23 -0
- package/lib/types/namespace.d.ts +9 -0
- package/lib/types/patch-manager.d.ts +47 -0
- package/lib/types/settings.d.ts +29 -0
- package/lib/types/skills.d.ts +77 -0
- package/package.json +103 -0
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# dsh-loop-engine
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-loop-engine)
|
|
4
|
+
|
|
5
|
+
Switch the agent loop engine of **dsh web** the same way you switch a model: a
|
|
6
|
+
"Loop engine" dropdown in Settings chooses which driver runs your agents — the
|
|
7
|
+
built-in in-process loop, the Claude Code CLI, the Codex CLI, or the Pi CLI —
|
|
8
|
+
without changing anything in the main repository.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
- **Engine chosen from Settings, not from code.** Pick `in-process` (default),
|
|
13
|
+
`claude-code`, `codex`, or `pi` in the settings page; the choice is stored
|
|
14
|
+
durably and survives profile edits.
|
|
15
|
+
- **Claude Code execution.** The Claude Code CLI drives agents, with token
|
|
16
|
+
streaming forwarded into the session log.
|
|
17
|
+
- **Codex execution.** The driver spawns `codex app-server` and streams its
|
|
18
|
+
token-level deltas into the session log, one turn per step.
|
|
19
|
+
- **Pi execution.** The driver spawns `pi --mode rpc` and streams its
|
|
20
|
+
`message_update` deltas into the session log, one stateless `new_session` +
|
|
21
|
+
`prompt` per step.
|
|
22
|
+
- **Extensible.** Adding an engine is one driver module plus one entry in the
|
|
23
|
+
settings schema.
|
|
24
|
+
- **Zero main-repo changes.** Installed purely as a profile dependency plus one
|
|
25
|
+
composition row.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- dsh `0.1.1-rc.2` (or a build whose peer packages match).
|
|
30
|
+
- For the Claude Code engine: the Claude Code CLI installed and logged in on
|
|
31
|
+
the host.
|
|
32
|
+
- For the Codex engine: authenticated either via `codex login` on the host or a
|
|
33
|
+
`CODEX_API_KEY` environment entry.
|
|
34
|
+
- For the Pi engine: authenticated the way `pi` expects (its own
|
|
35
|
+
`~/.pi/agent/auth.json` or the provider's API-key environment variable such as
|
|
36
|
+
`ANTHROPIC_API_KEY`).
|
|
37
|
+
- When the harness runs from a **source checkout** (e.g. `pnpm dsh` inside the
|
|
38
|
+
`deepseek-harness` repository), the profile must resolve this plugin's
|
|
39
|
+
harness peer packages to the monorepo **sources** via local `file:` shims
|
|
40
|
+
(the profile's `shims/` directory). A deployed install with one published
|
|
41
|
+
`node_modules` needs no shims.
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
1. Build the plugin:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pnpm install && pnpm run build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
2. Add the dependency in `$DSH_HOME/profiles/web/package.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"dsh-loop-engine": "1.0.0-rc2",
|
|
56
|
+
"...keep existing deps"
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The package is published on npm as `dsh-loop-engine`. For local
|
|
61
|
+
development, use a `file:` reference to your checkout instead of the version
|
|
62
|
+
string.
|
|
63
|
+
|
|
64
|
+
3. Append one composition row in `$DSH_HOME/profiles/web/cordis.patch.yml`:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
- insert:
|
|
68
|
+
- id: loop-engine
|
|
69
|
+
name: 'dsh-loop-engine'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
4. Install and restart:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
cd $DSH_HOME/profiles/web && pnpm install
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Restart `dsh web` once so the plugin is picked up.
|
|
79
|
+
|
|
80
|
+
> Switching engines rewrites a small managed block in `cordis.patch.yml`.
|
|
81
|
+
> Everything else you wrote in that file is preserved; only the plugin's own
|
|
82
|
+
> span changes.
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
1. Open **Settings → Loop engine**.
|
|
87
|
+
2. Choose an engine:
|
|
88
|
+
- **In-process** (default) — the built-in loop driver;
|
|
89
|
+
- **Claude Code CLI** — the Claude Code driver;
|
|
90
|
+
- **Codex CLI** — the OpenAI Codex driver;
|
|
91
|
+
- **Pi CLI** — the Pi (earendil-works/pi) driver.
|
|
92
|
+
3. Switching between these applies after restarting `dsh web`. To return
|
|
93
|
+
to the default, pick **In-process** and restart again.
|
|
94
|
+
4. To remove the plugin: delete the `loop-engine` row from
|
|
95
|
+
`cordis.patch.yml`, drop the dependency from `package.json`, then
|
|
96
|
+
`pnpm install` and restart `dsh web`.
|
|
97
|
+
|
|
98
|
+
### Codex engine details
|
|
99
|
+
|
|
100
|
+
The driver bypasses the `@openai/codex-sdk` (which only exposes whole-item
|
|
101
|
+
output) and spawns `codex app-server` as a child process, speaking JSON-RPC
|
|
102
|
+
over stdio. The app-server streams **token-level deltas** via
|
|
103
|
+
`item/agentMessage/delta` and `item/reasoning/summaryTextDelta`, so thinking
|
|
104
|
+
and replies paint progressively in the session — like the Claude Code engine.
|
|
105
|
+
|
|
106
|
+
- **Streaming.** Reasoning deltas and agent-message deltas are forwarded live
|
|
107
|
+
as `assistant/chunk` events; durable messages land at the correct step
|
|
108
|
+
boundaries with usage attached on turn completion.
|
|
109
|
+
- **Skills.** The Codex engine registers a skill provider that surfaces
|
|
110
|
+
`AGENTS.md` (project root and `~/.codex/AGENTS.md`) through the same dsh
|
|
111
|
+
skill-injection seam as Claude skills.
|
|
112
|
+
- **No interactive tool approval.** Permissions are the declarative
|
|
113
|
+
`sandboxMode` + `approvalPolicy` pair resolved per query from the session's
|
|
114
|
+
permission knobs (or pinned via the plugin's `sandboxMode`/`approvalPolicy`
|
|
115
|
+
config). A session `ask` policy maps to `on-request`, whose CLI prompt
|
|
116
|
+
degrades to a denial in the unattended dsh runtime.
|
|
117
|
+
- **No dsh subprocess sandbox integration.** The driver spawns `codex
|
|
118
|
+
app-server` itself; the dsh subprocess service (and its sandbox) does not
|
|
119
|
+
wrap it.
|
|
120
|
+
- **No engine-specific slash commands** are registered for Codex in this
|
|
121
|
+
version.
|
|
122
|
+
|
|
123
|
+
### Pi engine details
|
|
124
|
+
|
|
125
|
+
The driver spawns `pi --mode rpc` as a child process and speaks the strict-LF
|
|
126
|
+
(`\n`) JSONL protocol over stdio — never a generic line reader, because Pi
|
|
127
|
+
allows Unicode separators like U+2028 inside JSON strings. It runs **one
|
|
128
|
+
stateless session per dsh step**: a fresh `new_session`, then a single `prompt`
|
|
129
|
+
carrying the serialized session history. Like the Codex/Claude drivers, Pi owns
|
|
130
|
+
its own system prompt natively, so the dsh system-prompt assembly is not run —
|
|
131
|
+
the durable session log remains the sole source of model context.
|
|
132
|
+
|
|
133
|
+
- **Streaming.** `message_update` deltas (`text_delta` / `thinking_delta`) are
|
|
134
|
+
forwarded live as `assistant/chunk` events; tool calls and results land as
|
|
135
|
+
`tool/call` + `tool/result`, and usage is attached on `turn_end`.
|
|
136
|
+
- **Sandboxed by the dsh subprocess seam.** Pi has **no permission system** ("it
|
|
137
|
+
runs with the permissions of the user"), so the driver routes the whole `pi`
|
|
138
|
+
child through the dsh subprocess service and prunes `--tools` to the resolved
|
|
139
|
+
sandbox stance — `read-only` (default), `workspace-write`, or
|
|
140
|
+
`danger-full-access` (no pruning). An `ask` policy degrades to a read-only
|
|
141
|
+
denial because Pi has no interactive approval callback.
|
|
142
|
+
- **Skills.** The Pi engine registers a skill provider that surfaces
|
|
143
|
+
`AGENTS.md` (project root and `~/.pi/AGENTS.md`) through the same dsh
|
|
144
|
+
skill-injection seam.
|
|
145
|
+
- **Model/provider.** Provider and model are passed to the child via
|
|
146
|
+
`--provider` / `--model` (a pinned `thinkingLevel` is appended as
|
|
147
|
+
`:<level>`); when omitted, Pi's native settings own the model.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# dsh-loop-engine
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-loop-engine)
|
|
4
|
+
|
|
5
|
+
像切换模型一样切换 **dsh web** 的 agent 循环引擎:设置页的「Loop engine」下拉选择运行 agent 的驱动——内置 in-process 循环、Claude Code CLI、Codex CLI,或 Pi CLI——**无需改动主仓库**。
|
|
6
|
+
|
|
7
|
+
## 能做什么
|
|
8
|
+
|
|
9
|
+
- **引擎由设置决定,而非改代码。** 在设置页选 `in-process`(默认)、`claude-code`、`codex` 或 `pi`,选择持久保存,编辑 profile 也不丢失。
|
|
10
|
+
- **Claude Code 执行。** 由 Claude Code CLI 驱动 agent,并把 token 流式转发进会话日志。
|
|
11
|
+
- **Codex 执行。** 驱动 spawn `codex app-server` 并把其 token 级增量流式转发进会话日志,每一步一个 turn。
|
|
12
|
+
- **Pi 执行。** 驱动 spawn `pi --mode rpc` 并把其 `message_update` 增量流式转发进会话日志,每一步一个无状态 `new_session` + `prompt`。
|
|
13
|
+
- **可扩展。** 新增引擎只需加一个驱动模块 + 设置 schema 里加一个枚举值。
|
|
14
|
+
- **零主仓改动。** 仅作为 profile 依赖 + 一行组合配置安装。
|
|
15
|
+
|
|
16
|
+
## 环境要求
|
|
17
|
+
|
|
18
|
+
- dsh `0.1.1-rc.2`(或 peer 包版本匹配的构建)。
|
|
19
|
+
- 使用 Claude Code 引擎时需要本机已安装并登录 Claude Code CLI。
|
|
20
|
+
- 使用 Codex 引擎时需要完成认证:本机执行过 `codex login`,或配置 `CODEX_API_KEY` 环境变量。
|
|
21
|
+
- 使用 Pi 引擎时需要以 `pi` 要求的方式完成认证(其自身的 `~/.pi/agent/auth.json`,或提供方的 API-key 环境变量,如 `ANTHROPIC_API_KEY`)。
|
|
22
|
+
- 当 harness 以**源码方式**运行(如在 `deepseek-harness` 仓库内执行 `pnpm dsh`)时,profile 需要把本插件的 harness peer 包解析到主仓 **源码**,通过 profile 的 `shims/` 目录里的本地 `file:` 垫片实现;正式部署(单一发布版 `node_modules`)则无需垫片。
|
|
23
|
+
|
|
24
|
+
## 配置方法
|
|
25
|
+
|
|
26
|
+
1. 构建插件:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pnpm install && pnpm run build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. 在 `$DSH_HOME/profiles/web/package.json` 添加依赖:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"dsh-loop-engine": "1.0.0-rc2",
|
|
37
|
+
"...保留其余依赖"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
包已发布到 npm,名称 `dsh-loop-engine`。本地开发时改用指向你本地代码副本的 `file:` 引用。
|
|
42
|
+
|
|
43
|
+
3. 在 `$DSH_HOME/profiles/web/cordis.patch.yml` 追加一行组合:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
- insert:
|
|
47
|
+
- id: loop-engine
|
|
48
|
+
name: 'dsh-loop-engine'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
4. 安装并重启:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
cd $DSH_HOME/profiles/web && pnpm install
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
重启一次 `dsh web` 使插件生效。
|
|
58
|
+
|
|
59
|
+
> 切换引擎会重写 `cordis.patch.yml` 中一小段受管理的内容,文件里你写的其它部分都会保留,只改动插件自己的区间。
|
|
60
|
+
|
|
61
|
+
## 使用方法
|
|
62
|
+
|
|
63
|
+
1. 打开 **Settings → Loop engine**。
|
|
64
|
+
2. 选择引擎:
|
|
65
|
+
- **In-process**(默认)——内置循环驱动;
|
|
66
|
+
- **Claude Code CLI** —— Claude Code 驱动;
|
|
67
|
+
- **Codex CLI** —— OpenAI Codex 驱动;
|
|
68
|
+
- **Pi CLI** —— Pi(earendil-works/pi)驱动。
|
|
69
|
+
3. 引擎之间切换在**重启 `dsh web` 后生效**;切回默认时选择 **In-process** 并再次重启即可。
|
|
70
|
+
4. 卸载插件:从 `cordis.patch.yml` 删除 `loop-engine` 行,并从 `package.json` 移除依赖,然后 `pnpm install` 并重启 `dsh web`。
|
|
71
|
+
|
|
72
|
+
### Codex 引擎的实现细节
|
|
73
|
+
|
|
74
|
+
驱动绕过 `@openai/codex-sdk`(它只输出整条 item),改为 spawn `codex app-server` 子进程,通过 stdio 走 JSON-RPC。app-server 会流式输出 **token 级增量**(`item/agentMessage/delta` 与 `item/reasoning/summaryTextDelta`),因此思考与回复会在会话里逐步呈现——与 Claude Code 引擎一致。
|
|
75
|
+
|
|
76
|
+
- **流式。** 推理增量与回复增量实时转发为 `assistant/chunk` 事件;持久化消息在正确的 step 边界落盘,turn 结束时附加用量。
|
|
77
|
+
- **技能。** Codex 引擎注册了一个技能提供者,把 `AGENTS.md`(项目根与 `~/.codex/AGENTS.md`)通过与 Claude 技能相同的 dsh 技能注入接口暴露出来。
|
|
78
|
+
- **无交互式工具审批。** 权限是声明式的 `sandboxMode` + `approvalPolicy` 组合,每次查询按会话的权限开关解析(也可通过插件的 `sandboxMode`/`approvalPolicy` 配置固定)。会话的 `ask` 策略映射为 `on-request`,其 CLI 交互提示在无人值守的 dsh 运行时会退化为拒绝。
|
|
79
|
+
- **不接入 dsh subprocess 沙箱。** 驱动自行 spawn `codex app-server`,dsh 的 subprocess 服务(及其沙箱)不会包裹它。
|
|
80
|
+
- **本版本不为 Codex 注册引擎专属斜杠命令。**
|
|
81
|
+
|
|
82
|
+
### Pi 引擎的实现细节
|
|
83
|
+
|
|
84
|
+
驱动 spawn `pi --mode rpc` 子进程,通过 stdio 走 strict-LF(`\n`)JSONL 协议——从不用通用行读取器,因为 Pi 允许在 JSON 字符串里使用 U+2028 等 Unicode 分隔符。它**每个 dsh step 运行一个无状态会话**:先 `new_session`,再发一条携带序列化会话历史的 `prompt`。与 Codex/Claude 驱动一样,Pi 原生拥有自己的 system prompt,因此不会运行 dsh 的 system-prompt 组装——持久化会话日志仍是模型上下文的唯一来源。
|
|
85
|
+
|
|
86
|
+
- **流式。** `message_update` 增量(`text_delta` / `thinking_delta`)实时转发为 `assistant/chunk` 事件;工具调用与结果落为 `tool/call` + `tool/result`,用量在 `turn_end` 附加。
|
|
87
|
+
- **由 dsh subprocess 接口做沙箱化。** Pi **没有权限系统**("以用户权限运行"),因此驱动把整个 `pi` 子进程经由 dsh subprocess 服务转发,并把 `--tools` 裁剪到已解析的沙箱立场——`read-only`(默认)、`workspace-write` 或 `danger-full-access`(不裁剪)。`ask` 策略退化为只读拒绝,因为 Pi 没有交互式审批回调。
|
|
88
|
+
- **技能。** Pi 引擎注册了一个技能提供者,把 `AGENTS.md`(项目根与 `~/.pi/AGENTS.md`)通过与 Claude 技能相同的 dsh 技能注入接口暴露出来。
|
|
89
|
+
- **模型/提供方。** 通过 `--provider` / `--model` 传给子进程(固定的 `thinkingLevel` 会追加为 `:<level>`);省略时由 Pi 原生设置接管模型。
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
var module = { exports: {} }; var exports = module.exports; window.__ModuleLoader__.load({ id: "dsh-loop-engine", factory: (require) => {
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/client/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
apply: () => apply,
|
|
25
|
+
inject: () => inject
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/client/LoopEngineSection.tsx
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
32
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
33
|
+
var ENGINE_OPTIONS = [
|
|
34
|
+
{ value: "in-process", key: "engineInProcess" },
|
|
35
|
+
{ value: "claude-code", key: "engineClaudeCode" },
|
|
36
|
+
{ value: "codex", key: "engineCodex" },
|
|
37
|
+
{ value: "pi", key: "enginePi" }
|
|
38
|
+
];
|
|
39
|
+
function engineLabelKey(engine) {
|
|
40
|
+
switch (engine) {
|
|
41
|
+
case "claude-code":
|
|
42
|
+
return "engineClaudeCode";
|
|
43
|
+
case "codex":
|
|
44
|
+
return "engineCodex";
|
|
45
|
+
case "pi":
|
|
46
|
+
return "enginePi";
|
|
47
|
+
default:
|
|
48
|
+
return "engineInProcess";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
var shell = {
|
|
52
|
+
display: "flex",
|
|
53
|
+
flexDirection: "column",
|
|
54
|
+
gap: 12,
|
|
55
|
+
maxWidth: 720,
|
|
56
|
+
color: "var(--dsw-alias-label-primary)"
|
|
57
|
+
};
|
|
58
|
+
var titleStyle = {
|
|
59
|
+
margin: 0,
|
|
60
|
+
fontSize: 18,
|
|
61
|
+
fontWeight: 600
|
|
62
|
+
};
|
|
63
|
+
var intro = {
|
|
64
|
+
margin: 0,
|
|
65
|
+
fontSize: 13,
|
|
66
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
67
|
+
};
|
|
68
|
+
var trigger = {
|
|
69
|
+
appearance: "none",
|
|
70
|
+
boxSizing: "border-box",
|
|
71
|
+
display: "inline-flex",
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
gap: 8,
|
|
74
|
+
width: "fit-content",
|
|
75
|
+
minWidth: 200,
|
|
76
|
+
padding: "9px 12px",
|
|
77
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
78
|
+
borderRadius: 10,
|
|
79
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
80
|
+
color: "var(--dsw-alias-label-primary)",
|
|
81
|
+
font: "inherit",
|
|
82
|
+
fontSize: 13,
|
|
83
|
+
cursor: "pointer"
|
|
84
|
+
};
|
|
85
|
+
var triggerDisabled = { ...trigger, opacity: 0.5, cursor: "default" };
|
|
86
|
+
var notice = {
|
|
87
|
+
margin: 0,
|
|
88
|
+
fontSize: 12,
|
|
89
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
90
|
+
};
|
|
91
|
+
var error = {
|
|
92
|
+
margin: 0,
|
|
93
|
+
fontSize: 13,
|
|
94
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
95
|
+
};
|
|
96
|
+
var confirmBody = {
|
|
97
|
+
margin: 0,
|
|
98
|
+
fontSize: 13,
|
|
99
|
+
lineHeight: 1.55,
|
|
100
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
101
|
+
};
|
|
102
|
+
function LoopEngineSection(props) {
|
|
103
|
+
const { controller, useSnapshot, t } = props;
|
|
104
|
+
const { status, engine, writable } = useSnapshot((snapshot) => snapshot);
|
|
105
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
106
|
+
const [pending, setPending] = (0, import_react.useState)(null);
|
|
107
|
+
const navId = (0, import_react.useId)();
|
|
108
|
+
const triggerRef = (0, import_react.useRef)(null);
|
|
109
|
+
if (status === "unavailable") {
|
|
110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": navId, style: shell, children: [
|
|
111
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: navId, style: titleStyle, children: t("nav") }),
|
|
112
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: intro, children: t("description") }),
|
|
113
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", style: error, children: t("unavailable") })
|
|
114
|
+
] });
|
|
115
|
+
}
|
|
116
|
+
const disabled = status === "saving" || !writable;
|
|
117
|
+
const label = t(engineLabelKey(engine));
|
|
118
|
+
const onSelect = (next) => {
|
|
119
|
+
setOpen(false);
|
|
120
|
+
const value = next;
|
|
121
|
+
if (value === engine) return;
|
|
122
|
+
setPending(value);
|
|
123
|
+
};
|
|
124
|
+
const confirmSwitch = () => {
|
|
125
|
+
const value = pending;
|
|
126
|
+
setPending(null);
|
|
127
|
+
if (value !== null) {
|
|
128
|
+
void controller.setEngine(value).then((landed) => {
|
|
129
|
+
if (landed) window.location.reload();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const cancelSwitch = () => {
|
|
134
|
+
setPending(null);
|
|
135
|
+
};
|
|
136
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": navId, style: shell, children: [
|
|
137
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: navId, style: titleStyle, children: t("nav") }),
|
|
138
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: intro, children: t("description") }),
|
|
139
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
140
|
+
import_dsh_client_ui_primitives.Menu,
|
|
141
|
+
{
|
|
142
|
+
open,
|
|
143
|
+
onClose: () => {
|
|
144
|
+
setOpen(false);
|
|
145
|
+
},
|
|
146
|
+
items: ENGINE_OPTIONS.map((option) => ({ id: option.value, label: t(option.key) })),
|
|
147
|
+
selectedId: engine,
|
|
148
|
+
onSelect,
|
|
149
|
+
align: "start",
|
|
150
|
+
portal: true,
|
|
151
|
+
getAnchorRect: () => triggerRef.current?.getBoundingClientRect() ?? null,
|
|
152
|
+
anchor: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
153
|
+
"button",
|
|
154
|
+
{
|
|
155
|
+
type: "button",
|
|
156
|
+
ref: triggerRef,
|
|
157
|
+
"aria-haspopup": "menu",
|
|
158
|
+
"aria-expanded": open,
|
|
159
|
+
disabled,
|
|
160
|
+
style: disabled ? triggerDisabled : trigger,
|
|
161
|
+
onClick: () => {
|
|
162
|
+
setOpen(!open);
|
|
163
|
+
},
|
|
164
|
+
children: [
|
|
165
|
+
label,
|
|
166
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 })
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
),
|
|
172
|
+
status === "saving" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: notice, children: t("saving") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: notice, children: t("switchNotice") }),
|
|
173
|
+
engine === "claude-code" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: notice, children: t("claudeModelNotice") }) : null,
|
|
174
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
175
|
+
import_dsh_client_ui_primitives.Modal,
|
|
176
|
+
{
|
|
177
|
+
open: pending !== null,
|
|
178
|
+
onClose: cancelSwitch,
|
|
179
|
+
title: t("confirmTitle"),
|
|
180
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
181
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", onClick: cancelSwitch, children: t("cancelAction") }),
|
|
182
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", onClick: confirmSwitch, children: t("confirmAction") })
|
|
183
|
+
] }),
|
|
184
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: confirmBody, children: t("confirmBody") })
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
] });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/client/LoopEngineBadge.tsx
|
|
191
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
192
|
+
var pill = {
|
|
193
|
+
display: "inline-flex",
|
|
194
|
+
alignItems: "center",
|
|
195
|
+
gap: 4,
|
|
196
|
+
padding: "2px 8px",
|
|
197
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
198
|
+
borderRadius: 999,
|
|
199
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
200
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
201
|
+
fontSize: 12,
|
|
202
|
+
lineHeight: "18px",
|
|
203
|
+
whiteSpace: "nowrap"
|
|
204
|
+
};
|
|
205
|
+
function LoopEngineBadge(props) {
|
|
206
|
+
const { useSnapshot, t } = props;
|
|
207
|
+
const { status, engine } = useSnapshot((state) => state);
|
|
208
|
+
if (status !== "ready") return null;
|
|
209
|
+
const label = t(
|
|
210
|
+
engine === "claude-code" ? "engineClaudeCode" : engine === "codex" ? "engineCodex" : engine === "pi" ? "enginePi" : "engineInProcess"
|
|
211
|
+
);
|
|
212
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: pill, title: t("description"), children: [
|
|
213
|
+
t("nav"),
|
|
214
|
+
" \xB7 ",
|
|
215
|
+
label
|
|
216
|
+
] });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/client/store.ts
|
|
220
|
+
var import_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
221
|
+
function decodeLoopEngine(section) {
|
|
222
|
+
if (typeof section !== "object" || section === null || Array.isArray(section)) return void 0;
|
|
223
|
+
const engine = section.engine;
|
|
224
|
+
return engine === "in-process" || engine === "claude-code" || engine === "codex" || engine === "pi" ? { engine } : void 0;
|
|
225
|
+
}
|
|
226
|
+
var LoopEngineStore = class {
|
|
227
|
+
/**
|
|
228
|
+
* @param scope - the loop engine settings namespace scope.
|
|
229
|
+
*/
|
|
230
|
+
constructor(scope) {
|
|
231
|
+
this.scope = scope;
|
|
232
|
+
}
|
|
233
|
+
scope;
|
|
234
|
+
/** uSES-safe state source shared by the registered settings section. */
|
|
235
|
+
store = (0, import_client.createSnapshotStore)({
|
|
236
|
+
status: "loading",
|
|
237
|
+
engine: "in-process",
|
|
238
|
+
writable: false,
|
|
239
|
+
error: null
|
|
240
|
+
});
|
|
241
|
+
following;
|
|
242
|
+
saving = false;
|
|
243
|
+
/** Begin following the bound scope and publish its current answer. */
|
|
244
|
+
load() {
|
|
245
|
+
this.following ??= this.scope.subscribe(() => {
|
|
246
|
+
this.derive();
|
|
247
|
+
});
|
|
248
|
+
this.derive();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Persist the selected engine. Success is judged against the snapshot the
|
|
252
|
+
* write left behind, so a refused write reports error after its recovery.
|
|
253
|
+
* @param engine - the engine to select for future Agent turns.
|
|
254
|
+
* @returns whether the write landed.
|
|
255
|
+
*/
|
|
256
|
+
async setEngine(engine) {
|
|
257
|
+
this.saving = true;
|
|
258
|
+
this.store.update((state) => {
|
|
259
|
+
state.status = "saving";
|
|
260
|
+
state.error = null;
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
await this.scope.set("engine", engine);
|
|
264
|
+
} finally {
|
|
265
|
+
this.saving = false;
|
|
266
|
+
}
|
|
267
|
+
this.derive();
|
|
268
|
+
const { engine: settled } = this.store.getSnapshot();
|
|
269
|
+
const landed = settled === engine;
|
|
270
|
+
if (!landed) {
|
|
271
|
+
this.store.update((state) => {
|
|
272
|
+
state.status = "unavailable";
|
|
273
|
+
state.error = "the loop engine selection did not persist";
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return landed;
|
|
277
|
+
}
|
|
278
|
+
/** Stop following the scope. */
|
|
279
|
+
dispose() {
|
|
280
|
+
this.following?.();
|
|
281
|
+
this.following = void 0;
|
|
282
|
+
}
|
|
283
|
+
derive() {
|
|
284
|
+
if (this.saving) return;
|
|
285
|
+
const scope = this.scope.getSnapshot();
|
|
286
|
+
switch (scope.status) {
|
|
287
|
+
case "loading":
|
|
288
|
+
this.store.update((state) => {
|
|
289
|
+
state.status = "loading";
|
|
290
|
+
state.error = null;
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
case "unavailable":
|
|
294
|
+
this.store.update((state) => {
|
|
295
|
+
state.status = "unavailable";
|
|
296
|
+
state.engine = "in-process";
|
|
297
|
+
state.error = null;
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
case "ready": {
|
|
301
|
+
const engine = scope.value?.engine ?? "in-process";
|
|
302
|
+
this.store.update((state) => {
|
|
303
|
+
state.status = "ready";
|
|
304
|
+
state.engine = engine;
|
|
305
|
+
state.writable = scope.writable;
|
|
306
|
+
state.error = null;
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
default: {
|
|
311
|
+
const exhaustive = scope.status;
|
|
312
|
+
throw new Error(`unexpected loop engine scope status: ${String(exhaustive)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/client/locales.ts
|
|
319
|
+
var zh = {
|
|
320
|
+
nav: "\u5FAA\u73AF\u5F15\u64CE",
|
|
321
|
+
description: "\u9009\u62E9\u9A71\u52A8\u4F1A\u8BDD\u7684 Agent \u6267\u884C\u5F15\u64CE\uFF0C\u5207\u6362\u5BF9\u540E\u7EED\u4F1A\u8BDD\u751F\u6548\u3002",
|
|
322
|
+
engineInProcess: "\u8FDB\u7A0B\u5185\u5F15\u64CE\uFF08\u9ED8\u8BA4\uFF09",
|
|
323
|
+
engineClaudeCode: "Claude Code CLI",
|
|
324
|
+
engineCodex: "Codex CLI",
|
|
325
|
+
enginePi: "Pi CLI",
|
|
326
|
+
unavailable: "\u5FAA\u73AF\u5F15\u64CE\u8BBE\u7F6E\u4E0D\u53EF\u7528",
|
|
327
|
+
switchNotice: "\u5207\u6362\u5F15\u64CE\u4F1A\u4E2D\u65AD\u5F53\u524D\u4F7F\u7528\u65E7\u5F15\u64CE\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u3002",
|
|
328
|
+
saving: "\u4FDD\u5B58\u4E2D\u2026",
|
|
329
|
+
confirmTitle: "\u5207\u6362\u5FAA\u73AF\u5F15\u64CE\uFF1F",
|
|
330
|
+
confirmBody: "\u5207\u6362\u4F1A\u4E2D\u65AD\u5F53\u524D\u4F7F\u7528\u65E7\u5F15\u64CE\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\uFF0C\u786E\u8BA4\u540E\u9875\u9762\u5C06\u81EA\u52A8\u5237\u65B0\uFF0C\u65B0\u5BF9\u8BDD\u4F7F\u7528\u65B0\u5F15\u64CE\u3002\u786E\u8BA4\u5207\u6362\u5417\uFF1F",
|
|
331
|
+
confirmAction: "\u5207\u6362",
|
|
332
|
+
cancelAction: "\u53D6\u6D88",
|
|
333
|
+
claudeModelNotice: "\u5F53\u524D\u4F7F\u7528 Claude Code \u5F15\u64CE\uFF1A\u5B9E\u9645\u6A21\u578B\u7531 Claude Code \u539F\u751F\u51B3\u5B9A\uFF0C\u9875\u9762\u4E0A\u7684\u6A21\u578B\u9009\u62E9\u4E0D\u751F\u6548\u3002"
|
|
334
|
+
};
|
|
335
|
+
var en = {
|
|
336
|
+
nav: "Loop engine",
|
|
337
|
+
description: "Choose the agent execution engine that drives sessions; the switch applies to new turns.",
|
|
338
|
+
engineInProcess: "In-process engine (default)",
|
|
339
|
+
engineClaudeCode: "Claude Code CLI",
|
|
340
|
+
engineCodex: "Codex CLI",
|
|
341
|
+
enginePi: "Pi CLI",
|
|
342
|
+
unavailable: "Loop engine settings are unavailable",
|
|
343
|
+
switchNotice: "Switching engines interrupts sessions currently running on the previous engine.",
|
|
344
|
+
saving: "Saving\u2026",
|
|
345
|
+
confirmTitle: "Switch loop engine?",
|
|
346
|
+
confirmBody: "Switching interrupts sessions currently running on the previous engine; the page reloads after the switch and new turns use the new engine. Switch now?",
|
|
347
|
+
confirmAction: "Switch",
|
|
348
|
+
cancelAction: "Cancel",
|
|
349
|
+
claudeModelNotice: "Claude Code engine active: the actual model is decided natively by Claude Code; the model selector in this session has no effect."
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// src/namespace.ts
|
|
353
|
+
var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
|
|
354
|
+
|
|
355
|
+
// src/client/index.ts
|
|
356
|
+
var NS = "settings.loop-engine";
|
|
357
|
+
var inject = ["slots", "locale", "settingsScope"];
|
|
358
|
+
function apply(ctx) {
|
|
359
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "loop-engine: copy dictionaries");
|
|
360
|
+
const scope = ctx.settingsScope.bind({
|
|
361
|
+
namespace: LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL,
|
|
362
|
+
decode: decodeLoopEngine
|
|
363
|
+
});
|
|
364
|
+
const controller = new LoopEngineStore(scope);
|
|
365
|
+
ctx.effect(() => {
|
|
366
|
+
controller.load();
|
|
367
|
+
return () => {
|
|
368
|
+
controller.dispose();
|
|
369
|
+
};
|
|
370
|
+
}, "loop-engine: store lifecycle");
|
|
371
|
+
const t = ctx.locale.bind(NS);
|
|
372
|
+
const injected = () => ({
|
|
373
|
+
controller,
|
|
374
|
+
hooks: { snapshot: controller.store },
|
|
375
|
+
t
|
|
376
|
+
});
|
|
377
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
378
|
+
name: "settings.section",
|
|
379
|
+
id: "loop-engine",
|
|
380
|
+
order: 30,
|
|
381
|
+
label: () => t("nav"),
|
|
382
|
+
inject: injected
|
|
383
|
+
}, LoopEngineSection));
|
|
384
|
+
ctx.inject(["slots", "conversation"], (scope2) => {
|
|
385
|
+
const badgeInjected = () => ({
|
|
386
|
+
hooks: { snapshot: controller.store },
|
|
387
|
+
t
|
|
388
|
+
});
|
|
389
|
+
scope2.effect(() => {
|
|
390
|
+
return scope2.slots.register({
|
|
391
|
+
name: "conversation.session.header.actions",
|
|
392
|
+
id: "loop-engine",
|
|
393
|
+
// Static session context precedes interactive actions (agent-preset's
|
|
394
|
+
// label sits at -10, so the engine chip leads the header).
|
|
395
|
+
order: -20,
|
|
396
|
+
locale: NS,
|
|
397
|
+
inject: badgeInjected
|
|
398
|
+
}, LoopEngineBadge);
|
|
399
|
+
}, "loop-engine: session header engine badge");
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return module.exports; } });
|
|
403
|
+
//# sourceMappingURL=client.js.map
|