dsh-loop-engine 1.0.0-rc3 → 1.0.0-rc5
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 +25 -103
- package/README.zh.md +9 -51
- package/lib/client.js +212 -3
- package/lib/index.js +329 -130
- package/lib/invariant.js +22 -6
- package/lib/types/client/LoopEngineComposerSelect.d.ts +40 -0
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/store.d.ts +13 -1
- package/lib/types/commands.d.ts +43 -6
- package/lib/types/driver-core/context-files.d.ts +62 -0
- package/lib/types/engine-codex/skills.d.ts +13 -10
- package/lib/types/engine-pi/skills.d.ts +40 -11
- package/lib/types/patch-manager.d.ts +4 -1
- package/lib/types/settings.d.ts +2 -0
- package/lib/types/skills.d.ts +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,23 +7,6 @@ Switch the agent loop engine of **dsh web** the same way you switch a model: a
|
|
|
7
7
|
built-in in-process loop, the Claude Code CLI, the Codex CLI, or the Pi CLI —
|
|
8
8
|
without changing anything in the main repository.
|
|
9
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
|
-
bundle layer, with no edits to the main repository.
|
|
26
|
-
|
|
27
10
|
## Install
|
|
28
11
|
|
|
29
12
|
```sh
|
|
@@ -32,13 +15,12 @@ dsh plugin --profile web add dsh-loop-engine
|
|
|
32
15
|
|
|
33
16
|
Restart `dsh web`, then open **Settings → Loop engine**.
|
|
34
17
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
18
|
+
> Switching engines rewrites a small managed block in `cordis.patch.yml`.
|
|
19
|
+
> Everything else you wrote in that file is preserved; only the plugin's own
|
|
20
|
+
> span changes.
|
|
38
21
|
|
|
39
22
|
### Requirements
|
|
40
23
|
|
|
41
|
-
- dsh `0.1.1-rc.2` (or a build whose peer packages match).
|
|
42
24
|
- For the Claude Code engine: the Claude Code CLI installed and logged in on
|
|
43
25
|
the host.
|
|
44
26
|
- For the Codex engine: authenticated either via `codex login` on the host or a
|
|
@@ -46,93 +28,33 @@ a bundle layer — the package declares `dsh.bundle.patch`, so no manual edit of
|
|
|
46
28
|
- For the Pi engine: authenticated the way `pi` expects (its own
|
|
47
29
|
`~/.pi/agent/auth.json` or the provider's API-key environment variable such as
|
|
48
30
|
`ANTHROPIC_API_KEY`).
|
|
49
|
-
- When the harness runs from a **source checkout** (e.g. `pnpm dsh` inside the
|
|
50
|
-
`deepseek-harness` repository), the profile must resolve this plugin's
|
|
51
|
-
harness peer packages to the monorepo **sources** via local `file:` shims
|
|
52
|
-
(the profile's `shims/` directory). A deployed install with one published
|
|
53
|
-
`node_modules` needs no shims.
|
|
54
|
-
|
|
55
|
-
> Switching engines rewrites a small managed block in `cordis.patch.yml`.
|
|
56
|
-
> Everything else you wrote in that file is preserved; only the plugin's own
|
|
57
|
-
> span changes.
|
|
58
|
-
|
|
59
|
-
### Local development
|
|
60
|
-
|
|
61
|
-
To work against a checkout instead of the published package, add it with a
|
|
62
|
-
`file:` reference (build it first — the package has no `prepare` script, so
|
|
63
|
-
pnpm will not build it for you):
|
|
64
|
-
|
|
65
|
-
```sh
|
|
66
|
-
cd /path/to/dsh-loop-engine && pnpm install && pnpm run build
|
|
67
|
-
dsh plugin --profile web add file:/path/to/dsh-loop-engine
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Or, from inside the checkout: `dsh plugin --profile web add .`
|
|
71
31
|
|
|
72
32
|
## Usage
|
|
73
33
|
|
|
74
|
-
1.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- **Codex CLI** — the OpenAI Codex driver;
|
|
79
|
-
- **Pi CLI** — the Pi (earendil-works/pi) driver.
|
|
80
|
-
3. Switching between these applies after restarting `dsh web`. To return
|
|
81
|
-
to the default, pick **In-process** and restart again.
|
|
82
|
-
4. To remove the plugin: `dsh plugin --profile web remove dsh-loop-engine`, then
|
|
34
|
+
1. Pick an engine in **Settings → Loop engine** — `in-process` (default),
|
|
35
|
+
`claude-code`, `codex`, or `pi` — then restart `dsh web`.
|
|
36
|
+
2. To return to the default, pick **In-process** and restart again.
|
|
37
|
+
3. To remove the plugin: `dsh plugin --profile web remove dsh-loop-engine`, then
|
|
83
38
|
restart `dsh web`.
|
|
84
39
|
|
|
85
|
-
###
|
|
86
|
-
|
|
87
|
-
The driver
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
`
|
|
101
|
-
|
|
102
|
-
config). A session `ask` policy maps to `on-request`, whose CLI prompt
|
|
103
|
-
degrades to a denial in the unattended dsh runtime.
|
|
104
|
-
- **No dsh subprocess sandbox integration.** The driver spawns `codex
|
|
105
|
-
app-server` itself; the dsh subprocess service (and its sandbox) does not
|
|
106
|
-
wrap it.
|
|
107
|
-
- **No engine-specific slash commands** are registered for Codex in this
|
|
108
|
-
version.
|
|
109
|
-
|
|
110
|
-
### Pi engine details
|
|
111
|
-
|
|
112
|
-
The driver spawns `pi --mode rpc` as a child process and speaks the strict-LF
|
|
113
|
-
(`\n`) JSONL protocol over stdio — never a generic line reader, because Pi
|
|
114
|
-
allows Unicode separators like U+2028 inside JSON strings. It runs **one
|
|
115
|
-
stateless session per dsh step**: a fresh `new_session`, then a single `prompt`
|
|
116
|
-
carrying the serialized session history. Like the Codex/Claude drivers, Pi owns
|
|
117
|
-
its own system prompt natively, so the dsh system-prompt assembly is not run —
|
|
118
|
-
the durable session log remains the sole source of model context.
|
|
119
|
-
|
|
120
|
-
- **Streaming.** `message_update` deltas (`text_delta` / `thinking_delta`) are
|
|
121
|
-
forwarded live as `assistant/chunk` events; tool calls and results land as
|
|
122
|
-
`tool/call` + `tool/result`, and usage is attached on `turn_end`.
|
|
123
|
-
- **Sandboxed by the dsh subprocess seam.** Pi has **no permission system** ("it
|
|
124
|
-
runs with the permissions of the user"), so the driver routes the whole `pi`
|
|
125
|
-
child through the dsh subprocess service and prunes `--tools` to the resolved
|
|
126
|
-
sandbox stance — `read-only` (default), `workspace-write`, or
|
|
127
|
-
`danger-full-access` (no pruning). An `ask` policy degrades to a read-only
|
|
128
|
-
denial because Pi has no interactive approval callback.
|
|
129
|
-
- **Skills.** The Pi engine registers a skill provider that surfaces
|
|
130
|
-
`AGENTS.md` (project root and `~/.pi/AGENTS.md`) through the same dsh
|
|
131
|
-
skill-injection seam.
|
|
132
|
-
- **Model/provider.** Provider and model are passed to the child via
|
|
133
|
-
`--provider` / `--model` (a pinned `thinkingLevel` is appended as
|
|
134
|
-
`:<level>`); when omitted, Pi's native settings own the model.
|
|
40
|
+
### Engine notes
|
|
41
|
+
|
|
42
|
+
- The Claude Code driver runs one SDK query per step; its slash commands are
|
|
43
|
+
bridged into the web menu (built-ins plus user-level `~/.claude/commands/`)
|
|
44
|
+
and forwarded to the engine, which expands them natively. Project-level
|
|
45
|
+
`.claude/commands/` files stay engine-side and also work typed directly.
|
|
46
|
+
- The Codex driver runs `codex app-server` and has no interactive tool
|
|
47
|
+
approval — permissions come from the session's `sandboxMode` +
|
|
48
|
+
`approvalPolicy`. Its `AGENTS.md` instruction files are surfaced through the
|
|
49
|
+
dsh skill-injection seam across every directory from the session cwd up to
|
|
50
|
+
the git root, plus `~/.codex/AGENTS.md`.
|
|
51
|
+
- The Pi driver runs `pi --mode rpc`; Pi has no permission system, so the whole
|
|
52
|
+
child is sandboxed through the dsh subprocess service (default `read-only`).
|
|
53
|
+
Its context files (`AGENTS.md`/`CLAUDE.md` with `AGENTS.override.md`
|
|
54
|
+
preferred, plus the user-level file under the pi config dir) and its
|
|
55
|
+
`skills/` catalogs (`~/.pi/agent/skills/` and `.pi/skills/`) are surfaced
|
|
56
|
+
through the dsh skill-injection seam.
|
|
135
57
|
|
|
136
58
|
## License
|
|
137
59
|
|
|
138
|
-
MIT
|
|
60
|
+
MIT
|
package/README.zh.md
CHANGED
|
@@ -4,15 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
像切换模型一样切换 **dsh web** 的 agent 循环引擎:设置页的「Loop engine」下拉选择运行 agent 的驱动——内置 in-process 循环、Claude Code CLI、Codex CLI,或 Pi CLI——**无需改动主仓库**。
|
|
6
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 依赖 + 一个 bundle 层安装,无需改动主仓库。
|
|
15
|
-
|
|
16
7
|
## 安装
|
|
17
8
|
|
|
18
9
|
```sh
|
|
@@ -21,59 +12,26 @@ dsh plugin --profile web add dsh-loop-engine
|
|
|
21
12
|
|
|
22
13
|
重启 `dsh web`,然后打开 **Settings → Loop engine**。
|
|
23
14
|
|
|
24
|
-
|
|
15
|
+
> 切换引擎会重写 `cordis.patch.yml` 中一小段受管理的内容,文件里你写的其它部分都会保留,只改动插件自己的区间。
|
|
25
16
|
|
|
26
17
|
### 环境要求
|
|
27
18
|
|
|
28
|
-
- dsh `0.1.1-rc.2`(或 peer 包版本匹配的构建)。
|
|
29
19
|
- 使用 Claude Code 引擎时需要本机已安装并登录 Claude Code CLI。
|
|
30
20
|
- 使用 Codex 引擎时需要完成认证:本机执行过 `codex login`,或配置 `CODEX_API_KEY` 环境变量。
|
|
31
21
|
- 使用 Pi 引擎时需要以 `pi` 要求的方式完成认证(其自身的 `~/.pi/agent/auth.json`,或提供方的 API-key 环境变量,如 `ANTHROPIC_API_KEY`)。
|
|
32
|
-
- 当 harness 以**源码方式**运行(如在 `deepseek-harness` 仓库内执行 `pnpm dsh`)时,profile 需要把本插件的 harness peer 包解析到主仓 **源码**,通过 profile 的 `shims/` 目录里的本地 `file:` 垫片实现;正式部署(单一发布版 `node_modules`)则无需垫片。
|
|
33
|
-
|
|
34
|
-
> 切换引擎会重写 `cordis.patch.yml` 中一小段受管理的内容,文件里你写的其它部分都会保留,只改动插件自己的区间。
|
|
35
|
-
|
|
36
|
-
### 本地开发
|
|
37
|
-
|
|
38
|
-
要加载本地 checkout 而非已发布包时,用 `file:` 引用添加(需先构建——本包没有 `prepare` 脚本,pnpm 不会替你构建):
|
|
39
|
-
|
|
40
|
-
```sh
|
|
41
|
-
cd /path/to/dsh-loop-engine && pnpm install && pnpm run build
|
|
42
|
-
dsh plugin --profile web add file:/path/to/dsh-loop-engine
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
或者,在 checkout 目录内直接 `dsh plugin --profile web add .`
|
|
46
22
|
|
|
47
23
|
## 使用方法
|
|
48
24
|
|
|
49
|
-
1.
|
|
50
|
-
2.
|
|
51
|
-
|
|
52
|
-
- **Claude Code CLI** —— Claude Code 驱动;
|
|
53
|
-
- **Codex CLI** —— OpenAI Codex 驱动;
|
|
54
|
-
- **Pi CLI** —— Pi(earendil-works/pi)驱动。
|
|
55
|
-
3. 引擎之间切换在**重启 `dsh web` 后生效**;切回默认时选择 **In-process** 并再次重启即可。
|
|
56
|
-
4. 卸载插件:`dsh plugin --profile web remove dsh-loop-engine`,然后重启 `dsh web`。
|
|
57
|
-
|
|
58
|
-
### Codex 引擎的实现细节
|
|
59
|
-
|
|
60
|
-
驱动绕过 `@openai/codex-sdk`(它只输出整条 item),改为 spawn `codex app-server` 子进程,通过 stdio 走 JSON-RPC。app-server 会流式输出 **token 级增量**(`item/agentMessage/delta` 与 `item/reasoning/summaryTextDelta`),因此思考与回复会在会话里逐步呈现——与 Claude Code 引擎一致。
|
|
61
|
-
|
|
62
|
-
- **流式。** 推理增量与回复增量实时转发为 `assistant/chunk` 事件;持久化消息在正确的 step 边界落盘,turn 结束时附加用量。
|
|
63
|
-
- **技能。** Codex 引擎注册了一个技能提供者,把 `AGENTS.md`(项目根与 `~/.codex/AGENTS.md`)通过与 Claude 技能相同的 dsh 技能注入接口暴露出来。
|
|
64
|
-
- **无交互式工具审批。** 权限是声明式的 `sandboxMode` + `approvalPolicy` 组合,每次查询按会话的权限开关解析(也可通过插件的 `sandboxMode`/`approvalPolicy` 配置固定)。会话的 `ask` 策略映射为 `on-request`,其 CLI 交互提示在无人值守的 dsh 运行时会退化为拒绝。
|
|
65
|
-
- **不接入 dsh subprocess 沙箱。** 驱动自行 spawn `codex app-server`,dsh 的 subprocess 服务(及其沙箱)不会包裹它。
|
|
66
|
-
- **本版本不为 Codex 注册引擎专属斜杠命令。**
|
|
67
|
-
|
|
68
|
-
### Pi 引擎的实现细节
|
|
25
|
+
1. 在 **Settings → Loop engine** 选择引擎——`in-process`(默认)、`claude-code`、`codex` 或 `pi`——然后重启 `dsh web`。
|
|
26
|
+
2. 要切回默认,选 **In-process** 再重启即可。
|
|
27
|
+
3. 卸载插件:`dsh plugin --profile web remove dsh-loop-engine`,然后重启 `dsh web`。
|
|
69
28
|
|
|
70
|
-
|
|
29
|
+
### 引擎说明
|
|
71
30
|
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
- **模型/提供方。** 通过 `--provider` / `--model` 传给子进程(固定的 `thinkingLevel` 会追加为 `:<level>`);省略时由 Pi 原生设置接管模型。
|
|
31
|
+
- Codex 驱动运行 `codex app-server`,没有交互式工具审批——权限来自会话的 `sandboxMode` + `approvalPolicy`。
|
|
32
|
+
- Pi 驱动运行 `pi --mode rpc`;Pi 没有权限系统,所以整个子进程经 dsh subprocess 服务做沙箱化(默认 `read-only`)。
|
|
33
|
+
- 两者都往会话日志流式输出,并通过 dsh 技能注入接口暴露 `AGENTS.md`。
|
|
76
34
|
|
|
77
35
|
## License
|
|
78
36
|
|
|
79
|
-
MIT
|
|
37
|
+
MIT
|
package/lib/client.js
CHANGED
|
@@ -93,6 +93,20 @@ var error = {
|
|
|
93
93
|
fontSize: 13,
|
|
94
94
|
color: "var(--dsw-alias-state-error-primary)"
|
|
95
95
|
};
|
|
96
|
+
var toggleRow = {
|
|
97
|
+
display: "flex",
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
gap: 8,
|
|
100
|
+
fontSize: 13,
|
|
101
|
+
color: "var(--dsw-alias-label-primary)",
|
|
102
|
+
cursor: "pointer"
|
|
103
|
+
};
|
|
104
|
+
var toggleCheckbox = {
|
|
105
|
+
width: 16,
|
|
106
|
+
height: 16,
|
|
107
|
+
accentColor: "var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary))",
|
|
108
|
+
cursor: "pointer"
|
|
109
|
+
};
|
|
96
110
|
var confirmBody = {
|
|
97
111
|
margin: 0,
|
|
98
112
|
fontSize: 13,
|
|
@@ -101,7 +115,7 @@ var confirmBody = {
|
|
|
101
115
|
};
|
|
102
116
|
function LoopEngineSection(props) {
|
|
103
117
|
const { controller, useSnapshot, t } = props;
|
|
104
|
-
const { status, engine, writable } = useSnapshot((snapshot) => snapshot);
|
|
118
|
+
const { status, engine, showInComposer, writable } = useSnapshot((snapshot) => snapshot);
|
|
105
119
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
106
120
|
const [pending, setPending] = (0, import_react.useState)(null);
|
|
107
121
|
const navId = (0, import_react.useId)();
|
|
@@ -171,6 +185,21 @@ function LoopEngineSection(props) {
|
|
|
171
185
|
),
|
|
172
186
|
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
187
|
engine === "claude-code" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: notice, children: t("claudeModelNotice") }) : null,
|
|
188
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: toggleRow, children: [
|
|
189
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
190
|
+
"input",
|
|
191
|
+
{
|
|
192
|
+
type: "checkbox",
|
|
193
|
+
checked: showInComposer,
|
|
194
|
+
disabled,
|
|
195
|
+
style: toggleCheckbox,
|
|
196
|
+
onChange: (event) => {
|
|
197
|
+
void controller.setShowInComposer(event.currentTarget.checked);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
t("showInComposerLabel")
|
|
202
|
+
] }),
|
|
174
203
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
175
204
|
import_dsh_client_ui_primitives.Modal,
|
|
176
205
|
{
|
|
@@ -216,12 +245,140 @@ function LoopEngineBadge(props) {
|
|
|
216
245
|
] });
|
|
217
246
|
}
|
|
218
247
|
|
|
248
|
+
// src/client/LoopEngineComposerSelect.tsx
|
|
249
|
+
var import_react2 = require("react");
|
|
250
|
+
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
251
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
252
|
+
var ENGINE_OPTIONS2 = [
|
|
253
|
+
{ value: "in-process", key: "engineInProcess" },
|
|
254
|
+
{ value: "claude-code", key: "engineClaudeCode" },
|
|
255
|
+
{ value: "codex", key: "engineCodex" },
|
|
256
|
+
{ value: "pi", key: "enginePi" }
|
|
257
|
+
];
|
|
258
|
+
function engineLabelKey2(engine) {
|
|
259
|
+
switch (engine) {
|
|
260
|
+
case "claude-code":
|
|
261
|
+
return "engineClaudeCode";
|
|
262
|
+
case "codex":
|
|
263
|
+
return "engineCodex";
|
|
264
|
+
case "pi":
|
|
265
|
+
return "enginePi";
|
|
266
|
+
default:
|
|
267
|
+
return "engineInProcess";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
var trigger2 = {
|
|
271
|
+
appearance: "none",
|
|
272
|
+
boxSizing: "border-box",
|
|
273
|
+
display: "inline-flex",
|
|
274
|
+
alignItems: "center",
|
|
275
|
+
gap: 6,
|
|
276
|
+
padding: "4px 8px",
|
|
277
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
278
|
+
borderRadius: 10,
|
|
279
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
280
|
+
color: "var(--dsw-alias-label-primary)",
|
|
281
|
+
font: "inherit",
|
|
282
|
+
fontSize: 12,
|
|
283
|
+
lineHeight: "20px",
|
|
284
|
+
whiteSpace: "nowrap",
|
|
285
|
+
cursor: "pointer"
|
|
286
|
+
};
|
|
287
|
+
var triggerDisabled2 = { ...trigger2, opacity: 0.5, cursor: "default" };
|
|
288
|
+
var confirmBody2 = {
|
|
289
|
+
margin: 0,
|
|
290
|
+
fontSize: 13,
|
|
291
|
+
lineHeight: 1.55,
|
|
292
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
293
|
+
};
|
|
294
|
+
function LoopEngineComposerSelect(props) {
|
|
295
|
+
const { controller, useSnapshot, t } = props;
|
|
296
|
+
const { status, engine, showInComposer, writable } = useSnapshot((snapshot) => snapshot);
|
|
297
|
+
const [open, setOpen] = (0, import_react2.useState)(false);
|
|
298
|
+
const [pending, setPending] = (0, import_react2.useState)(null);
|
|
299
|
+
const triggerRef = (0, import_react2.useRef)(null);
|
|
300
|
+
if (status !== "ready" || !showInComposer) return null;
|
|
301
|
+
const disabled = !writable;
|
|
302
|
+
const label = t(engineLabelKey2(engine));
|
|
303
|
+
const title = engine === "claude-code" ? t("claudeModelNotice") : t("description");
|
|
304
|
+
const onSelect = (next) => {
|
|
305
|
+
setOpen(false);
|
|
306
|
+
const value = next;
|
|
307
|
+
if (value === engine) return;
|
|
308
|
+
setPending(value);
|
|
309
|
+
};
|
|
310
|
+
const confirmSwitch = () => {
|
|
311
|
+
const value = pending;
|
|
312
|
+
setPending(null);
|
|
313
|
+
if (value !== null) {
|
|
314
|
+
void controller.setEngine(value).then((landed) => {
|
|
315
|
+
if (landed) window.location.reload();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const cancelSwitch = () => {
|
|
320
|
+
setPending(null);
|
|
321
|
+
};
|
|
322
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
323
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
324
|
+
import_dsh_client_ui_primitives2.Menu,
|
|
325
|
+
{
|
|
326
|
+
open,
|
|
327
|
+
onClose: () => {
|
|
328
|
+
setOpen(false);
|
|
329
|
+
},
|
|
330
|
+
items: ENGINE_OPTIONS2.map((option) => ({ id: option.value, label: t(option.key) })),
|
|
331
|
+
selectedId: engine,
|
|
332
|
+
onSelect,
|
|
333
|
+
align: "start",
|
|
334
|
+
portal: true,
|
|
335
|
+
getAnchorRect: () => triggerRef.current?.getBoundingClientRect() ?? null,
|
|
336
|
+
anchor: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
337
|
+
"button",
|
|
338
|
+
{
|
|
339
|
+
type: "button",
|
|
340
|
+
ref: triggerRef,
|
|
341
|
+
"aria-haspopup": "menu",
|
|
342
|
+
"aria-expanded": open,
|
|
343
|
+
disabled,
|
|
344
|
+
style: disabled ? triggerDisabled2 : trigger2,
|
|
345
|
+
title,
|
|
346
|
+
onClick: () => {
|
|
347
|
+
setOpen(!open);
|
|
348
|
+
},
|
|
349
|
+
children: [
|
|
350
|
+
label,
|
|
351
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.IconChevronDownOutline14, { size: 14 })
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
),
|
|
357
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
358
|
+
import_dsh_client_ui_primitives2.Modal,
|
|
359
|
+
{
|
|
360
|
+
open: pending !== null,
|
|
361
|
+
onClose: cancelSwitch,
|
|
362
|
+
title: t("confirmTitle"),
|
|
363
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
364
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "outline", onClick: cancelSwitch, children: t("cancelAction") }),
|
|
365
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "primary", onClick: confirmSwitch, children: t("confirmAction") })
|
|
366
|
+
] }),
|
|
367
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: confirmBody2, children: t("confirmBody") })
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
] });
|
|
371
|
+
}
|
|
372
|
+
|
|
219
373
|
// src/client/store.ts
|
|
220
374
|
var import_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
221
375
|
function decodeLoopEngine(section) {
|
|
222
376
|
if (typeof section !== "object" || section === null || Array.isArray(section)) return void 0;
|
|
223
|
-
const engine = section
|
|
224
|
-
|
|
377
|
+
const { engine, showInComposer } = section;
|
|
378
|
+
if (engine !== "in-process" && engine !== "claude-code" && engine !== "codex" && engine !== "pi") {
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
return { engine, showInComposer: showInComposer !== false };
|
|
225
382
|
}
|
|
226
383
|
var LoopEngineStore = class {
|
|
227
384
|
/**
|
|
@@ -235,6 +392,7 @@ var LoopEngineStore = class {
|
|
|
235
392
|
store = (0, import_client.createSnapshotStore)({
|
|
236
393
|
status: "loading",
|
|
237
394
|
engine: "in-process",
|
|
395
|
+
showInComposer: true,
|
|
238
396
|
writable: false,
|
|
239
397
|
error: null
|
|
240
398
|
});
|
|
@@ -275,6 +433,36 @@ var LoopEngineStore = class {
|
|
|
275
433
|
}
|
|
276
434
|
return landed;
|
|
277
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Persist whether the composer shows the engine picker. Success is judged
|
|
438
|
+
* against the snapshot the write left behind, so a refused write reports
|
|
439
|
+
* error after its recovery. Unlike {@link setEngine}, landing does not reload
|
|
440
|
+
* the page — the toggle only changes composer visibility.
|
|
441
|
+
* @param show - whether the chat page composer reveals the engine picker.
|
|
442
|
+
* @returns whether the write landed.
|
|
443
|
+
*/
|
|
444
|
+
async setShowInComposer(show) {
|
|
445
|
+
this.saving = true;
|
|
446
|
+
this.store.update((state) => {
|
|
447
|
+
state.status = "saving";
|
|
448
|
+
state.error = null;
|
|
449
|
+
});
|
|
450
|
+
try {
|
|
451
|
+
await this.scope.set("showInComposer", show);
|
|
452
|
+
} finally {
|
|
453
|
+
this.saving = false;
|
|
454
|
+
}
|
|
455
|
+
this.derive();
|
|
456
|
+
const { showInComposer: settled } = this.store.getSnapshot();
|
|
457
|
+
const landed = settled === show;
|
|
458
|
+
if (!landed) {
|
|
459
|
+
this.store.update((state) => {
|
|
460
|
+
state.status = "unavailable";
|
|
461
|
+
state.error = "the loop engine display setting did not persist";
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return landed;
|
|
465
|
+
}
|
|
278
466
|
/** Stop following the scope. */
|
|
279
467
|
dispose() {
|
|
280
468
|
this.following?.();
|
|
@@ -294,14 +482,17 @@ var LoopEngineStore = class {
|
|
|
294
482
|
this.store.update((state) => {
|
|
295
483
|
state.status = "unavailable";
|
|
296
484
|
state.engine = "in-process";
|
|
485
|
+
state.showInComposer = true;
|
|
297
486
|
state.error = null;
|
|
298
487
|
});
|
|
299
488
|
return;
|
|
300
489
|
case "ready": {
|
|
301
490
|
const engine = scope.value?.engine ?? "in-process";
|
|
491
|
+
const showInComposer = scope.value?.showInComposer ?? true;
|
|
302
492
|
this.store.update((state) => {
|
|
303
493
|
state.status = "ready";
|
|
304
494
|
state.engine = engine;
|
|
495
|
+
state.showInComposer = showInComposer;
|
|
305
496
|
state.writable = scope.writable;
|
|
306
497
|
state.error = null;
|
|
307
498
|
});
|
|
@@ -323,6 +514,7 @@ var zh = {
|
|
|
323
514
|
engineClaudeCode: "Claude Code CLI",
|
|
324
515
|
engineCodex: "Codex CLI",
|
|
325
516
|
enginePi: "Pi CLI",
|
|
517
|
+
showInComposerLabel: "\u5728\u5BF9\u8BDD\u9875\u663E\u793A\u5F15\u64CE\u9009\u62E9\u5668",
|
|
326
518
|
unavailable: "\u5FAA\u73AF\u5F15\u64CE\u8BBE\u7F6E\u4E0D\u53EF\u7528",
|
|
327
519
|
switchNotice: "\u5207\u6362\u5F15\u64CE\u4F1A\u4E2D\u65AD\u5F53\u524D\u4F7F\u7528\u65E7\u5F15\u64CE\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u3002",
|
|
328
520
|
saving: "\u4FDD\u5B58\u4E2D\u2026",
|
|
@@ -339,6 +531,7 @@ var en = {
|
|
|
339
531
|
engineClaudeCode: "Claude Code CLI",
|
|
340
532
|
engineCodex: "Codex CLI",
|
|
341
533
|
enginePi: "Pi CLI",
|
|
534
|
+
showInComposerLabel: "Show the engine selector in the chat page",
|
|
342
535
|
unavailable: "Loop engine settings are unavailable",
|
|
343
536
|
switchNotice: "Switching engines interrupts sessions currently running on the previous engine.",
|
|
344
537
|
saving: "Saving\u2026",
|
|
@@ -398,6 +591,22 @@ function apply(ctx) {
|
|
|
398
591
|
}, LoopEngineBadge);
|
|
399
592
|
}, "loop-engine: session header engine badge");
|
|
400
593
|
});
|
|
594
|
+
ctx.inject(["slots", "conversation"], (scope2) => {
|
|
595
|
+
const composerInjected = () => ({
|
|
596
|
+
controller,
|
|
597
|
+
hooks: { snapshot: controller.store },
|
|
598
|
+
t
|
|
599
|
+
});
|
|
600
|
+
scope2.effect(() => {
|
|
601
|
+
return scope2.slots.register({
|
|
602
|
+
name: "conversation.input.right",
|
|
603
|
+
id: "loop-engine",
|
|
604
|
+
order: 0,
|
|
605
|
+
locale: NS,
|
|
606
|
+
inject: composerInjected
|
|
607
|
+
}, LoopEngineComposerSelect);
|
|
608
|
+
}, "loop-engine: composer engine select");
|
|
609
|
+
});
|
|
401
610
|
}
|
|
402
611
|
return module.exports; } });
|
|
403
612
|
//# sourceMappingURL=client.js.map
|