dsh-loop-engine 1.0.0-rc2 → 1.0.0-rc4

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 CHANGED
@@ -7,26 +7,20 @@ 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
10
+ ## Install
11
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.
12
+ ```sh
13
+ dsh plugin --profile web add dsh-loop-engine
14
+ ```
26
15
 
27
- ## Requirements
16
+ Restart `dsh web`, then open **Settings → Loop engine**.
17
+
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.
21
+
22
+ ### Requirements
28
23
 
29
- - dsh `0.1.1-rc.2` (or a build whose peer packages match).
30
24
  - For the Claude Code engine: the Claude Code CLI installed and logged in on
31
25
  the host.
32
26
  - For the Codex engine: authenticated either via `codex login` on the host or a
@@ -34,118 +28,33 @@ without changing anything in the main repository.
34
28
  - For the Pi engine: authenticated the way `pi` expects (its own
35
29
  `~/.pi/agent/auth.json` or the provider's API-key environment variable such as
36
30
  `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
31
 
84
32
  ## Usage
85
33
 
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.
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
38
+ restart `dsh web`.
39
+
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.
148
57
 
149
58
  ## License
150
59
 
151
- MIT
60
+ MIT
package/README.zh.md CHANGED
@@ -4,90 +4,34 @@
4
4
 
5
5
  像切换模型一样切换 **dsh web** 的 agent 循环引擎:设置页的「Loop engine」下拉选择运行 agent 的驱动——内置 in-process 循环、Claude Code CLI、Codex CLI,或 Pi CLI——**无需改动主仓库**。
6
6
 
7
- ## 能做什么
7
+ ## 安装
8
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 依赖 + 一行组合配置安装。
9
+ ```sh
10
+ dsh plugin --profile web add dsh-loop-engine
11
+ ```
15
12
 
16
- ## 环境要求
13
+ 重启 `dsh web`,然后打开 **Settings → Loop engine**。
14
+
15
+ > 切换引擎会重写 `cordis.patch.yml` 中一小段受管理的内容,文件里你写的其它部分都会保留,只改动插件自己的区间。
16
+
17
+ ### 环境要求
17
18
 
18
- - dsh `0.1.1-rc.2`(或 peer 包版本匹配的构建)。
19
19
  - 使用 Claude Code 引擎时需要本机已安装并登录 Claude Code CLI。
20
20
  - 使用 Codex 引擎时需要完成认证:本机执行过 `codex login`,或配置 `CODEX_API_KEY` 环境变量。
21
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
22
 
61
23
  ## 使用方法
62
24
 
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 引擎的实现细节
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`。
83
28
 
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 组装——持久化会话日志仍是模型上下文的唯一来源。
29
+ ### 引擎说明
85
30
 
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 原生设置接管模型。
31
+ - Codex 驱动运行 `codex app-server`,没有交互式工具审批——权限来自会话的 `sandboxMode` + `approvalPolicy`。
32
+ - Pi 驱动运行 `pi --mode rpc`;Pi 没有权限系统,所以整个子进程经 dsh subprocess 服务做沙箱化(默认 `read-only`)。
33
+ - 两者都往会话日志流式输出,并通过 dsh 技能注入接口暴露 `AGENTS.md`。
90
34
 
91
35
  ## License
92
36
 
93
- MIT
37
+ MIT
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: loop-engine
3
+ name: 'dsh-loop-engine'
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.engine;
224
- return engine === "in-process" || engine === "claude-code" || engine === "codex" || engine === "pi" ? { engine } : void 0;
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