dsh-loop-engine 1.0.0-rc3 → 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,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
- `dsh plugin add` installs the package into the web profile and registers it as
36
- a bundle layer — the package declares `dsh.bundle.patch`, so no manual edit of
37
- `cordis.patch.yml` is needed. It is published on npm as `dsh-loop-engine`.
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. Open **Settings → Loop engine**.
75
- 2. Choose an engine:
76
- - **In-process** (default) — the built-in loop driver;
77
- - **Claude Code CLI** — the Claude Code driver;
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
- ### Codex engine details
86
-
87
- The driver bypasses the `@openai/codex-sdk` (which only exposes whole-item
88
- output) and spawns `codex app-server` as a child process, speaking JSON-RPC
89
- over stdio. The app-server streams **token-level deltas** via
90
- `item/agentMessage/delta` and `item/reasoning/summaryTextDelta`, so thinking
91
- and replies paint progressively in the session — like the Claude Code engine.
92
-
93
- - **Streaming.** Reasoning deltas and agent-message deltas are forwarded live
94
- as `assistant/chunk` events; durable messages land at the correct step
95
- boundaries with usage attached on turn completion.
96
- - **Skills.** The Codex engine registers a skill provider that surfaces
97
- `AGENTS.md` (project root and `~/.codex/AGENTS.md`) through the same dsh
98
- skill-injection seam as Claude skills.
99
- - **No interactive tool approval.** Permissions are the declarative
100
- `sandboxMode` + `approvalPolicy` pair resolved per query from the session's
101
- permission knobs (or pinned via the plugin's `sandboxMode`/`approvalPolicy`
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
- `dsh plugin add` 会把包装进 web profile,并注册为 bundle 层——包声明了 `dsh.bundle.patch`,所以无需手动改 `cordis.patch.yml`。包已发布到 npm,名称 `dsh-loop-engine`。
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. 打开 **Settings → Loop engine**。
50
- 2. 选择引擎:
51
- - **In-process**(默认)——内置循环驱动;
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
- 驱动 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
+ ### 引擎说明
71
30
 
72
- - **流式。** `message_update` 增量(`text_delta` / `thinking_delta`)实时转发为 `assistant/chunk` 事件;工具调用与结果落为 `tool/call` + `tool/result`,用量在 `turn_end` 附加。
73
- - **由 dsh subprocess 接口做沙箱化。** Pi **没有权限系统**("以用户权限运行"),因此驱动把整个 `pi` 子进程经由 dsh subprocess 服务转发,并把 `--tools` 裁剪到已解析的沙箱立场——`read-only`(默认)、`workspace-write` 或 `danger-full-access`(不裁剪)。`ask` 策略退化为只读拒绝,因为 Pi 没有交互式审批回调。
74
- - **技能。** Pi 引擎注册了一个技能提供者,把 `AGENTS.md`(项目根与 `~/.pi/AGENTS.md`)通过与 Claude 技能相同的 dsh 技能注入接口暴露出来。
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.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