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.
Files changed (46) hide show
  1. package/README.md +151 -0
  2. package/README.zh.md +93 -0
  3. package/lib/client.js +403 -0
  4. package/lib/index.js +4310 -0
  5. package/lib/invariant.js +83 -0
  6. package/lib/types/client/LoopEngineBadge.d.ts +34 -0
  7. package/lib/types/client/LoopEngineSection.d.ts +34 -0
  8. package/lib/types/client/index.d.ts +28 -0
  9. package/lib/types/client/locales.d.ts +40 -0
  10. package/lib/types/client/store.d.ts +45 -0
  11. package/lib/types/commands.d.ts +32 -0
  12. package/lib/types/driver-core/ownership.d.ts +41 -0
  13. package/lib/types/driver-core/permission-knobs.d.ts +26 -0
  14. package/lib/types/driver-core/prompt.d.ts +23 -0
  15. package/lib/types/driver-core/skill-inject.d.ts +59 -0
  16. package/lib/types/engine-claude/agent.d.ts +102 -0
  17. package/lib/types/engine-claude/loop.d.ts +89 -0
  18. package/lib/types/engine-claude/mapping.d.ts +83 -0
  19. package/lib/types/engine-claude/permission.d.ts +41 -0
  20. package/lib/types/engine-claude/process.d.ts +59 -0
  21. package/lib/types/engine-claude/sdk.d.ts +57 -0
  22. package/lib/types/engine-claude/types.d.ts +18 -0
  23. package/lib/types/engine-codex/agent.d.ts +109 -0
  24. package/lib/types/engine-codex/appserver/client.d.ts +49 -0
  25. package/lib/types/engine-codex/appserver/mapping.d.ts +67 -0
  26. package/lib/types/engine-codex/appserver/thread.d.ts +66 -0
  27. package/lib/types/engine-codex/appserver/types.d.ts +215 -0
  28. package/lib/types/engine-codex/loop.d.ts +92 -0
  29. package/lib/types/engine-codex/permission.d.ts +32 -0
  30. package/lib/types/engine-codex/skills.d.ts +26 -0
  31. package/lib/types/engine-codex/types.d.ts +19 -0
  32. package/lib/types/engine-pi/agent.d.ts +125 -0
  33. package/lib/types/engine-pi/loop.d.ts +96 -0
  34. package/lib/types/engine-pi/permission.d.ts +43 -0
  35. package/lib/types/engine-pi/rpc/client.d.ts +105 -0
  36. package/lib/types/engine-pi/rpc/mapping.d.ts +37 -0
  37. package/lib/types/engine-pi/rpc/types.d.ts +235 -0
  38. package/lib/types/engine-pi/skills.d.ts +26 -0
  39. package/lib/types/engine-pi/types.d.ts +27 -0
  40. package/lib/types/index.d.ts +96 -0
  41. package/lib/types/invariant.d.ts +23 -0
  42. package/lib/types/namespace.d.ts +9 -0
  43. package/lib/types/patch-manager.d.ts +47 -0
  44. package/lib/types/settings.d.ts +29 -0
  45. package/lib/types/skills.d.ts +77 -0
  46. package/package.json +103 -0
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # dsh-loop-engine
2
+
3
+ [![npm version](https://img.shields.io/npm/v/dsh-loop-engine?color=cb3837)](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
+ [![npm version](https://img.shields.io/npm/v/dsh-loop-engine?color=cb3837)](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