dsh-loop-engine-allinone 0.0.0-stage → 0.4.0

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 (143) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/CONTRIBUTING.md +78 -0
  3. package/LICENSE +23 -0
  4. package/NOTICE +45 -0
  5. package/README.md +221 -2
  6. package/README.zh.md +89 -0
  7. package/SECURITY.md +39 -0
  8. package/cordis.patch.yml +3 -0
  9. package/lib/client.js +2166 -0
  10. package/lib/index.js +11621 -0
  11. package/lib/invariant.js +453 -0
  12. package/lib/types/agent-preset-ids.d.ts +276 -0
  13. package/lib/types/client/LoopEngineBadge.d.ts +71 -0
  14. package/lib/types/client/LoopEngineComposerSelect.d.ts +126 -0
  15. package/lib/types/client/LoopEngineSection.d.ts +35 -0
  16. package/lib/types/client/engine-marks.d.ts +87 -0
  17. package/lib/types/client/index.d.ts +39 -0
  18. package/lib/types/client/locales.d.ts +182 -0
  19. package/lib/types/client/reload.d.ts +135 -0
  20. package/lib/types/client/session-engine.d.ts +508 -0
  21. package/lib/types/client/store.d.ts +103 -0
  22. package/lib/types/client/turn-status.d.ts +234 -0
  23. package/lib/types/client/use-session-engine.d.ts +74 -0
  24. package/lib/types/commands.d.ts +77 -0
  25. package/lib/types/compat.d.ts +25 -0
  26. package/lib/types/driver-acp/agent.d.ts +289 -0
  27. package/lib/types/driver-acp/approval.d.ts +27 -0
  28. package/lib/types/driver-acp/capabilities.d.ts +200 -0
  29. package/lib/types/driver-acp/client.d.ts +297 -0
  30. package/lib/types/driver-acp/executable.d.ts +53 -0
  31. package/lib/types/driver-acp/mapping.d.ts +76 -0
  32. package/lib/types/driver-acp/profile.d.ts +126 -0
  33. package/lib/types/driver-acp/runtime.d.ts +45 -0
  34. package/lib/types/driver-acp/subprocess.d.ts +19 -0
  35. package/lib/types/driver-acp/terminal.d.ts +98 -0
  36. package/lib/types/driver-acp/transport.d.ts +43 -0
  37. package/lib/types/driver-acp/types.d.ts +152 -0
  38. package/lib/types/driver-cli/capability.d.ts +42 -0
  39. package/lib/types/driver-cli/cli-agent.d.ts +258 -0
  40. package/lib/types/driver-cli/cli-loop.d.ts +46 -0
  41. package/lib/types/driver-cli/flatten.d.ts +35 -0
  42. package/lib/types/driver-cli/frames.d.ts +128 -0
  43. package/lib/types/driver-cli/ndjson.d.ts +106 -0
  44. package/lib/types/driver-cli/session-sink.d.ts +333 -0
  45. package/lib/types/driver-cli/stream-json.d.ts +61 -0
  46. package/lib/types/driver-cli/text.d.ts +66 -0
  47. package/lib/types/driver-cli/transport.d.ts +84 -0
  48. package/lib/types/driver-core/agents-md-skill-provider.d.ts +72 -0
  49. package/lib/types/driver-core/assistant-stream.d.ts +68 -0
  50. package/lib/types/driver-core/context-files.d.ts +62 -0
  51. package/lib/types/driver-core/host-servers.d.ts +119 -0
  52. package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
  53. package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +78 -0
  54. package/lib/types/driver-core/idle-child.d.ts +57 -0
  55. package/lib/types/driver-core/image-access.d.ts +25 -0
  56. package/lib/types/driver-core/inbox.d.ts +104 -0
  57. package/lib/types/driver-core/model-handover.d.ts +134 -0
  58. package/lib/types/driver-core/ownership.d.ts +41 -0
  59. package/lib/types/driver-core/permission-knobs.d.ts +26 -0
  60. package/lib/types/driver-core/prompt.d.ts +64 -0
  61. package/lib/types/driver-core/session-lifetime.d.ts +62 -0
  62. package/lib/types/driver-core/session-model.d.ts +128 -0
  63. package/lib/types/driver-core/skill-inject.d.ts +59 -0
  64. package/lib/types/driver-core/step-close.d.ts +49 -0
  65. package/lib/types/driver-core/step-images.d.ts +62 -0
  66. package/lib/types/driver-core/step-tools.d.ts +43 -0
  67. package/lib/types/driver-core/system-head.d.ts +43 -0
  68. package/lib/types/engine-agy/agent.d.ts +67 -0
  69. package/lib/types/engine-agy/frames.d.ts +90 -0
  70. package/lib/types/engine-agy/loop.d.ts +64 -0
  71. package/lib/types/engine-agy/process.d.ts +173 -0
  72. package/lib/types/engine-claude/agent.d.ts +186 -0
  73. package/lib/types/engine-claude/loop.d.ts +73 -0
  74. package/lib/types/engine-claude/mapping.d.ts +108 -0
  75. package/lib/types/engine-claude/permission.d.ts +41 -0
  76. package/lib/types/engine-claude/process.d.ts +59 -0
  77. package/lib/types/engine-claude/sdk.d.ts +67 -0
  78. package/lib/types/engine-claude/types.d.ts +28 -0
  79. package/lib/types/engine-codex/agent.d.ts +216 -0
  80. package/lib/types/engine-codex/appserver/client.d.ts +80 -0
  81. package/lib/types/engine-codex/appserver/mapping.d.ts +67 -0
  82. package/lib/types/engine-codex/appserver/thread.d.ts +66 -0
  83. package/lib/types/engine-codex/appserver/types.d.ts +225 -0
  84. package/lib/types/engine-codex/loop.d.ts +75 -0
  85. package/lib/types/engine-codex/model-handover.d.ts +56 -0
  86. package/lib/types/engine-codex/permission.d.ts +126 -0
  87. package/lib/types/engine-codex/skills.d.ts +28 -0
  88. package/lib/types/engine-codex/types.d.ts +36 -0
  89. package/lib/types/engine-drivers.d.ts +132 -0
  90. package/lib/types/engine-grok/agent.d.ts +24 -0
  91. package/lib/types/engine-grok/loop.d.ts +61 -0
  92. package/lib/types/engine-grok/process.d.ts +46 -0
  93. package/lib/types/engine-grok/profile.d.ts +82 -0
  94. package/lib/types/engine-hermes/agent.d.ts +47 -0
  95. package/lib/types/engine-hermes/loop.d.ts +56 -0
  96. package/lib/types/engine-hermes/process.d.ts +92 -0
  97. package/lib/types/engine-kimi/acp/client.d.ts +123 -0
  98. package/lib/types/engine-kimi/acp/mapping.d.ts +73 -0
  99. package/lib/types/engine-kimi/acp/types.d.ts +143 -0
  100. package/lib/types/engine-kimi/agent.d.ts +24 -0
  101. package/lib/types/engine-kimi/commands.d.ts +44 -0
  102. package/lib/types/engine-kimi/loop.d.ts +62 -0
  103. package/lib/types/engine-kimi/model-handover.d.ts +32 -0
  104. package/lib/types/engine-kimi/permission.d.ts +28 -0
  105. package/lib/types/engine-kimi/process.d.ts +47 -0
  106. package/lib/types/engine-kimi/profile.d.ts +19 -0
  107. package/lib/types/engine-kimi/skills.d.ts +52 -0
  108. package/lib/types/engine-kimi/types.d.ts +25 -0
  109. package/lib/types/engine-minimaxcode/agent.d.ts +24 -0
  110. package/lib/types/engine-minimaxcode/loop.d.ts +83 -0
  111. package/lib/types/engine-minimaxcode/process.d.ts +53 -0
  112. package/lib/types/engine-minimaxcode/profile.d.ts +72 -0
  113. package/lib/types/engine-of-session.d.ts +97 -0
  114. package/lib/types/engine-pi/agent.d.ts +230 -0
  115. package/lib/types/engine-pi/loop.d.ts +72 -0
  116. package/lib/types/engine-pi/model-handover.d.ts +84 -0
  117. package/lib/types/engine-pi/permission.d.ts +47 -0
  118. package/lib/types/engine-pi/rpc/client.d.ts +107 -0
  119. package/lib/types/engine-pi/rpc/mapping.d.ts +37 -0
  120. package/lib/types/engine-pi/rpc/types.d.ts +249 -0
  121. package/lib/types/engine-pi/skills.d.ts +47 -0
  122. package/lib/types/engine-pi/types.d.ts +36 -0
  123. package/lib/types/engine-qoderclicn/agent.d.ts +59 -0
  124. package/lib/types/engine-qoderclicn/loop.d.ts +51 -0
  125. package/lib/types/engine-qoderclicn/process.d.ts +113 -0
  126. package/lib/types/engine-registry.d.ts +584 -0
  127. package/lib/types/engine-remote.d.ts +192 -0
  128. package/lib/types/engine-surface.d.ts +36 -0
  129. package/lib/types/engine-zcode/agent.d.ts +50 -0
  130. package/lib/types/engine-zcode/loop.d.ts +47 -0
  131. package/lib/types/engine-zcode/process.d.ts +111 -0
  132. package/lib/types/index.d.ts +205 -0
  133. package/lib/types/invariant.d.ts +26 -0
  134. package/lib/types/model-selection-reset.d.ts +271 -0
  135. package/lib/types/namespace.d.ts +25 -0
  136. package/lib/types/patch-manager.d.ts +104 -0
  137. package/lib/types/preset.d.ts +254 -0
  138. package/lib/types/provider-route.d.ts +245 -0
  139. package/lib/types/router-loop.d.ts +458 -0
  140. package/lib/types/session-engine-store.d.ts +138 -0
  141. package/lib/types/settings.d.ts +60 -0
  142. package/lib/types/skills.d.ts +85 -0
  143. package/package.json +150 -4
package/lib/client.js ADDED
@@ -0,0 +1,2166 @@
1
+ var module = { exports: {} }; var exports = module.exports; window.__ModuleLoader__.load({ id: "dsh-loop-engine-allinone", factory: (require) => {
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/client/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ apply: () => apply,
35
+ inject: () => inject
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/client/LoopEngineSection.tsx
40
+ var import_react = require("react");
41
+ var primitives = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
42
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
43
+
44
+ // src/engine-registry.ts
45
+ var HOSTED_ENGINE_IDS = [
46
+ "claude-code",
47
+ "codex",
48
+ "pi",
49
+ "kimi",
50
+ "agy",
51
+ "zcode",
52
+ "qoderclicn",
53
+ "hermes",
54
+ "grok",
55
+ "minimaxcode"
56
+ ];
57
+ var LOOP_ENGINE_IDS = ["in-process", ...HOSTED_ENGINE_IDS];
58
+ var ENGINE_REGISTRY = {
59
+ "claude-code": {
60
+ id: "claude-code",
61
+ label: "Claude Code",
62
+ repo: "https://github.com/anthropics/claude-code",
63
+ // The SDK is an npm dependency, but its `pathToClaudeCodeExecutable` option
64
+ // hands the work to an external binary, so the deployment may unbind it.
65
+ bundled: true,
66
+ binConfigKey: "claudeBin",
67
+ versionArgv: ["--version"],
68
+ loginHint: "run `claude` once and sign in, or point `claudeBin` at a signed-in install",
69
+ authPaths: [".claude/.credentials.json", ".claude.json"],
70
+ localeKey: "engineClaudeCode",
71
+ capabilities: {
72
+ streaming: true,
73
+ // The Agent SDK delivers an async message stream; the driver reads it as
74
+ // frames, which is the ndjson family's shape.
75
+ protocol: "ndjson",
76
+ sessionResume: true,
77
+ permissionModes: ["default", "acceptEdits", "bypassPermissions", "plan"],
78
+ images: true,
79
+ reasoningLevels: false
80
+ }
81
+ },
82
+ codex: {
83
+ id: "codex",
84
+ label: "Codex",
85
+ repo: "https://github.com/openai/codex",
86
+ bundled: true,
87
+ binConfigKey: "codexBin",
88
+ versionArgv: ["--version"],
89
+ loginHint: "run `codex login`, or set CODEX_API_KEY, or point `codexBin` at a signed-in install",
90
+ authPaths: [".codex/auth.json"],
91
+ localeKey: "engineCodex",
92
+ capabilities: {
93
+ streaming: true,
94
+ protocol: "jsonrpc",
95
+ sessionResume: true,
96
+ // Codex declares its stance as a (sandbox mode, approval policy) PAIR at
97
+ // `thread/start`, so the modes are the sandbox half.
98
+ permissionModes: ["read-only", "workspace-write", "danger-full-access"],
99
+ images: true,
100
+ reasoningLevels: true
101
+ }
102
+ },
103
+ pi: {
104
+ id: "pi",
105
+ label: "Pi",
106
+ repo: "https://github.com/earendil-works/pi",
107
+ bundled: true,
108
+ binConfigKey: "piBin",
109
+ versionArgv: ["--version"],
110
+ loginHint: "run `pi` once and sign in, or set the provider API-key variable pi expects",
111
+ authPaths: [".pi/agent/auth.json"],
112
+ localeKey: "enginePi",
113
+ capabilities: {
114
+ streaming: true,
115
+ protocol: "jsonrpc",
116
+ // Every step opens a fresh `pi --mode rpc` session, so there is no id to
117
+ // resume across launches.
118
+ sessionResume: false,
119
+ permissionModes: ["read-only", "workspace-write", "danger-full-access"],
120
+ images: true,
121
+ reasoningLevels: true
122
+ }
123
+ },
124
+ kimi: {
125
+ id: "kimi",
126
+ label: "Kimi Code",
127
+ repo: "https://github.com/MoonshotAI/kimi-cli",
128
+ bundled: false,
129
+ binConfigKey: "kimiBin",
130
+ versionArgv: ["--version"],
131
+ updateArgv: ["update"],
132
+ loginHint: "run `kimi` once and sign in (or `kimi login`)",
133
+ authPaths: [".kimi-code/credentials.json", ".kimi-code/auth.json"],
134
+ localeKey: "engineKimi",
135
+ capabilities: {
136
+ streaming: true,
137
+ protocol: "acp",
138
+ sessionResume: true,
139
+ permissionModes: ["ask", "allow", "deny"],
140
+ images: true,
141
+ reasoningLevels: false
142
+ }
143
+ },
144
+ agy: {
145
+ id: "agy",
146
+ label: "Antigravity",
147
+ repo: "https://antigravity.google",
148
+ bundled: false,
149
+ binConfigKey: "agyBin",
150
+ versionArgv: ["--version"],
151
+ updateArgv: ["update"],
152
+ loginHint: "run `agy` once and complete the Google sign-in it opens",
153
+ authPaths: [".gemini/oauth_creds.json"],
154
+ localeKey: "engineAgy",
155
+ // Verified with `agy models` on a signed-in host (2026-10-17) and confirmed
156
+ // selectable end to end: `--model gemini-3.8-flash-low` answered PONG.
157
+ models: [
158
+ { id: "gemini-3.8-flash-high", label: "Gemini 3.8 Flash (High)" },
159
+ { id: "gemini-3.8-flash-medium", label: "Gemini 3.8 Flash (Medium)" },
160
+ { id: "gemini-3.8-flash-low", label: "Gemini 3.8 Flash (Low)" },
161
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (Thinking)" },
162
+ { id: "claude-opus-4-6-thinking", label: "Claude Opus 4.6 (Thinking)" },
163
+ { id: "gpt-oss-120b-medium", label: "GPT-OSS 120B (Medium)" }
164
+ ],
165
+ capabilities: {
166
+ // VERIFIED against a signed-in host: `step_update` frames carry
167
+ // `text_delta` increments, and the driver maps them (see
168
+ // `engine-agy/frames.ts`).
169
+ streaming: true,
170
+ protocol: "ndjson",
171
+ // The CLI continues a conversation with `--conversation <id>` and reports
172
+ // the id it used, but this driver does NOT use it: the plugin is stateless
173
+ // by design, replaying the durable transcript into every step, so there is
174
+ // nothing to carry across steps and nothing that could leak between them.
175
+ sessionResume: true,
176
+ // The CLI's EXECUTION modes. They are deliberately not treated as the
177
+ // permission switch: `--mode accept-edits` and `--mode plan` were both
178
+ // verified to leave `permission_mode` at `request-review`. What actually
179
+ // gates tools headless is `--dangerously-skip-permissions`, mapped from
180
+ // dsh's sandbox knob (`agyDangerouslySkipPermissions`).
181
+ permissionModes: ["accept-edits", "auto", "plan"],
182
+ // Verified absent: there is no image input on this surface.
183
+ images: false,
184
+ reasoningLevels: true
185
+ }
186
+ },
187
+ zcode: {
188
+ id: "zcode",
189
+ label: "ZCode",
190
+ repo: "https://z.ai",
191
+ bundled: false,
192
+ binConfigKey: "zcodeBin",
193
+ versionArgv: ["version"],
194
+ updateArgv: ["plugins", "update"],
195
+ loginHint: "run `zcode login` to sign in with Z.AI",
196
+ authPaths: [".zcode/v2", ".zcode/cli"],
197
+ localeKey: "engineZcode",
198
+ capabilities: {
199
+ streaming: true,
200
+ protocol: "jsonrpc",
201
+ sessionResume: true,
202
+ permissionModes: ["build", "edit", "plan", "yolo"],
203
+ images: false,
204
+ reasoningLevels: false
205
+ }
206
+ },
207
+ qoderclicn: {
208
+ id: "qoderclicn",
209
+ label: "Qoder CN",
210
+ repo: "https://qoder.com",
211
+ bundled: false,
212
+ binConfigKey: "qoderclicnBin",
213
+ versionArgv: ["--version"],
214
+ updateArgv: ["update"],
215
+ loginHint: "run `qoderclicn login` to sign in to Qoder CN",
216
+ authPaths: [".qoder-cn/.auth/user"],
217
+ localeKey: "engineQoderclicn",
218
+ capabilities: {
219
+ streaming: true,
220
+ // `-p --output-format stream-json` emits Claude Code's stream-json frames,
221
+ // one JSON document per line.
222
+ protocol: "ndjson",
223
+ sessionResume: true,
224
+ permissionModes: ["default", "accept_edits", "bypass_permissions", "dont_ask", "auto"],
225
+ // `--attachment` takes files, but a probe could not confirm image blocks
226
+ // survive the round trip, so the engine does not claim the capability.
227
+ images: false,
228
+ reasoningLevels: true
229
+ }
230
+ },
231
+ hermes: {
232
+ id: "hermes",
233
+ label: "Hermes Agent",
234
+ repo: "https://github.com/NousResearch/hermes",
235
+ bundled: false,
236
+ binConfigKey: "hermesBin",
237
+ versionArgv: ["--version"],
238
+ updateArgv: ["update"],
239
+ loginHint: "run `hermes setup` (or `hermes auth`) to sign in to an inference provider",
240
+ authPaths: ["auth.json"],
241
+ localeKey: "engineHermes",
242
+ capabilities: {
243
+ // `-z PROMPT` prints the finished answer; a probe found no structured
244
+ // stream, so the driver reads the whole of stdout as one reply.
245
+ streaming: false,
246
+ protocol: "text",
247
+ sessionResume: true,
248
+ permissionModes: ["safe-mode", "yolo"],
249
+ images: false,
250
+ reasoningLevels: true
251
+ }
252
+ },
253
+ grok: {
254
+ id: "grok",
255
+ label: "Grok Build",
256
+ repo: "https://github.com/xai-org/grok-build",
257
+ bundled: false,
258
+ binConfigKey: "grokBin",
259
+ versionArgv: ["--version"],
260
+ // Verified against the released binary (1.0.59): `grok update Check for
261
+ // updates or install a specific version`. The vendor's own installer
262
+ // (`x.ai/cli/install.sh`) is the other upgrade path and reinstalls in place.
263
+ updateArgv: ["update"],
264
+ loginHint: "run `grok` once and complete the sign-in it opens (or `grok login`), then retry",
265
+ authPaths: [".grok/auth.json"],
266
+ localeKey: "engineGrok",
267
+ // Measured from the SIGNED-IN agent's own `initialize` answer
268
+ // (`_meta.modelState`): the catalog it will actually serve, whose
269
+ // `currentModelId` is `grok-4.7`. This REPLACED an earlier list taken from
270
+ // the unauthenticated `grok models` command, which reported `grok-4.6` and
271
+ // `grok-4.5` — ids the ACP surface then rejected with `-32602 unknown model
272
+ // id`. The lesson is the fork's own rule: a catalog is read from the surface
273
+ // the driver talks to, not from a sibling command that may be reading a
274
+ // different source.
275
+ models: [
276
+ { id: "grok-4.7", label: "Grok 4.7" }
277
+ ],
278
+ capabilities: {
279
+ // Measured: `session/update` frames carry `agent_message_chunk` and
280
+ // `agent_thought_chunk` increments.
281
+ streaming: true,
282
+ protocol: "acp",
283
+ // Measured: `initialize` answers `loadSession: true` with
284
+ // `sessionCapabilities.resume`, and the user guide documents
285
+ // `session/load` plus headless `--resume`/`--continue`.
286
+ sessionResume: true,
287
+ // The ACP gate is `session/request_permission`, and this engine's own
288
+ // always-approve switch is the `--always-approve` argv flag (alias
289
+ // `--yolo`), which `session/new` can also express as `_meta.yoloMode`.
290
+ permissionModes: ["ask", "always-approve"],
291
+ // Measured: `initialize` answers `promptCapabilities.image: false`, so the
292
+ // driver keeps images as path-bearing placeholders instead of sending
293
+ // blocks this engine would refuse.
294
+ images: false,
295
+ // The user guide documents the `reasoning_effort` session config option
296
+ // (`minimal`, `low`, `medium`, `high`, `xhigh`).
297
+ reasoningLevels: true,
298
+ // Measured: the engine answers `protocolVersion: 1` to a client that
299
+ // announces 1.
300
+ acpProtocolVersion: 1
301
+ }
302
+ },
303
+ minimaxcode: {
304
+ id: "minimaxcode",
305
+ label: "MiniMax Code",
306
+ // The vendor's documentation site; the GitHub org (`github.com/MiniMax-AI`)
307
+ // publishes no CLI repository for this product.
308
+ repo: "https://agent.minimax.io/docs/cli/quick-start",
309
+ bundled: false,
310
+ binConfigKey: "minimaxcodeBin",
311
+ versionArgv: ["--version"],
312
+ // Verified against 0.6.12: `mcode update Check for and install a Minimax
313
+ // Code update`.
314
+ updateArgv: ["update"],
315
+ // The subscription path is the whole point of this engine: signing in is what
316
+ // enables the M Plan allowance, and `provider use token-plan` is what makes
317
+ // the CLI spend it instead of an API key.
318
+ loginHint: "run `mcode login --region cn|global`, then `mcode provider use token-plan` to spend your M Plan subscription",
319
+ // Measured on this host: credentials live per REGION under
320
+ // `~/.minimax/auth/prod/<region>/mcode-public/auth.json`, and the CLI writes
321
+ // an `auth-state.json` beside them. The directory, not the file, is the
322
+ // "signed in at least once" fact.
323
+ authPaths: [".minimax/auth/prod"],
324
+ region: {
325
+ configKey: "mcodeRegion",
326
+ // MEASURED, and not the name one would guess: `MCODE_REGION` is INERT on
327
+ // the `mcode acp` path (a child with `MCODE_REGION=en` still authenticated
328
+ // as the `cn` account), while `MAVIS_REGION=en` flipped it to "auth
329
+ // required" — so `MAVIS_REGION` is the variable THIS surface honours. The
330
+ // `MCODE_*` family belongs to the CLI's Electron/mcode-tools broker, which
331
+ // is a different process. A driver that set `MCODE_REGION` would look
332
+ // correct and change nothing.
333
+ envVar: "MAVIS_REGION",
334
+ default: "cn",
335
+ // TWO vocabularies, and conflating them would set a value the CLI rejects:
336
+ // the LOGIN flag speaks `cn|global` (`mcode login --region cn|global`),
337
+ // while the region VALUE the runtime reads and validates is `cn|en`
338
+ // (shipped code: `i!=="cn"&&i!=="en"` throws "The MCode mcode-tools host
339
+ // environment is incomplete"; the same value picks the `zh`/`en` locale and
340
+ // the `mavis` endpoint host). So `en` is this knob's spelling of the
341
+ // non-China account, and the values below are the ones this variable
342
+ // accepts. Only `cn` was measurable here — this host's account is a `cn`
343
+ // one, and the `en` side was observed only as "the cn credentials no longer
344
+ // apply".
345
+ values: ["cn", "en"]
346
+ },
347
+ localeKey: "engineMinimaxcode",
348
+ // Measured from the live `session/new` answer (unauthenticated catalog):
349
+ // MiniMax Code's own composite model ids, whose `v:` suffix selects the
350
+ // variant. A subscription can expose more; a stale id fails loud at the
351
+ // engine, which is the honest outcome.
352
+ models: [
353
+ { id: "m:minimax:MiniMax-M3:v:thinking", label: "M3 \xB7 thinking" },
354
+ { id: "m:minimax:MiniMax-M3:v:", label: "M3" },
355
+ { id: "m:minimax:MiniMax-M3.1-Flash-Preview:v:thinking", label: "M3.1-Flash-Preview \xB7 thinking" },
356
+ { id: "m:minimax:MiniMax-M3.1-Flash-Preview:v:", label: "M3.1-Flash-Preview" },
357
+ { id: "m:minimax:MiniMax-M2.7-highspeed:v:thinking", label: "M2.7-highspeed \xB7 thinking" },
358
+ { id: "m:minimax:MiniMax-M2.7:v:thinking", label: "M2.7 \xB7 thinking" }
359
+ ],
360
+ capabilities: {
361
+ // Measured: `agent_message_chunk` / `agent_thought_chunk` increments, and
362
+ // the ACP handshake answered a real prompt with `stopReason: end_turn`.
363
+ streaming: true,
364
+ protocol: "acp",
365
+ // Measured: `loadSession: true`, `sessionCapabilities.resume`, and a real
366
+ // `session/load` was ACCEPTED on 0.6.12.
367
+ sessionResume: true,
368
+ // Measured from the live `session/new` configOptions: the engine's
369
+ // `permissionMode` option is exactly these three values.
370
+ permissionModes: ["default", "auto", "bypassPermissions"],
371
+ // Measured: `promptCapabilities.image: true` (and the engine's own catalog
372
+ // declares image/video modalities for the M3 models).
373
+ images: true,
374
+ // Measured: a `thinkingEffort` config option
375
+ // (`default`/`low`/`medium`/`high`/`xhigh`/`max`).
376
+ reasoningLevels: true,
377
+ // Measured: the engine answers `protocolVersion: 1`.
378
+ acpProtocolVersion: 1
379
+ }
380
+ }
381
+ };
382
+ var ENGINE_DESCRIPTORS = HOSTED_ENGINE_IDS.map((id) => ENGINE_REGISTRY[id]);
383
+ function isHostedEngine(engine) {
384
+ return engine !== void 0 && engine !== "in-process";
385
+ }
386
+ function isLoopEngineId(value) {
387
+ return LOOP_ENGINE_IDS.includes(value);
388
+ }
389
+
390
+ // src/agent-preset-ids.ts
391
+ var LOOP_ENGINE_REFUSAL_CODES = [
392
+ "session-closed",
393
+ "turn-running",
394
+ "subagent-session",
395
+ "not-driven",
396
+ "router-unmounted",
397
+ "record-failed",
398
+ "rebuild-failed"
399
+ ];
400
+ function isLoopEngineRefusalCode(value) {
401
+ return LOOP_ENGINE_REFUSAL_CODES.includes(value);
402
+ }
403
+ function hostedEngineOf(session) {
404
+ if (session.kind !== "engine" || session.engine === "in-process") return void 0;
405
+ return session.engine;
406
+ }
407
+
408
+ // src/client/locales.ts
409
+ var zh = {
410
+ nav: "\u5FAA\u73AF\u5F15\u64CE",
411
+ description: "\u9009\u62E9\u65B0\u4F1A\u8BDD\u9ED8\u8BA4\u4F7F\u7528\u7684 Agent \u6267\u884C\u5F15\u64CE\uFF1B\u6BCF\u4E2A\u4F1A\u8BDD\u4E5F\u53EF\u4EE5\u5728\u65B0\u4F1A\u8BDD\u9875\u5355\u72EC\u9009\u62E9\u81EA\u5DF1\u7684\u5F15\u64CE\u3002",
412
+ engineInProcess: "\u8FDB\u7A0B\u5185\u5F15\u64CE\uFF08\u9ED8\u8BA4\uFF09",
413
+ engineClaudeCode: "Claude Code CLI",
414
+ engineCodex: "Codex CLI",
415
+ enginePi: "Pi CLI",
416
+ engineKimi: "Kimi Code CLI",
417
+ engineAgy: "Antigravity CLI",
418
+ engineZcode: "ZCode CLI",
419
+ engineQoderclicn: "Qoder CN CLI",
420
+ engineHermes: "Hermes Agent CLI",
421
+ engineGrok: "Grok Build CLI",
422
+ engineMinimaxcode: "MiniMax Code CLI",
423
+ engineLegacy: "\u65E7\u7248\u6258\u7BA1\u5F15\u64CE",
424
+ engineUnrecorded: "\u672A\u8BB0\u5F55",
425
+ engineLoading: "\u8BFB\u53D6\u4E2D\u2026",
426
+ showInComposerLabel: "\u5728\u5BF9\u8BDD\u9875\u663E\u793A\u5F15\u64CE\u9009\u62E9\u5668",
427
+ showEngineBadgeLabel: "\u5728\u5BF9\u8BDD\u5934\u90E8\u663E\u793A\u5F15\u64CE\u6807\u8BB0",
428
+ unavailable: "\u5FAA\u73AF\u5F15\u64CE\u8BBE\u7F6E\u4E0D\u53EF\u7528",
429
+ switchNotice: "\u8FD9\u4E00\u9009\u62E9\u53EA\u51B3\u5B9A\u65B0\u4F1A\u8BDD\u7528\u54EA\u4E2A\u5F15\u64CE\uFF0C\u5DF2\u7ECF\u5728\u8DD1\u7684\u4F1A\u8BDD\u4E0D\u53D7\u5F71\u54CD\u3002",
430
+ saving: "\u4FDD\u5B58\u4E2D\u2026",
431
+ confirmTitle: "\u5207\u6362\u5FAA\u73AF\u5F15\u64CE\uFF1F",
432
+ confirmBody: "\u6B64\u540E\u65B0\u5EFA\u7684\u4F1A\u8BDD\u4F1A\u4F7F\u7528\u8FD9\u4E2A\u5F15\u64CE\uFF1B\u5DF2\u5B58\u5728\u7684\u4F1A\u8BDD\u4FDD\u6301\u5B83\u4EEC\u5404\u81EA\u7684\u5F15\u64CE\u4E0D\u53D8\uFF0C\u4E5F\u4E0D\u9700\u8981\u5237\u65B0\u9875\u9762\u3002\u786E\u8BA4\u5207\u6362\u5417\uFF1F",
433
+ confirmAction: "\u5207\u6362",
434
+ cancelAction: "\u53D6\u6D88",
435
+ closeLabel: "\u5173\u95ED",
436
+ hostedEngineModelNotice: "\u672C\u4F1A\u8BDD\u7531\u5916\u90E8\u5F15\u64CE\u9A71\u52A8\uFF1A\u9ED8\u8BA4\u7528\u54EA\u4E2A\u6A21\u578B\u7531\u8BE5\u5F15\u64CE\u81EA\u5DF1\u51B3\u5B9A\u3002\u5728\u6A21\u578B\u83DC\u5355\u91CC\u9009\u4E00\u4E2A\u771F\u5B9E dsh \u6A21\u578B\uFF0C\u4F1A\u628A\u8FD9\u4E2A\u6A21\u578B\u7684\u7AEF\u70B9\u4E0E\u51ED\u636E\u4E00\u5E76\u4EA4\u7ED9\u8BE5\u5F15\u64CE\u4F7F\u7528\uFF0C\u8BA9\u5B83\u771F\u7684\u8D70 dsh \u914D\u7F6E\u7684\u90A3\u4E2A\u7AEF\u70B9\u5BF9\u8BDD\u3002\u80FD\u4E0D\u80FD\u7528\u53D6\u51B3\u4E8E\u8BE5\u5F15\u64CE\u662F\u5426\u652F\u6301\u90A3\u4E2A\u534F\u8BAE\uFF0C\u4E0D\u652F\u6301\u5C31\u4F1A\u7531\u5F15\u64CE\u62A5\u9519\u3002\u6A21\u578B\u83DC\u5355\u91CC\u6240\u6709\u6258\u7BA1\u5F15\u64CE\u5171\u7528\u4E00\u4E2A `external` \u5206\u7EC4\uFF08\u6A21\u578B\u76EE\u5F55\u662F\u6574\u4E2A Host \u4EE3\u9645\u5171\u4EAB\u7684\uFF0C\u4E0D\u6309\u4F1A\u8BDD\u533A\u5206\uFF0C\u6240\u4EE5\u5404\u5BB6\u5F15\u64CE\u6298\u53E0\u6210\u8FD9\u4E00\u6761\uFF09\uFF0C\u540D\u4E0B\u90A3\u6761\u300Cdefault\u300D\u8868\u793A\u4EA4\u56DE\u5F15\u64CE\u81EA\u5DF1\u7684\u9ED8\u8BA4\uFF0C\u4E0D\u662F dsh \u80FD\u63D0\u4F9B\u7684\u6A21\u578B\u3002",
437
+ sessionNotice: "\u672C\u4F1A\u8BDD\u5F53\u524D\u8FD0\u884C\u7684\u5F15\u64CE\uFF1A\u6709\u6D3B agent \u65F6\u5C31\u662F\u6B63\u5728\u9A71\u52A8\u5B83\u7684\u90A3\u4E2A\u5F15\u64CE\uFF1B\u6CA1\u6709\u6D3B agent \u65F6\u624D\u770B\u63D2\u4EF6\u7684\u4F1A\u8BDD\u7EA7\u8BB0\u5F55\uFF0C\u6CA1\u6709\u8BB0\u5F55\u518D\u56DE\u9000\u5230\u5B83\u81EA\u5DF1\u7684 agent preset\u3002",
438
+ legacySessionNotice: "\u8FD9\u662F\u672C\u63D2\u4EF6\u65E9\u671F\u7248\u672C\u521B\u5EFA\u7684\u4F1A\u8BDD\uFF1A\u5B83\u5F53\u65F6\u8DD1\u7684\u6258\u7BA1\u5F15\u64CE\u6CA1\u6709\u88AB\u8BB0\u5F55\u3002\u9009\u4E00\u4E2A\u5F15\u64CE\u5373\u53EF\u5728\u672C\u4F1A\u8BDD\u91CC\u5207\u6362\u3002",
439
+ enginePendingPrefix: "\u5207\u5230 ",
440
+ enginePendingSuffix: " \xB7 \u5C1A\u672A\u63A5\u7BA1",
441
+ engineMenuPendingSuffix: "\uFF08\u5C1A\u672A\u63A5\u7BA1\uFF09",
442
+ pendingSessionNotice: "\u672C\u4F1A\u8BDD\u7684\u8BB0\u5F55\u5199\u7740\u4E00\u4E2A\u5F15\u64CE\uFF0C\u4F46\u5B83\u6CA1\u80FD\u63A5\u7BA1\uFF1A\u672C\u4F1A\u8BDD\u6B64\u523B\u4ECD\u7531\u53E6\u4E00\u4E2A\u5F15\u64CE\u9A71\u52A8\uFF08\u6240\u4EE5\u8FD9\u91CC\u5148\u5199\u7684\u662F\u6B63\u5728\u8DD1\u7684\u90A3\u4E2A\uFF09\u3002\u51FA\u73B0\u8FD9\u79CD\u60C5\u51B5\u53EA\u6709\u4E00\u79CD\u539F\u56E0\u2014\u2014\u4E0A\u4E00\u6B21\u5207\u6362\u8981\u62C6\u6389\u65E7 agent \u624D\u80FD\u8BA9\u65B0\u5F15\u64CE\u63A5\u7BA1\uFF0C\u800C\u90A3\u6B21\u62C6\u5378\u6CA1\u6709\u5B8C\u6210\u3002\u518D\u9009\u4E00\u6B21\u76EE\u6807\u5F15\u64CE\u5373\u53EF\u91CD\u8BD5\u3002",
443
+ pendingComposerHint: "\u672C\u4F1A\u8BDD\u7684\u8BB0\u5F55\u5199\u7740\u4E00\u4E2A\u5F15\u64CE\uFF0C\u4F46\u5B83\u6CA1\u80FD\u63A5\u7BA1\uFF1A\u672C\u4F1A\u8BDD\u6B64\u523B\u4ECD\u7531\u53E6\u4E00\u4E2A\u5F15\u64CE\u9A71\u52A8\uFF08\u6807\u7B7E\u4E0A\u5148\u5199\u7684\u662F\u6B63\u5728\u8DD1\u7684\u90A3\u4E2A\uFF09\u3002\u6D89\u53CA\u8FDB\u7A0B\u5185\u5F15\u64CE\u7684\u5207\u6362\u8981\u62C6\u6389\u65E7 agent \u624D\u80FD\u843D\u5230\u4E0B\u4E00\u6B21\u6784\u5EFA\u4E0A\uFF0C\u51FA\u73B0\u8FD9\u79CD\u60C5\u51B5\u8BF4\u660E\u90A3\u6B21\u62C6\u5378\u6CA1\u6709\u6210\u529F\u2014\u2014\u518D\u9009\u4E00\u6B21\u76EE\u6807\u5F15\u64CE\u5373\u53EF\u91CD\u8BD5\uFF1B\u9009\u5F53\u524D\u6B63\u5728\u8DD1\u7684\u90A3\u4E2A\u5F15\u64CE\u5219\u4F1A\u628A\u8BB0\u5F55\u6539\u56DE\u53BB\uFF0C\u8BA9\u5B83\u4FDD\u6301\u539F\u6837\u3002",
444
+ composerHint: "\u9009\u62E9\u672C\u4F1A\u8BDD\u8FD0\u884C\u7684\u5F15\u64CE\uFF1A\u6258\u7BA1\u5F15\u64CE\u4E4B\u95F4\u9009\u4E2D\u5373\u751F\u6548\uFF0C\u539F\u5730\u6362\u624B\uFF0C\u4F1A\u8BDD\u4FDD\u6301\u6253\u5F00\u3001agent \u88AB\u66FF\u6362\uFF1B\u4E0E\u8FDB\u7A0B\u5185\u5F15\u64CE\u4E4B\u95F4\u7684\u5207\u6362\u65E0\u6CD5\u539F\u5730\u63A5\u7BA1\uFF08harness \u7684 loop \u65E2\u4E0D\u4EA4\u51FA\u6D3B\u4F1A\u8BDD\u3001\u4E5F\u4E0D\u63A5\u7BA1\u522B\u4EBA\u5EFA\u7684\u4F1A\u8BDD\uFF09\uFF0C\u9009\u4E2D\u65F6\u4F1A\u5148\u8BF4\u660E\u4EE3\u4EF7\u5E76\u7B49\u786E\u8BA4\uFF0C\u786E\u8BA4\u540E\u5BBF\u4E3B\u62C6\u6389\u8FD9\u6761\u4F1A\u8BDD\u7684 agent \u5E76\u8BA9\u9875\u9762\u91CD\u65B0\u8F7D\u5165\uFF0C\u56DE\u6765\u65F6\u4ECD\u662F\u8FD9\u6761\u4F1A\u8BDD\uFF0C\u7531\u65B0\u5F15\u64CE\u91CD\u5EFA\uFF08\u90A3\u6B21\u91CD\u8F7D\u4F1A\u4E22\u6389\u8FD9\u4E00\u9875\u7684\u6EDA\u52A8\u4F4D\u7F6E\u4E0E\u672A\u63D0\u4EA4\u7684\u8349\u7A3F\uFF0C\u4F1A\u8BDD\u8BB0\u5F55\u4E0D\u53D7\u5F71\u54CD\uFF09\u3002\u4F1A\u8BDD\u9700\u5DF2\u6253\u5F00\u4E14\u7A7A\u95F2\uFF0C\u5426\u5219\u5BBF\u4E3B\u4F1A\u8BF4\u660E\u539F\u56E0\u3002",
445
+ switchFailedTitle: "\u5207\u6362\u672A\u751F\u6548",
446
+ refusedSessionClosed: "\u8FD9\u6761\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u6253\u5F00\u3002\u5148\u6253\u5F00\u5B83\uFF0C\u518D\u5207\u6362\u5F15\u64CE\u3002",
447
+ refusedTurnRunning: "\u8FD9\u6761\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D\u2014\u2014\u7B49\u8FD9\u4E00\u8F6E\u7ED3\u675F\u540E\u518D\u5207\u6362\u5F15\u64CE\u3002\u5F53\u524D\u8FD9\u4E00\u8F6E\u4E0D\u4F1A\u88AB\u6253\u65AD\u3002",
448
+ refusedSubagentSession: "\u8FD9\u662F\u5B50\u4EE3\u7406\u4F1A\u8BDD\uFF1B\u5B83\u7684\u5F15\u64CE\u8DDF\u968F\u6D3E\u53D1\u5B83\u7684\u4E3B\u4F1A\u8BDD\uFF0C\u4E0D\u80FD\u5355\u72EC\u5207\u6362\u3002",
449
+ refusedRouterNotReady: "\u8FD9\u4E2A\u8FDB\u7A0B\u6682\u65F6\u65E0\u6CD5\u5207\u6362\u5F15\u64CE\uFF08\u5FAA\u73AF\u5F15\u64CE\u8DEF\u7531\u5668\u8FD8\u6CA1\u5C31\u7EEA\uFF09\u3002",
450
+ refusedRecordFailed: "\u5F15\u64CE\u8BB0\u5F55\u5199\u5165\u5931\u8D25\uFF0C\u5207\u6362\u6CA1\u6709\u751F\u6548\u3002",
451
+ refusedRebuildFailed: "\u5207\u6362\u6CA1\u6709\u751F\u6548\uFF1A\u65B0\u5F15\u64CE\u6CA1\u80FD\u5EFA\u8D77\u6765\uFF0C\u8FD9\u6761\u4F1A\u8BDD\u7684 agent \u5DF2\u7ECF\u91CA\u653E\u3002\u518D\u6253\u5F00\u4E00\u6B21\u8FD9\u6761\u4F1A\u8BDD\uFF0C\u5B83\u5C31\u4F1A\u7528\u65B0\u5F15\u64CE\u91CD\u5EFA\u3002",
452
+ switchReloadConfirmTitle: "\u5207\u6362\u9700\u8981\u91CD\u65B0\u8F7D\u5165\u9875\u9762",
453
+ switchReloadConfirmBody: "\u4E0E\u8FDB\u7A0B\u5185\u5F15\u64CE\u4E4B\u95F4\u7684\u5207\u6362\u65E0\u6CD5\u539F\u5730\u63A5\u7BA1\uFF1A\u5BBF\u4E3B\u4F1A\u91CA\u653E\u8FD9\u6761\u4F1A\u8BDD\u7684 agent\uFF0C\u5E76\u91CD\u65B0\u8F7D\u5165\u9875\u9762\uFF0C\u7531\u65B0\u5F15\u64CE\u91CD\u5EFA\u5B83\u3002\u8FD9\u4E00\u9875\u7684\u6EDA\u52A8\u4F4D\u7F6E\u3001\u8FD8\u6CA1\u53D1\u51FA\u7684\u8349\u7A3F\u4F1A\u4E22\u5931\uFF1B\u4F1A\u8BDD\u8BB0\u5F55\uFF08\u5BF9\u8BDD\u5386\u53F2\uFF09\u4E0D\u53D7\u5F71\u54CD\u3002\u73B0\u5728\u5207\u6362\u5417\uFF1F",
454
+ switchReloadConfirmAction: "\u5207\u6362\u5E76\u91CD\u8F7D",
455
+ switchReloadTitle: "\u5207\u6362\u5DF2\u751F\u6548\uFF0C\u6B63\u5728\u91CD\u65B0\u8F7D\u5165\u9875\u9762",
456
+ switchReloadBody: "\u5DF2\u5207\u5230\u65B0\u5F15\u64CE\uFF0C\u6B63\u5728\u91CD\u65B0\u8F7D\u5165\u9875\u9762\u5E76\u56DE\u5230\u672C\u4F1A\u8BDD\u2026",
457
+ switchUnavailable: "\u5F53\u524D\u9875\u9762\u62FF\u4E0D\u5230\u5F15\u64CE\u5207\u6362\u670D\u52A1\uFF0C\u65E0\u6CD5\u5207\u6362\u672C\u4F1A\u8BDD\u7684\u5F15\u64CE\u3002"
458
+ };
459
+ var en = {
460
+ nav: "Loop engine",
461
+ description: "Choose the agent execution engine NEW sessions start on; each session can also pick its own engine on the new-session screen.",
462
+ engineInProcess: "In-process engine (default)",
463
+ engineClaudeCode: "Claude Code CLI",
464
+ engineCodex: "Codex CLI",
465
+ enginePi: "Pi CLI",
466
+ engineKimi: "Kimi Code CLI",
467
+ engineAgy: "Antigravity CLI",
468
+ engineZcode: "ZCode CLI",
469
+ engineQoderclicn: "Qoder CN CLI",
470
+ engineHermes: "Hermes Agent CLI",
471
+ engineGrok: "Grok Build CLI",
472
+ engineMinimaxcode: "MiniMax Code CLI",
473
+ engineLegacy: "Legacy hosted engine",
474
+ engineUnrecorded: "Not recorded",
475
+ engineLoading: "Reading\u2026",
476
+ showInComposerLabel: "Show the engine selector in the chat page",
477
+ showEngineBadgeLabel: "Show the engine badge in the conversation header",
478
+ unavailable: "Loop engine settings are unavailable",
479
+ switchNotice: "This only decides what new sessions run; sessions already running keep their own engine.",
480
+ saving: "Saving\u2026",
481
+ confirmTitle: "Switch loop engine?",
482
+ confirmBody: "Sessions created after this point use the new engine. Sessions that already exist keep the engine they were created with, and nothing reloads. Switch now?",
483
+ confirmAction: "Switch",
484
+ cancelAction: "Cancel",
485
+ closeLabel: "Close",
486
+ hostedEngineModelNotice: `This session is driven by a hosted engine: by default that engine decides the model. Picking a real dsh model in the menu hands the engine that model's endpoint and credential too, so the engine really talks to the endpoint dsh configured. Whether it works depends on whether the engine supports that protocol, and one it does not support reports an error from the engine. Every hosted engine shares one provider group, "external", in the model menu (the model catalog is shared across the whole Host generation rather than per session, so the engines collapse into that one group), whose single entry, "default", means "hand the choice back to the engine's own default" \u2014 not a model dsh can serve.`,
487
+ sessionNotice: "The engine this session runs right now: the agent driving it, when one is live, and otherwise the plugin's per-session record (falling back to the session's own agent preset when there is no record).",
488
+ legacySessionNotice: "This session was created by an earlier version of the plugin: the hosted engine it ran was never recorded. Pick any engine to switch it here.",
489
+ enginePendingPrefix: "\u2192 ",
490
+ enginePendingSuffix: " \xB7 not in force",
491
+ engineMenuPendingSuffix: " (not in force)",
492
+ pendingSessionNotice: "This session's record names an engine that has not taken over: the session is still driven by another engine right now \u2014 which is the one named first. There is exactly one way this happens: a switch had to release the old agent for the new engine to take over, and that release did not complete. Pick the target engine again to retry it.",
493
+ pendingComposerHint: "This session's record names an engine that has not taken over: the session is still driven by another engine (the label names that one first). A switch involving the in-process engine has to release the old agent for the new engine to take over on the session's next build, and this state means that release did not succeed \u2014 pick the target engine again to retry it, or pick the engine the session is actually running to put the record back and leave it as it is.",
494
+ composerHint: "Choose the engine this session runs. A pick between two hosted engines applies at once \u2014 the agent is swapped in place and the session stays open. A switch involving the in-process engine cannot be taken over in place (the harness loop neither hands over a live session nor takes over one it did not create), so it says what it costs and waits for a confirmation first; once confirmed, the host releases this session's agent and the page reloads itself, coming back to this same session, rebuilt on the new engine \u2014 that reload drops this page's scroll position and any unsent draft, while the conversation record is unaffected. The session must be open and idle, or the host says why.",
495
+ switchFailedTitle: "Switch not applied",
496
+ refusedSessionClosed: "This session is not open yet. Open it, then switch its engine.",
497
+ refusedTurnRunning: "This session is running \u2014 switch its engine after this turn ends. The turn in flight is not interrupted.",
498
+ refusedSubagentSession: "This is a subagent session; its engine follows the main session that spawned it and cannot be switched on its own.",
499
+ refusedRouterNotReady: "This process cannot switch engines right now (the loop engine router is not ready).",
500
+ refusedRecordFailed: "The engine record could not be written, so the switch did not happen.",
501
+ refusedRebuildFailed: "The switch did not happen: the new engine could not be started and this session's agent has already been released. Open this session again and it is rebuilt on the new engine.",
502
+ switchReloadConfirmTitle: "This switch reloads the page",
503
+ switchReloadConfirmBody: "A switch involving the in-process engine cannot be handed over in place: the host releases this session's agent and reloads the page, which rebuilds the session on the new engine. Your scroll position and any unsent draft in this page are lost; the conversation record is not. Switch now?",
504
+ switchReloadConfirmAction: "Switch and reload",
505
+ switchReloadTitle: "Switch applied \u2014 reloading the page",
506
+ switchReloadBody: "Switched. Reloading the page and returning to this session\u2026",
507
+ switchUnavailable: "This page cannot reach the engine switch endpoint, so the session engine was not switched."
508
+ };
509
+ function engineLabelKey(engine) {
510
+ if (engine === "in-process") return "engineInProcess";
511
+ return ENGINE_REGISTRY[engine].localeKey;
512
+ }
513
+ function engineStateLabelKey(state) {
514
+ if (state.kind === "engine") return engineLabelKey(state.engine);
515
+ return state.kind === "legacy" ? "engineLegacy" : "engineUnrecorded";
516
+ }
517
+ function pendingEngineText(t, engine) {
518
+ return `${t("enginePendingPrefix")}${t(engineLabelKey(engine))}${t("enginePendingSuffix")}`;
519
+ }
520
+ var REFUSAL_FACES = {
521
+ "session-closed": { body: "refusedSessionClosed", detail: false },
522
+ "turn-running": { body: "refusedTurnRunning", detail: false },
523
+ "subagent-session": { body: "refusedSubagentSession", detail: false },
524
+ "not-driven": { body: "refusedRouterNotReady", detail: false },
525
+ "router-unmounted": { body: "refusedRouterNotReady", detail: false },
526
+ "record-failed": { body: "refusedRecordFailed", detail: true },
527
+ "rebuild-failed": { body: "refusedRebuildFailed", detail: true }
528
+ };
529
+ function refusalFace(code) {
530
+ return code === void 0 ? { detail: false } : REFUSAL_FACES[code];
531
+ }
532
+
533
+ // src/client/LoopEngineSection.tsx
534
+ var import_jsx_runtime = require("react/jsx-runtime");
535
+ var IconChevronDown = primitives.IconChevronDownOutlineRegular ?? primitives.IconChevronDownOutline14;
536
+ var ENGINE_OPTIONS = LOOP_ENGINE_IDS.map((value) => ({ value, key: engineLabelKey(value) }));
537
+ var shell = {
538
+ display: "flex",
539
+ flexDirection: "column",
540
+ gap: 12,
541
+ maxWidth: 720,
542
+ color: "var(--dsw-alias-label-primary)"
543
+ };
544
+ var titleStyle = {
545
+ margin: 0,
546
+ fontSize: 18,
547
+ fontWeight: 600
548
+ };
549
+ var intro = {
550
+ margin: 0,
551
+ fontSize: 13,
552
+ color: "var(--dsw-alias-label-tertiary)"
553
+ };
554
+ var trigger = {
555
+ appearance: "none",
556
+ boxSizing: "border-box",
557
+ display: "inline-flex",
558
+ alignItems: "center",
559
+ gap: 8,
560
+ width: "fit-content",
561
+ minWidth: 200,
562
+ padding: "9px 12px",
563
+ border: "1px solid var(--dsw-alias-border-l2)",
564
+ borderRadius: 10,
565
+ background: "var(--dsw-alias-bg-layer-1)",
566
+ color: "var(--dsw-alias-label-primary)",
567
+ font: "inherit",
568
+ fontSize: 13,
569
+ cursor: "pointer"
570
+ };
571
+ var triggerDisabled = { ...trigger, opacity: 0.5, cursor: "default" };
572
+ var notice = {
573
+ margin: 0,
574
+ fontSize: 12,
575
+ color: "var(--dsw-alias-label-secondary)"
576
+ };
577
+ var error = {
578
+ margin: 0,
579
+ fontSize: 13,
580
+ color: "var(--dsw-alias-state-error-primary)"
581
+ };
582
+ var toggleRow = {
583
+ display: "flex",
584
+ alignItems: "center",
585
+ gap: 8,
586
+ fontSize: 13,
587
+ color: "var(--dsw-alias-label-primary)",
588
+ cursor: "pointer"
589
+ };
590
+ var toggleCheckbox = {
591
+ width: 16,
592
+ height: 16,
593
+ accentColor: "var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary))",
594
+ cursor: "pointer"
595
+ };
596
+ var confirmBody = {
597
+ margin: 0,
598
+ fontSize: 13,
599
+ lineHeight: 1.55,
600
+ color: "var(--dsw-alias-label-secondary)"
601
+ };
602
+ function LoopEngineSection(props) {
603
+ const { controller, useSnapshot, t } = props;
604
+ const { status, engine, showInComposer, showEngineBadge, writable } = useSnapshot((snapshot) => snapshot);
605
+ const [open, setOpen] = (0, import_react.useState)(false);
606
+ const [pending, setPending] = (0, import_react.useState)(null);
607
+ const navId = (0, import_react.useId)();
608
+ const triggerRef = (0, import_react.useRef)(null);
609
+ if (status === "unavailable") {
610
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": navId, style: shell, children: [
611
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: navId, style: titleStyle, children: t("nav") }),
612
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: intro, children: t("description") }),
613
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", style: error, children: t("unavailable") })
614
+ ] });
615
+ }
616
+ const disabled = status === "saving" || !writable;
617
+ const label = t(engineLabelKey(engine));
618
+ const onSelect = (next) => {
619
+ setOpen(false);
620
+ const value = next;
621
+ if (value === engine) return;
622
+ setPending(value);
623
+ };
624
+ const confirmSwitch = () => {
625
+ const value = pending;
626
+ setPending(null);
627
+ if (value !== null) {
628
+ void controller.setEngine(value);
629
+ }
630
+ };
631
+ const cancelSwitch = () => {
632
+ setPending(null);
633
+ };
634
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-labelledby": navId, style: shell, children: [
635
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: navId, style: titleStyle, children: t("nav") }),
636
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: intro, children: t("description") }),
637
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
638
+ import_dsh_client_ui_primitives.Menu,
639
+ {
640
+ open,
641
+ onClose: () => {
642
+ setOpen(false);
643
+ },
644
+ items: ENGINE_OPTIONS.map((option) => ({ id: option.value, label: t(option.key) })),
645
+ selectedId: engine,
646
+ onSelect,
647
+ align: "start",
648
+ portal: true,
649
+ getAnchorRect: () => triggerRef.current?.getBoundingClientRect() ?? null,
650
+ anchor: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
651
+ "button",
652
+ {
653
+ type: "button",
654
+ ref: triggerRef,
655
+ "aria-haspopup": "menu",
656
+ "aria-expanded": open,
657
+ disabled,
658
+ style: disabled ? triggerDisabled : trigger,
659
+ onClick: () => {
660
+ setOpen(!open);
661
+ },
662
+ children: [
663
+ label,
664
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconChevronDown, { size: 14 })
665
+ ]
666
+ }
667
+ )
668
+ }
669
+ ),
670
+ 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") }),
671
+ isHostedEngine(engine) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: notice, children: t("hostedEngineModelNotice") }) : null,
672
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: toggleRow, children: [
673
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
674
+ "input",
675
+ {
676
+ type: "checkbox",
677
+ checked: showInComposer,
678
+ disabled,
679
+ style: toggleCheckbox,
680
+ onChange: (event) => {
681
+ void controller.setShowInComposer(event.currentTarget.checked);
682
+ }
683
+ }
684
+ ),
685
+ t("showInComposerLabel")
686
+ ] }),
687
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: toggleRow, children: [
688
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
689
+ "input",
690
+ {
691
+ type: "checkbox",
692
+ checked: showEngineBadge,
693
+ disabled,
694
+ style: toggleCheckbox,
695
+ onChange: (event) => {
696
+ void controller.setShowEngineBadge(event.currentTarget.checked);
697
+ }
698
+ }
699
+ ),
700
+ t("showEngineBadgeLabel")
701
+ ] }),
702
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
703
+ import_dsh_client_ui_primitives.Modal,
704
+ {
705
+ open: pending !== null,
706
+ onClose: cancelSwitch,
707
+ title: t("confirmTitle"),
708
+ closeLabel: t("closeLabel"),
709
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
710
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", onClick: cancelSwitch, children: t("cancelAction") }),
711
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", onClick: confirmSwitch, children: t("confirmAction") })
712
+ ] }),
713
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: confirmBody, children: t("confirmBody") })
714
+ }
715
+ )
716
+ ] });
717
+ }
718
+
719
+ // src/client/use-session-engine.ts
720
+ var import_react2 = require("react");
721
+
722
+ // src/client/turn-status.ts
723
+ var ENGINE_ATTR = "data-loop-engine";
724
+ var RUNNING_ATTR = "data-loop-engine-running";
725
+ var ROW_BUTTON = "button[data-turn-process]";
726
+ var RUNNING_ROW = "[data-chat-running]";
727
+ var LIVE_ATTR = "data-loop-engine-live";
728
+ var PLUGIN_ID = "dsh-loop-engine-allinone";
729
+ var RUNNING_ICON = `html[${ENGINE_ATTR}] ${RUNNING_ROW} [class$="_runningIcon"] { display: none; }`;
730
+ var ROW_SELECTORS = [
731
+ /** 0.1.5 line: the turn-status row, whose class name ends `_turnStatus`. */
732
+ '[class$="_turnStatus"]',
733
+ /** 0.1.7 line: the marked live turn-process button, and its label span. */
734
+ `${ROW_BUTTON}[${LIVE_ATTR}] [class$="_label"]`
735
+ ];
736
+ var GRADIENT_VARS = ["--dsw-static-deepseek-500", "--dsw-static-deepseek-200"];
737
+ var RUNNING_VARS = ["--dsw-alias-label-deep-diving", "--dsw-alias-label-deep-diving-shimmer"];
738
+ var ENGINE_PAINT = [
739
+ ["claude-code", {
740
+ colour: "#d97757",
741
+ tint: "#f5bda6",
742
+ glyph: "\u273B",
743
+ extras: [],
744
+ animation: "le-bloom 1.6s ease-in-out infinite"
745
+ }],
746
+ ["codex", {
747
+ colour: "#a9b1c0",
748
+ tint: "#e6eaf2",
749
+ glyph: "\u2022",
750
+ extras: ["text-shadow: 0 0 6px currentColor"],
751
+ animation: "le-pulse 1.4s ease-in-out infinite"
752
+ }],
753
+ ["pi", {
754
+ colour: "#8e4ec6",
755
+ tint: "#d6bff0",
756
+ glyph: "\u280B",
757
+ extras: ["font-size: 1.1em"],
758
+ animation: "le-braille 1s linear infinite"
759
+ }],
760
+ ["kimi", {
761
+ colour: "#e5484d",
762
+ tint: "#f5b2b4",
763
+ glyph: "\u{1F317}",
764
+ extras: [],
765
+ animation: "le-moon 2.5s linear infinite"
766
+ }],
767
+ // ── The engines added by the registry fork ──────────────────────────────
768
+ // Their animation must be a TRANSFORM/OPACITY one (`le-bloom`, `le-pulse`,
769
+ // `le-flicker`, `le-drift`): `le-braille` and `le-moon` step `content`, so
770
+ // handing one of those to a monogram would replace the letter every frame.
771
+ ["agy", {
772
+ // Antigravity: the arch from its own app icon, in the blue that icon leads
773
+ // with. The row glyph is a CHARACTER — a `::before` cannot carry a path — so
774
+ // it is the letter the arch reads as.
775
+ colour: "#4285f4",
776
+ tint: "#a8c4fb",
777
+ glyph: "A",
778
+ extras: ["font-weight: 700"],
779
+ animation: "le-bloom 1.8s ease-in-out infinite"
780
+ }],
781
+ ["zcode", {
782
+ // ZCode / GLM: a "Z", in the teal this family tends to use. The flicker
783
+ // reads as an electric charge rather than a heartbeat.
784
+ colour: "#0d9488",
785
+ tint: "#8fd8d2",
786
+ glyph: "Z",
787
+ extras: ["font-weight: 700"],
788
+ animation: "le-flicker 1.9s steps(1, end) infinite"
789
+ }],
790
+ ["qoderclicn", {
791
+ // Qoder CN: its mark is a green disc split by a white crescent, so the colour
792
+ // is the vendor's own green rather than a hue picked for contrast. The glyph
793
+ // is the vendor's initial.
794
+ colour: "#16a34a",
795
+ tint: "#a7e8bd",
796
+ glyph: "Q",
797
+ extras: ["font-weight: 700"],
798
+ animation: "le-pulse 1.6s ease-in-out infinite"
799
+ }],
800
+ ["hermes", {
801
+ // Hermes Agent: the psi, which drifts — a messenger that does not stand
802
+ // still. Amber keeps it clear of Claude's terracotta and Kimi's red.
803
+ colour: "#d97706",
804
+ tint: "#f3cd94",
805
+ glyph: "\u03A8",
806
+ extras: ["font-weight: 700"],
807
+ animation: "le-drift 2.2s ease-in-out infinite"
808
+ }],
809
+ ["grok", {
810
+ // Grok Build: a monochrome black mark on a white page, so the row is
811
+ // deliberately colourless-ish — the neutral near-black xAI leads with — and
812
+ // the flicker reads as the charged edge of the mark's own diagonal.
813
+ colour: "#1f2328",
814
+ tint: "#c9ced6",
815
+ glyph: "G",
816
+ extras: ["font-weight: 700"],
817
+ animation: "le-flicker 2.1s steps(1, end) infinite"
818
+ }],
819
+ ["minimaxcode", {
820
+ // MiniMax Code: a white mark on the brand's blue tile, so the row is that
821
+ // blue. The pulse is the tile's own breathing rhythm.
822
+ colour: "#0f7bf0",
823
+ tint: "#a9d4ff",
824
+ glyph: "M",
825
+ extras: ["font-weight: 700"],
826
+ animation: "le-pulse 1.7s ease-in-out infinite"
827
+ }]
828
+ ];
829
+ var engineRules = (properties, glyph, vars) => ENGINE_PAINT.map(([engine, paint]) => `${properties(engine)} {
830
+ ${vars[0]}: ${paint.colour};
831
+ ${vars[1]}: ${paint.tint};
832
+ }
833
+ ${glyph(engine)}::before {
834
+ content: "${paint.glyph}";
835
+ color: ${paint.colour};
836
+ -webkit-text-fill-color: ${paint.colour};
837
+ ${paint.extras.map((extra) => ` ${extra};
838
+ `).join("")} animation: ${paint.animation};
839
+ }`).join("\n\n");
840
+ var GLYPH_KEYFRAMES = `/* Grows from small to large each cycle \u2014 the opposite of a twinkle. The range
841
+ is deliberately wide (3x) because a bare size change on a thin glyph reads
842
+ weakly otherwise, and the large state is held briefly (50%-62%) so it blooms
843
+ rather than throbs. 1.35x extends ~2.5px per side at the 14px glyph, inside
844
+ the 6px ::before margin. */
845
+ @keyframes le-bloom {
846
+ 0%, 100% { transform: scale(0.45); opacity: 0.45; }
847
+ 50%, 62% { transform: scale(1.35); opacity: 1; }
848
+ }
849
+ /* A terminal braille spinner. The glyph is an ordinary text character, not an
850
+ emoji, so the engine color actually paints \u2014 a colored emoji silently ignores
851
+ color/-webkit-text-fill-color. Stepping content walks the ten dot frames. */
852
+ @keyframes le-braille {
853
+ 0% { content: "\u280B"; }
854
+ 10% { content: "\u2819"; }
855
+ 20% { content: "\u2839"; }
856
+ 30% { content: "\u2838"; }
857
+ 40% { content: "\u283C"; }
858
+ 50% { content: "\u2834"; }
859
+ 60% { content: "\u2826"; }
860
+ 70% { content: "\u2827"; }
861
+ 80% { content: "\u2807"; }
862
+ 90% { content: "\u280F"; }
863
+ 100% { content: "\u280B"; }
864
+ }
865
+ /* A soft pulse for the codex dot: the glyph breathes between a small, dim
866
+ point and a larger, full-brightness one \u2014 a light dot, not a spinner. */
867
+ @keyframes le-pulse {
868
+ 0%, 100% { transform: scale(0.5); opacity: 0.35; }
869
+ 50% { transform: scale(1.3); opacity: 1; }
870
+ }
871
+ /* Two-step flicker for an engine whose mark should read as a charge rather than
872
+ a breath: full, dropped hard, back. The steps timing function holds each state
873
+ instead of easing between them, which is what makes it read as flicker rather
874
+ than as the pulse above. */
875
+ @keyframes le-flicker {
876
+ 0% { opacity: 1; }
877
+ 55% { opacity: 0.25; }
878
+ 62% { opacity: 1; }
879
+ 100% { opacity: 1; }
880
+ }
881
+ /* A slow vertical drift: the mark rises and settles. Small on purpose (2px) \u2014
882
+ the glyph sits on a text baseline, and anything larger reads as a layout
883
+ glitch rather than a motion. */
884
+ @keyframes le-drift {
885
+ 0%, 100% { transform: translateY(1.5px); opacity: 0.6; }
886
+ 50% { transform: translateY(-1.5px); opacity: 1; }
887
+ }
888
+ /* Moon phases rather than a rigid rotation: a spinning moon bitmap can only
889
+ squash and mirror itself, never show a full or a new moon. Stepping the
890
+ glyph through the phase set sweeps the lit edge across the disc AND actually
891
+ reaches \u{1F315} and \u{1F311}. The order runs waning first (full \u2192 new \u2192 full), so the lit
892
+ edge travels counter-clockwise, and the glyph under the row's first paint
893
+ (\u{1F317}) is a mid-phase rather than a full or a new moon. */
894
+ @keyframes le-moon {
895
+ 0% { content: "\u{1F315}"; }
896
+ 12.5% { content: "\u{1F316}"; }
897
+ 25% { content: "\u{1F317}"; }
898
+ 37.5% { content: "\u{1F318}"; }
899
+ 50% { content: "\u{1F311}"; }
900
+ 62.5% { content: "\u{1F312}"; }
901
+ 75% { content: "\u{1F313}"; }
902
+ 87.5% { content: "\u{1F314}"; }
903
+ 100% { content: "\u{1F315}"; }
904
+ }`;
905
+ var sheetFor = (ROW) => {
906
+ const GATE = `html[${ENGINE_ATTR}][${RUNNING_ATTR}]`;
907
+ const PER_ENGINE = (engine) => `html[${ENGINE_ATTR}="${engine}"][${RUNNING_ATTR}] ${ROW}`;
908
+ return `
909
+ ${GATE} ${ROW}::before {
910
+ margin-right: 6px;
911
+ background: none;
912
+ -webkit-background-clip: border-box;
913
+ background-clip: border-box;
914
+ }
915
+
916
+ /*
917
+ * The engine-colored sweep \u2014 and, on the 0.1.7 line, the sweep itself.
918
+ *
919
+ * The 0.1.5 row paints its own gradient (a \`linear-gradient\` over
920
+ * --dsw-static-deepseek-*, clipped to the text) and only needs its animation
921
+ * re-asserted; this rule declares that same gradient so the 0.1.7 row, whose
922
+ * turn-process button is plain tertiary text, gets the sweep back. Either way
923
+ * the per-engine override below recolors it by swapping the two custom
924
+ * properties. ui-chat disables the animation under
925
+ * \`prefers-reduced-motion: reduce\`, and a media query carries no specificity \u2014
926
+ * so this attribute-gated rule outranks it. That is deliberate here:
927
+ * deployment images ship with Windows' client-area animation off
928
+ * (SPI_GETCLIENTAREAANIMATION false), and with the guard in force EVERY
929
+ * indicator on this row is frozen \u2014 the sweep and the glyph alike. Scoped to
930
+ * hosted engines, so in-process sessions keep the stock reduced-motion
931
+ * behaviour; delete this rule and restore the guard at the foot of the sheet to
932
+ * hand the decision back to the OS.
933
+ */
934
+ ${GATE} ${ROW} {
935
+ background: linear-gradient(90deg,
936
+ var(--dsw-static-deepseek-500) 0%,
937
+ var(--dsw-static-deepseek-500) 40%,
938
+ var(--dsw-static-deepseek-200) 50%,
939
+ var(--dsw-static-deepseek-500) 60%,
940
+ var(--dsw-static-deepseek-500) 100%);
941
+ color: #0000;
942
+ -webkit-text-fill-color: transparent;
943
+ -webkit-background-clip: text;
944
+ background-clip: text;
945
+ background-position: 100% 0;
946
+ background-size: 250% 100%;
947
+ animation: le-shimmer 1.8s linear infinite;
948
+ }
949
+
950
+ @keyframes le-shimmer {
951
+ to { background-position: 0 0; }
952
+ }
953
+
954
+ ${engineRules(PER_ENGINE, PER_ENGINE, GRADIENT_VARS)}
955
+
956
+ ${GLYPH_KEYFRAMES}
957
+
958
+ `;
959
+ };
960
+ var sheetForRunning = () => {
961
+ const PER_ENGINE = (engine) => `html[${ENGINE_ATTR}="${engine}"] ${RUNNING_ROW}`;
962
+ const GLYPH = (engine) => `${PER_ENGINE(engine)} [class$="_runningContent"]`;
963
+ return `
964
+ ${engineRules(PER_ENGINE, GLYPH, RUNNING_VARS)}
965
+
966
+ ${RUNNING_ICON}
967
+
968
+ ${GLYPH_KEYFRAMES}
969
+
970
+ `;
971
+ };
972
+ var STYLESHEET = [...ROW_SELECTORS.map(sheetFor), sheetForRunning()].join("\n");
973
+ var focusedSessionId;
974
+ function reflectTurnStatusEngine(sessionId, engine, running) {
975
+ if (sessionId === void 0 || sessionId !== focusedSessionId) return;
976
+ writeTurnStatusEngine(engine);
977
+ if (running !== void 0) writeTurnStatusRunning(running);
978
+ }
979
+ function focusTurnStatusSession(sessionId) {
980
+ focusedSessionId = sessionId;
981
+ }
982
+ function blurTurnStatusSession(sessionId) {
983
+ if (focusedSessionId === sessionId) focusedSessionId = void 0;
984
+ }
985
+ function writeTurnStatusEngine(engine) {
986
+ if (typeof document === "undefined") return;
987
+ const root = document.documentElement;
988
+ if (engine === void 0 || engine === "in-process") {
989
+ delete root.dataset.loopEngine;
990
+ return;
991
+ }
992
+ root.dataset.loopEngine = engine;
993
+ }
994
+ function writeTurnStatusRunning(running) {
995
+ if (typeof document === "undefined") return;
996
+ const root = document.documentElement;
997
+ if (!running) {
998
+ delete root.dataset.loopEngineRunning;
999
+ stopLiveRowWatch();
1000
+ return;
1001
+ }
1002
+ root.dataset.loopEngineRunning = "";
1003
+ startLiveRowWatch();
1004
+ }
1005
+ var liveRow;
1006
+ var liveRowObserver;
1007
+ function markLiveRow() {
1008
+ const live = document.querySelector(RUNNING_ROW) !== null ? void 0 : Array.from(document.querySelectorAll(ROW_BUTTON)).at(-1);
1009
+ if (live === liveRow) return;
1010
+ liveRow?.removeAttribute(LIVE_ATTR);
1011
+ liveRow = live;
1012
+ live?.setAttribute(LIVE_ATTR, "");
1013
+ }
1014
+ function bringsRow(node) {
1015
+ if (node.nodeType !== 1) return false;
1016
+ const element = node;
1017
+ return [ROW_BUTTON, RUNNING_ROW].some((selector) => element.matches(selector) || element.querySelector(selector) !== null);
1018
+ }
1019
+ function startLiveRowWatch() {
1020
+ markLiveRow();
1021
+ if (liveRowObserver !== void 0) return;
1022
+ liveRowObserver = new MutationObserver((records) => {
1023
+ const rowTouched = records.some((record) => Array.from(record.addedNodes).some(bringsRow) || Array.from(record.removedNodes).some(bringsRow));
1024
+ if (rowTouched) markLiveRow();
1025
+ });
1026
+ liveRowObserver.observe(document.body ?? document.documentElement, { childList: true, subtree: true });
1027
+ }
1028
+ function stopLiveRowWatch() {
1029
+ liveRowObserver?.disconnect();
1030
+ liveRowObserver = void 0;
1031
+ liveRow?.removeAttribute(LIVE_ATTR);
1032
+ liveRow = void 0;
1033
+ }
1034
+ function installTurnStatusStyles(ctx) {
1035
+ if (typeof document === "undefined") return;
1036
+ ctx.effect(() => {
1037
+ const tag = document.createElement("style");
1038
+ tag.dataset.plugin = PLUGIN_ID;
1039
+ tag.dataset.pluginCss = `${PLUGIN_ID}/turn-status.css`;
1040
+ tag.textContent = STYLESHEET;
1041
+ document.head.appendChild(tag);
1042
+ return () => {
1043
+ tag.remove();
1044
+ delete document.documentElement.dataset.loopEngine;
1045
+ delete document.documentElement.dataset.loopEngineRunning;
1046
+ stopLiveRowWatch();
1047
+ };
1048
+ }, "loop-engine: per-engine turn status styles");
1049
+ }
1050
+
1051
+ // src/client/use-session-engine.ts
1052
+ function useEngineOfSession(cache, sessionId, running) {
1053
+ const [, bump] = (0, import_react2.useState)(0);
1054
+ const report = sessionId === void 0 ? void 0 : cache.read(sessionId);
1055
+ (0, import_react2.useEffect)(() => {
1056
+ if (sessionId === void 0) return;
1057
+ return cache.watch(sessionId, () => {
1058
+ bump((count) => count + 1);
1059
+ });
1060
+ }, [cache, sessionId]);
1061
+ const engine = report === void 0 ? void 0 : hostedEngineOf(report.engine);
1062
+ (0, import_react2.useEffect)(() => {
1063
+ if (sessionId === void 0) return;
1064
+ focusTurnStatusSession(sessionId);
1065
+ reflectTurnStatusEngine(sessionId, engine, running);
1066
+ return () => {
1067
+ blurTurnStatusSession(sessionId);
1068
+ };
1069
+ }, [sessionId, engine, running]);
1070
+ return report;
1071
+ }
1072
+
1073
+ // src/client/LoopEngineBadge.tsx
1074
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1075
+ var pill = {
1076
+ display: "inline-flex",
1077
+ alignItems: "center",
1078
+ gap: 4,
1079
+ padding: "2px 8px",
1080
+ border: "1px solid var(--dsw-alias-border-l2)",
1081
+ borderRadius: 999,
1082
+ background: "var(--dsw-alias-bg-layer-1)",
1083
+ color: "var(--dsw-alias-label-secondary)",
1084
+ fontSize: 12,
1085
+ lineHeight: "18px",
1086
+ whiteSpace: "nowrap"
1087
+ };
1088
+ function LoopEngineBadge(props) {
1089
+ const { sessionId, sessionEngines, useSession, useSnapshot, t } = props;
1090
+ const running = useSession?.((snapshot) => snapshot.running);
1091
+ const report = useEngineOfSession(sessionEngines, sessionId, running);
1092
+ const showEngineBadge = useSnapshot((snapshot) => snapshot.showEngineBadge);
1093
+ if (!showEngineBadge) return null;
1094
+ if (sessionId === void 0 || report === void 0) return null;
1095
+ const engine = report.engine;
1096
+ if (engine.kind === "unset") return null;
1097
+ const label = t(engineStateLabelKey(engine));
1098
+ const named = engine.kind === "engine" ? engine.engine : void 0;
1099
+ const pending = report.pending;
1100
+ const notice2 = pending === void 0 ? named === void 0 ? t("legacySessionNotice") : t("sessionNotice") : t("pendingSessionNotice");
1101
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: pill, title: notice2, children: [
1102
+ t("nav"),
1103
+ " \xB7 ",
1104
+ label,
1105
+ pending === void 0 ? null : ` \xB7 ${pendingEngineText(t, pending)}`
1106
+ ] });
1107
+ }
1108
+
1109
+ // src/client/LoopEngineComposerSelect.tsx
1110
+ var import_react3 = require("react");
1111
+ var primitives2 = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
1112
+
1113
+ // src/client/engine-marks.ts
1114
+ function markPaint(mark, colour) {
1115
+ return mark.strokeWidth === void 0 ? { mode: "fill", colour } : { mode: "stroke", colour, width: mark.strokeWidth };
1116
+ }
1117
+ var ENGINE_MARKS = {
1118
+ "claude-code": {
1119
+ // Simple Icons' Claude Code mark (the vendor's own asterisk).
1120
+ fillRule: "nonzero",
1121
+ viewBox: "0 0 24 24",
1122
+ d: "M21 10.5h3v3h-3v3h-1.5v3H18v-3h-1.5v3H15v-3H9v3H7.5v-3H6v3H4.5v-3H3v-3H0v-3h3v-6h18Zm-15 0h1.5v-3H6Zm10.5 0H18v-3h-1.5z"
1123
+ },
1124
+ codex: {
1125
+ // OpenAI's blossom — codex has no distinct vector mark of its own.
1126
+ fillRule: "nonzero",
1127
+ viewBox: "0 0 256 260",
1128
+ d: "M239.183914,106.202783 C245.054304,88.5242096 243.02228,69.1733805 233.607599,53.0998864 C219.451678,28.4588021 190.999703,15.7836129 163.213007,21.739505 C147.554077,4.32145883 123.794909,-3.42398554 100.87901,1.41873898 C77.9631105,6.26146349 59.3690093,22.9572536 52.0959621,45.2214219 C33.8436494,48.9644867 18.0901721,60.392749 8.86672513,76.5818033 C-5.443491,101.182962 -2.19544431,132.215255 16.8986662,153.320094 C11.0060865,170.990656 13.0197283,190.343991 22.4238231,206.422991 C36.5975553,231.072344 65.0680342,243.746566 92.8695738,237.783372 C105.235639,251.708249 123.001113,259.630942 141.623968,259.52692 C170.105359,259.552169 195.337611,241.165718 204.037777,214.045661 C222.28734,210.296356 238.038489,198.869783 247.267014,182.68528 C261.404453,158.127515 258.142494,127.262775 239.183914,106.202783 L239.183914,106.202783 Z M141.623968,242.541207 C130.255682,242.559177 119.243876,238.574642 110.519381,231.286197 L112.054146,230.416496 L163.724595,200.590881 C166.340648,199.056444 167.954321,196.256818 167.970781,193.224005 L167.970781,120.373788 L189.815614,133.010026 C190.034132,133.121423 190.186235,133.330564 190.224885,133.572774 L190.224885,193.940229 C190.168603,220.758427 168.442166,242.484864 141.623968,242.541207 Z M37.1575749,197.93062 C31.456498,188.086359 29.4094818,176.546984 31.3766237,165.342426 L32.9113895,166.263285 L84.6329973,196.088901 C87.2389349,197.618207 90.4682717,197.618207 93.0742093,196.088901 L156.255402,159.663793 L156.255402,184.885111 C156.243557,185.149771 156.111725,185.394602 155.89729,185.550176 L103.561776,215.733903 C80.3054953,229.131632 50.5924954,221.165435 37.1575749,197.93062 Z M23.5493181,85.3811273 C29.2899861,75.4733097 38.3511911,67.9162648 49.1287482,64.0478825 L49.1287482,125.438515 C49.0891492,128.459425 50.6965386,131.262556 53.3237748,132.754232 L116.198014,169.025864 L94.3531808,181.662102 C94.1132325,181.789434 93.8257461,181.789434 93.5857979,181.662102 L41.3526015,151.529534 C18.1419426,138.076098 10.1817681,108.385562 23.5493181,85.125333 L23.5493181,85.3811273 Z M203.0146,127.075598 L139.935725,90.4458545 L161.7294,77.8607748 C161.969348,77.7334434 162.256834,77.7334434 162.496783,77.8607748 L214.729979,108.044502 C231.032329,117.451747 240.437294,135.426109 238.871504,154.182739 C237.305714,172.939368 225.050719,189.105572 207.414262,195.67963 L207.414262,134.288998 C207.322521,131.276867 205.650697,128.535853 203.0146,127.075598 Z M224.757116,94.3850867 L223.22235,93.4642272 L171.60306,63.3828173 C168.981293,61.8443751 165.732456,61.8443751 163.110689,63.3828173 L99.9806554,99.8079259 L99.9806554,74.5866077 C99.9533004,74.3254088 100.071095,74.0701869 100.287609,73.9215426 L152.520805,43.7889738 C168.863098,34.3743518 189.174256,35.2529043 204.642579,46.0434841 C220.110903,56.8340638 227.949269,75.5923959 224.757116,94.1804513 L224.757116,94.3850867 Z M88.0606409,139.097931 L66.2158076,126.512851 C65.9950399,126.379091 65.8450965,126.154176 65.8065367,125.898945 L65.8065367,65.684966 C65.8314495,46.8285367 76.7500605,29.6846032 93.8270852,21.6883055 C110.90411,13.6920079 131.063833,16.2835462 145.5632,28.338998 L144.028434,29.2086986 L92.3579852,59.0343142 C89.7419327,60.5687513 88.1282597,63.3683767 88.1117998,66.4011901 L88.0606409,139.097931 Z M99.9294965,113.5185 L128.06687,97.3011417 L156.255402,113.5185 L156.255402,145.953218 L128.169187,162.170577 L99.9806554,145.953218 L99.9294965,113.5185 Z"
1129
+ },
1130
+ pi: {
1131
+ // pi.dev's official logo.
1132
+ viewBox: "0 0 800 800",
1133
+ fillRule: "evenodd",
1134
+ d: "M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z M517.36 400H634.72V634.72H517.36Z"
1135
+ },
1136
+ kimi: {
1137
+ // Rebuilt from the vendor's app tile as the user pointed it out: a bold "K"
1138
+ // whose two arms are buried IN the stem (they must overlap it, or their
1139
+ // perpendicular end cuts leave a wedge-shaped notch at the joint), plus the
1140
+ // brand's blue dot as an accent.
1141
+ fillRule: "nonzero",
1142
+ viewBox: "0 0 24 24",
1143
+ d: "M4.6,3.9 L7.5,3.9 L7.5,20.1 L4.6,20.1ZM4.71974,10.6858 L14.7197,2.78575 L16.4803,5.01425 L6.48026,12.9142ZM6.48026,11.0858 L16.4803,18.9858 L14.7197,21.2142 L4.71974,13.3142Z",
1144
+ accent: {
1145
+ d: "M19.4 5.9a2.0 2.0 0 1 0 0 4.0a2.0 2.0 0 1 0 0-4.0Z",
1146
+ fill: "#1d7ffb"
1147
+ }
1148
+ },
1149
+ agy: {
1150
+ // Traced from the vendor's own app icon: Antigravity's arch, the shape that
1151
+ viewBox: "0 0 24 24",
1152
+ d: "M11.24,2.58 L12.76,2.58 L13.84,3.01 L14.71,3.66 L15.79,5.18 L17.74,10.81 L18.83,15.14 L19.91,17.53 L22.29,20.56 L22.29,21.21 L21.86,21.43 L20.99,21.43 L19.91,20.78 L18.18,19.04 L15.57,14.71 L14.06,13.19 L12.76,12.76 L11.89,12.54 L9.94,13.19 L8.21,15.14 L6.04,18.83 L3.88,20.99 L3.01,21.43 L1.71,21.21 L1.93,20.34 L3.01,19.26 L4.96,15.79 L6.91,8.86 L8.43,4.96 L10.16,3.01Z"
1153
+ },
1154
+ zcode: {
1155
+ // Measured from the vendor tile (a dark rounded square with the "Z" knocked
1156
+ // out): one thick diagonal band plus two detached horizontal ticks. Written by
1157
+ // hand from sampled mask spans because a raster trace slurred the tick ends
1158
+ // and skewed the letter. Geometry in `data/trace-icons.py` `HAND_FIT`.
1159
+ viewBox: "0 0 24 24",
1160
+ d: "M14.16,2.77 L22.76,2.77 L9.76,21.23 L1.2,21.23ZM1.71,2.77 L12.47,2.77 L10.31,5.9 L1.71,5.9ZM13.82,18.01 L22.25,18.01 L22.25,21.23 L11.62,21.23Z"
1161
+ },
1162
+ qoderclicn: {
1163
+ // Traced at the artwork's native 256px from the icon embedded in
1164
+ // `qoderclicn.exe`: two interlocking rounded forms with the white crescent
1165
+ // between them — the part a coarse trace loses.
1166
+ //
1167
+ // Rendered as the vendor's OUTLINE variant (stroked, not filled): the app
1168
+ // tile uses a solid green fill, while the monochrome mark is a line drawing,
1169
+ // and the outline is what matches the other marks here.
1170
+ fillRule: "evenodd",
1171
+ viewBox: "0 0 24 24",
1172
+ strokeWidth: 1.75,
1173
+ d: "M9.6,1.64 L10.82,1.72 L10.9,1.8 L11.47,1.88 L11.96,2.13 L12.04,2.29 L12.53,2.53 L12.69,2.53 L13.34,2.86 L13.67,3.1 L13.99,3.18 L14.07,3.35 L14.72,3.67 L14.88,3.67 L15.05,3.83 L15.86,4.24 L16.18,4.32 L16.27,4.48 L16.51,4.57 L16.67,4.73 L17,4.89 L17.16,4.89 L18.05,5.38 L18.22,5.38 L18.22,5.46 L18.38,5.62 L19.19,6.03 L19.35,6.03 L19.68,6.19 L19.84,6.35 L20.08,6.43 L20.33,6.76 L20.49,6.76 L20.98,7.25 L20.98,7.33 L21.22,7.57 L21.22,7.73 L21.38,7.9 L21.38,7.98 L21.55,8.06 L21.79,8.63 L21.79,8.87 L21.87,8.95 L21.87,9.2 L21.95,9.28 L21.95,9.6 L22.03,9.68 L22.03,20.9 L21.95,20.98 L21.95,21.14 L21.55,21.63 L21.3,21.71 L21.22,21.87 L21.06,21.87 L20.98,21.95 L20.17,21.95 L20.08,21.87 L19.92,21.87 L19.84,21.71 L19.35,21.47 L18.95,21.38 L18.3,21.06 L18.22,21.14 L17.89,21.22 L17.73,21.38 L17.24,21.63 L17,21.63 L16.83,21.71 L16.75,21.87 L16.18,22.03 L16.02,22.12 L15.37,22.2 L15.29,22.28 L14.88,22.28 L14.8,22.36 L13.58,22.36 L13.5,22.28 L13.1,22.28 L12.77,22.12 L12.28,22.03 L11.96,21.87 L11.96,21.79 L11.8,21.63 L11.47,21.47 L11.31,21.47 L10.42,20.98 L10.25,20.98 L10.09,20.82 L10.01,20.82 L9.93,20.65 L9.28,20.33 L9.12,20.33 L8.14,19.76 L7.82,19.68 L7.73,19.52 L7.08,19.19 L6.92,19.19 L6.76,19.03 L5.95,18.62 L5.78,18.62 L5.7,18.46 L5.46,18.38 L5.3,18.22 L4.81,17.97 L4.65,17.97 L3.92,17.57 L3.75,17.32 L3.59,17.32 L3.02,16.75 L3.02,16.67 L2.78,16.43 L2.78,16.27 L2.62,15.94 L2.45,15.86 L2.21,15.29 L2.21,15.05 L2.13,14.97 L2.13,14.72 L2.05,14.64 L2.05,14.15 L1.97,14.07 L1.97,9.2 L2.05,9.12 L2.05,8.55 L2.13,8.47 L2.29,7.57 L2.37,7.49 L2.45,7.08 L2.62,7 L2.78,6.68 L2.78,6.43 L2.86,6.27 L3.02,6.11 L3.51,5.13 L3.83,4.89 L3.83,4.81 L4.24,4.4 L4.32,4.16 L4.65,3.83 L4.81,3.83 L5.3,3.35 L5.38,3.35 L5.54,3.1 L5.62,3.1 L5.78,2.94 L5.95,2.94 L6.35,2.62 L6.84,2.37 L7.08,2.37 L7.25,2.29 L7.33,2.13 L7.49,2.13 L7.82,1.97 L8.14,1.97 L8.22,1.88 L8.79,1.8 L8.87,1.72 L9.52,1.72ZM13.99,6.84 L14.15,6.84 L14.15,10.33 L14.07,10.42 L14.07,11.15 L13.99,11.23 L13.99,11.55 L13.91,11.63 L13.91,11.88 L13.83,11.96 L13.75,12.45 L13.58,12.53 L13.42,13.18 L13.1,13.83 L12.93,13.99 L12.85,14.32 L12.69,14.48 L12.69,14.56 L12.37,14.8 L12.37,14.88 L12.12,15.13 L12.12,15.29 L11.47,15.94 L11.39,15.94 L11.07,16.27 L10.98,16.27 L10.74,16.59 L10.66,16.59 L10.42,16.83 L10.25,16.83 L9.85,17.08 L9.77,17.16 L9.77,13.18 L9.85,13.1 L9.85,12.61 L9.93,12.53 L10.01,11.88 L10.09,11.8 L10.17,11.39 L10.25,11.23 L10.42,11.15 L10.42,10.9 L10.82,10.09 L10.98,9.93 L11.07,9.6 L11.23,9.44 L11.23,9.36 L11.55,9.12 L11.55,9.03 L11.8,8.79 L11.88,8.55 L12.53,7.9 L12.85,7.73 L13.18,7.33 L13.26,7.33 L13.67,7 L13.83,7Z"
1174
+ },
1175
+ hermes: {
1176
+ // Traced from the vendor's desktop icon in LINE-ART form (its counters left
1177
+ // open). NOTE: that artwork is a detailed character portrait, not a geometric
1178
+ // mark, so this is the original's own strokes rather than a designed glyph.
1179
+ // The filled-silhouette alternative was measured too and lost the face at
1180
+ // 64px, which is the size that decides whether the mark reads at all; a
1181
+ // purpose-drawn simplification would beat both if one becomes available.
1182
+ fillRule: "evenodd",
1183
+ viewBox: "0 0 24 24",
1184
+ d: "M8.83,2.33 L10.13,2.33 L11.76,3.14 L12.24,3.63 L12.89,4.77 L12.73,4.77 L12.57,5.09 L12.73,5.26 L13.06,5.09 L13.38,6.72 L13.54,6.88 L13.87,6.72 L13.71,6.39 L14.19,6.39 L14.36,6.23 L14.52,6.39 L14.68,6.23 L14.36,5.09 L15.01,5.42 L15.33,5.26 L15.17,5.09 L15.66,5.26 L15.98,5.09 L16.47,5.42 L16.96,4.77 L16.96,4.12 L17.12,4.28 L16.96,4.44 L17.28,4.61 L18.09,6.72 L18.58,9.48 L18.74,9.64 L18.74,10.29 L18.91,10.46 L18.91,10.94 L19.72,13.71 L19.72,14.36 L19.88,14.52 L19.88,16.79 L19.23,18.26 L19.39,18.42 L20.37,17.12 L20.69,15.98 L20.53,14.84 L20.86,15.66 L20.86,16.96 L20.37,18.09 L18.58,19.56 L18.42,20.37 L17.93,19.23 L16.79,18.09 L16.47,17.93 L15.17,17.93 L14.52,18.26 L13.22,19.56 L12.73,20.53 L12.41,22.32 L12.24,22.32 L12.41,21.34 L12.57,21.18 L12.89,19.39 L14.19,18.09 L14.03,17.93 L13.71,18.09 L13.38,17.93 L11.59,19.07 L11.27,19.23 L11.11,19.07 L8.34,22.32 L8.18,21.99 L7.86,22.32 L6.07,22.32 L6.39,21.67 L6.23,21.51 L6.07,21.51 L5.42,22.32 L5.09,22.32 L5.09,21.83 L5.42,21.34 L7.21,19.39 L7.37,18.58 L7.21,18.42 L7.04,18.74 L6.56,19.07 L5.74,19.07 L4.93,18.42 L4.77,17.93 L3.96,17.61 L3.14,16.47 L3.31,14.52 L3.47,14.19 L3.47,15.17 L4.12,15.82 L4.77,15.82 L5.26,15.33 L5.58,14.52 L5.74,12.41 L5.91,12.24 L6.07,10.13 L5.74,9.81 L5.42,9.97 L5.58,10.13 L4.77,9.81 L4.12,8.83 L4.12,8.18 L3.96,8.02 L4.12,7.86 L4.12,6.88 L4.44,5.91 L4.61,5.58 L4.61,6.07 L4.77,6.23 L5.09,6.07 L5.26,6.39 L5.42,6.07 L5.58,6.23 L6.23,6.07 L6.56,5.74 L6.23,5.42 L5.91,5.42 L6.07,5.26 L5.74,4.93 L5.42,5.09 L5.58,4.93 L5.26,4.77 L5.26,4.61 L7.04,3.14ZM10.94,1.68 L13.06,1.68 L13.22,1.84 L13.87,1.84 L15.82,2.82 L16.47,3.47 L16.31,3.63 L16.47,3.79 L16.47,4.28 L16.14,4.12 L15.66,4.77 L15.49,4.77 L15.01,4.77 L14.84,4.44 L14.68,4.77 L14.19,4.93 L14.19,4.61 L13.54,3.47 L12.41,2.33 L11.27,1.84 L10.13,1.84 L10.78,1.84ZM12.24,19.56 L11.59,21.18 L11.43,22.32 L10.78,22.32 L10.29,22.32 L10.46,21.99ZM8.99,8.83 L9.97,8.99 L10.78,9.48 L10.46,9.81 L10.13,9.32 L9.81,9.64 L9.81,9.97 L10.13,10.13 L9.81,10.29 L9.16,10.29 L9.64,10.29 L9.81,10.13 L9.16,9.81 L9.16,9.32 L8.99,9.16 L8.83,9.48 L8.67,9.48 L8.83,9.32 L8.67,9.16ZM11.27,17.12 L11.43,17.12 L11.76,18.09 L11.27,17.77 L10.94,17.93 L10.62,17.61 L11.11,17.61 L11.27,17.77 L11.43,17.61 L11.43,17.44 L11.11,17.28ZM6.88,12.41 L7.21,12.73 L7.86,12.73 L8.18,12.89 L8.18,13.06 L7.86,12.89 L7.37,13.06 L7.21,12.89 L7.04,13.22ZM7.37,8.51 L7.53,8.67 L8.34,8.67 L8.18,8.83 L8.34,8.99 L9.16,8.51 L9.64,8.51 L9.81,8.67 L11.11,8.67 L11.27,8.51 L12.08,8.99 L11.92,11.11 L11.76,11.27 L11.43,13.87 L11.27,14.03 L11.27,15.82 L11.27,15.98 L10.94,16.14 L10.62,15.49 L9.97,14.84 L7.86,15.01 L7.04,13.87 L6.39,12.24 L6.39,11.43 L6.72,11.27 L6.56,11.11 L7.21,10.46 L7.37,9.81 L6.88,9.48 L6.39,9.48 L6.23,9.16 L7.21,8.83ZM11.43,16.47 L12.89,17.77 L11.27,18.74 L10.94,18.91 L10.78,18.74 L10.78,18.42 L10.13,17.44 L10.46,17.44ZM10.62,4.44 L11.27,5.58 L11.11,5.91 L10.62,5.58 L10.29,5.74 L9.97,5.74 L9.64,5.58 L9.64,5.42 L10.13,5.26 L10.62,4.77ZM18.74,13.38 L19.07,14.36 L19.07,15.33 L18.74,16.14 L18.42,16.47 L18.74,15.49ZM7.04,4.93 L7.04,5.09 L7.53,5.42 L8.02,5.42 L8.34,4.93 L8.34,5.74 L8.18,5.58 L7.69,5.74 L7.21,5.74 L7.04,5.26 L6.72,5.74 L6.72,6.07 L6.56,5.58ZM8.51,20.21 L8.83,20.21 L8.18,21.51 L7.69,21.99 L7.86,21.02ZM12.08,3.96 L12.24,4.28 L11.92,4.61 L12.08,4.77 L11.59,5.09 L11.43,4.93 L11.11,4.77 L11.27,4.44ZM6.07,15.98 L6.07,16.31 L5.09,17.12 L4.28,17.12 L3.79,16.79 L4.12,16.96 L4.93,16.96 L5.58,16.63ZM6.88,16.96 L6.88,17.28 L6.23,18.26 L5.91,18.42 L5.26,18.26 L5.26,18.09 L6.23,17.93 L6.72,17.44ZM13.54,14.84 L13.54,15.33 L13.71,15.49 L13.38,15.82 L13.06,15.82 L12.89,15.82 L12.73,15.49ZM8.67,5.09 L9.32,5.09 L9.32,5.58 L8.67,5.58ZM13.38,13.06 L13.38,14.19 L12.89,14.52Z"
1185
+ },
1186
+ grok: {
1187
+ // Traced from the artwork the user supplied (102x98 PNG: the black ring and
1188
+ // its diagonal slash on a white page), at the 50% anti-aliased edge
1189
+ // (mask: luma < 128, grid 160, epsilon 1.4). The trace is
1190
+ // EIGHT-connected, which is load-bearing rather than cosmetic: the slash's
1191
+ // two tips are ~1px diagonals, and a 4-connected contour fragments them into
1192
+ // stubs the minimum-size filter then drops, silently lopping about 6% off
1193
+ // each tip. IoU against the source artwork: 0.909.
1194
+ //
1195
+ // The vendor's own ACP-registry icon (`data/vendor-icons/grok-build.svg`,
1196
+ // 16x16) is the SAME mark drawn about one source pixel BOLDER: it scores
1197
+ // 0.807 against this artwork under the same mask rule, and 0.901 against a
1198
+ // deliberately bolder iso-line, with the difference sitting uniformly on the
1199
+ // outer contour. The traced path is what matches the supplied artwork, so it
1200
+ // is the one shipped; the official path is recorded in
1201
+ // `docs/engine-registry.md` should a vendor-native vector be preferred later.
1202
+ fillRule: "evenodd",
1203
+ viewBox: "0 0 24 24",
1204
+ d: "M22.21,2.32 L22.34,2.45 L21.56,3.09 L21.56,3.62 L21.3,3.62 L21.17,4.13 L20.65,4.53 L20.65,5.3 L20.13,5.96 L20.13,9.21 L20.39,9.34 L20.39,10.38 L20.65,10.5 L20.65,13.11 L20.39,13.23 L20.39,14.28 L20.13,14.4 L19.87,15.71 L19.48,15.83 L18.7,17.4 L17.53,18.44 L17.14,18.44 L16.75,19.09 L16.23,19.09 L14.79,19.87 L13.75,19.87 L13.62,20.13 L10.12,19.87 L9.98,19.61 L9.21,19.61 L8.17,19.09 L7.9,18.57 L9.85,17.53 L11.15,17.79 L11.29,18.05 L13.62,18.05 L13.75,17.79 L14.79,17.79 L15.18,17.53 L17.4,15.71 L17.4,15.18 L17.92,14.79 L17.92,14.15 L18.31,14.02 L18.31,13.23 L18.57,13.11 L18.57,10.77 L18.31,10.63 L18.31,9.85 L17.92,9.72 L17.53,8.95 L15.45,9.98 L14.79,10.89 L14.28,10.89 L13.62,11.68 L13.23,11.68 L13.11,12.07 L12.32,12.32 L11.68,13.11 L11.29,13.11 L10.5,14.02 L10.38,13.88 L21.04,3.36 L21.04,3.09 L21.56,3.09ZM10.63,3.75 L13.88,3.75 L14.02,4.01 L15.05,4.01 L15.18,4.27 L16.48,4.53 L16.75,5.3 L16.23,5.3 L15.05,6.09 L14.28,6.09 L14.15,5.83 L10.63,5.83 L10.5,6.09 L9.21,6.35 L8.81,7 L8.43,7 L7.38,7.9 L7.38,8.43 L6.87,8.81 L6.35,9.85 L6.09,12.85 L6.35,12.97 L6.61,14.53 L7.12,14.93 L7.38,15.71 L7.78,15.83 L7.78,16.23 L5.7,17.79 L4.39,19.35 L3.62,19.61 L2.71,20.78 L1.67,21.56 L3.88,19.09 L4.13,18.31 L4.53,18.18 L4.79,16.88 L5.04,16.75 L4.53,13.88 L4.13,13.75 L4.13,10.12 L4.53,9.98 L4.53,9.08 L5.04,8.43 L5.3,7.38 L5.83,7 L5.83,6.47 L7,5.44 L7.78,5.18 L7.9,4.79 L9.6,4.01 L10.5,4.01Z"
1205
+ },
1206
+ minimaxcode: {
1207
+ // Traced from the artwork the user supplied (93x94 PNG: a white blob — two
1208
+ // ears, three lobes — on the brand's blue tile), grid 160, epsilon 1.4,
1209
+ // IoU 0.979.
1210
+ //
1211
+ // The mask rule is the interesting part: the tile is a GRADIENT (corner
1212
+ // rgb(0,161,253)), so a luminance or "not white" threshold swallows part of
1213
+ // the page and eats into the mark. The mark is the only thing whose DARKEST
1214
+ // channel is high, so the rule is `min(R,G,B) > 185`; the page's brightest
1215
+ // min-channel is 140, and the mask's bounding box stays >=9px inside the
1216
+ // frame, so no background can pass it.
1217
+ fillRule: "evenodd",
1218
+ viewBox: "0 0 24 24",
1219
+ d: "M10.77,1.67 L14.93,1.93 L15.05,2.32 L16.48,2.58 L18.05,3.75 L18.57,3.75 L18.7,4.13 L18.96,4.13 L19.09,3.75 L21.04,3.75 L21.17,4.13 L21.95,4.39 L21.69,5.56 L21.43,5.56 L21.3,5.18 L19.87,5.04 L20.39,6.09 L20.78,6.21 L21.3,7.9 L21.69,8.04 L21.69,9.21 L21.95,9.34 L21.69,13.5 L21.3,13.62 L21.04,15.05 L20.39,15.45 L20.39,15.97 L20.13,16.1 L20.78,16.62 L20.78,17.14 L21.3,17.92 L21.3,18.96 L21.04,19.09 L20.78,20.26 L19.87,20.78 L17.53,20.78 L17,20.26 L16.62,20.26 L15.83,19.22 L15.05,19.22 L14.67,19.73 L14.67,20.78 L14.28,20.91 L14.28,21.43 L13.75,22.08 L13.23,22.08 L13.11,22.34 L11.03,22.34 L10.5,22.08 L10.5,21.82 L9.85,21.43 L9.6,19.73 L9.21,19.61 L9.08,19.22 L8.69,19.22 L8.55,19.61 L8.04,19.61 L7.65,20.26 L6.74,20.78 L4.39,20.78 L3.88,20.26 L3.49,20.26 L3.49,19.73 L3.09,19.61 L3.09,18.83 L2.83,18.7 L3.09,17.27 L3.49,17.14 L3.49,16.62 L4.01,16.48 L4.01,16.1 L3.75,15.97 L3.49,14.79 L3.09,14.67 L3.09,14.15 L2.83,14.02 L2.58,12.07 L2.19,11.94 L2.19,9.98 L2.58,9.85 L2.83,7.52 L3.09,7.38 L3.09,6.87 L3.49,6.74 L3.49,6.21 L4.01,5.83 L4.01,5.04 L3.49,4.92 L2.19,5.56 L1.93,4.39 L3.49,3.49 L5.18,3.75 L5.44,4.13 L5.56,3.75 L7,3.23 L7.12,2.83 L7.65,2.83 L8.29,2.32 L9.08,2.32 L9.21,1.93 L10.63,1.93Z"
1220
+ }
1221
+ };
1222
+
1223
+ // src/client/LoopEngineComposerSelect.tsx
1224
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
1225
+
1226
+ // src/client/reload.ts
1227
+ var RELOAD_RETURN_KEY = "dsh-loop-engine-allinone:reload-session";
1228
+ function browserPage() {
1229
+ const globals = globalThis;
1230
+ return {
1231
+ stash: globals.sessionStorage,
1232
+ // Off a browser there is no page to reload; the stash is missing there too,
1233
+ // so nothing is ever armed for one.
1234
+ reload: () => {
1235
+ globals.location?.reload();
1236
+ }
1237
+ };
1238
+ }
1239
+ function armReloadReturn(page, sessionId) {
1240
+ const stash = page.stash;
1241
+ if (stash === void 0) return false;
1242
+ try {
1243
+ stash.setItem(RELOAD_RETURN_KEY, sessionId);
1244
+ return true;
1245
+ } catch {
1246
+ return false;
1247
+ }
1248
+ }
1249
+ function takeReloadReturn(page) {
1250
+ const stash = page.stash;
1251
+ if (stash === void 0) return void 0;
1252
+ let stashed = null;
1253
+ try {
1254
+ stashed = stash.getItem(RELOAD_RETURN_KEY);
1255
+ } catch {
1256
+ return void 0;
1257
+ }
1258
+ if (stashed === null || stashed.length === 0) return void 0;
1259
+ try {
1260
+ stash.removeItem(RELOAD_RETURN_KEY);
1261
+ } catch {
1262
+ }
1263
+ return stashed;
1264
+ }
1265
+ function restoreReloadReturn(sessions, page, warn) {
1266
+ const sessionId = takeReloadReturn(page);
1267
+ if (sessionId === void 0) return () => {
1268
+ };
1269
+ let unsubscribe;
1270
+ const settle = () => {
1271
+ const snapshot = sessions.list.getSnapshot();
1272
+ if (snapshot.phase !== "ready") return;
1273
+ unsubscribe?.();
1274
+ unsubscribe = void 0;
1275
+ if (!snapshot.ids.includes(sessionId)) {
1276
+ warn(`loop-engine: the session this page was reloaded for ("${sessionId}") is not in the session list; open it from the list`);
1277
+ return;
1278
+ }
1279
+ try {
1280
+ sessions.open(sessionId);
1281
+ } catch (error2) {
1282
+ warn(`loop-engine: could not reopen session "${sessionId}" after the reload: ${String(error2)}`);
1283
+ }
1284
+ };
1285
+ unsubscribe = sessions.list.subscribe(settle);
1286
+ settle();
1287
+ return () => {
1288
+ unsubscribe?.();
1289
+ };
1290
+ }
1291
+ function installReloadReturn(ctx, page = browserPage(), warn) {
1292
+ ctx.inject(["sessions"], (scope) => {
1293
+ const sessions = scope.get("sessions");
1294
+ if (sessions === void 0) return;
1295
+ const report = warn ?? ((message) => {
1296
+ scope.logger.warn(message);
1297
+ });
1298
+ scope.effect(
1299
+ () => restoreReloadReturn(sessions, page, report),
1300
+ "loop-engine: reopen the session this page was reloaded for"
1301
+ );
1302
+ });
1303
+ }
1304
+
1305
+ // src/client/session-engine.ts
1306
+ var LOOP_ENGINE_REMOTE_NAMESPACE = "loopEngine";
1307
+ var LOOP_ENGINE_REMOTE_METHOD = "engine";
1308
+ var LOOP_ENGINE_REMOTE_SELECT_METHOD = "select";
1309
+ function engineSwitchReady(report) {
1310
+ return report !== void 0;
1311
+ }
1312
+ function switchNeedsReload(current, target) {
1313
+ const from = current.kind === "engine" ? current.engine : "in-process";
1314
+ return from === "in-process" !== (target === "in-process");
1315
+ }
1316
+ var LOOP_ENGINE_PACKAGE = "dsh-loop-engine-allinone";
1317
+ function parseSessionEngine(value) {
1318
+ const kind = value?.kind;
1319
+ if (kind === "legacy" || kind === "unset") return { kind };
1320
+ const engine = value?.engine;
1321
+ if (kind === "engine" && isLoopEngineId(engine)) return { kind, engine };
1322
+ throw new TypeError(`loop-engine: "${String(kind)}" is not a session engine state`);
1323
+ }
1324
+ function parseSessionEngineReport(value) {
1325
+ const engine = parseSessionEngine(value?.engine);
1326
+ const pending = value?.pending;
1327
+ if (pending === void 0) return { engine };
1328
+ if (!isLoopEngineId(pending)) {
1329
+ throw new TypeError(`loop-engine: "${String(pending)}" is not a session engine state`);
1330
+ }
1331
+ return { engine, pending };
1332
+ }
1333
+ function parseSessionId(value) {
1334
+ const sessionId = value?.sessionId;
1335
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
1336
+ throw new TypeError("loop-engine: sessionId must be a non-empty string");
1337
+ }
1338
+ return sessionId;
1339
+ }
1340
+ function parseSelectRequest(value) {
1341
+ const sessionId = parseSessionId(value);
1342
+ const engine = value?.engine;
1343
+ if (!isLoopEngineId(engine)) {
1344
+ throw new TypeError(`loop-engine: engine must be one of ${LOOP_ENGINE_IDS.join(", ")}`);
1345
+ }
1346
+ return { sessionId, engine };
1347
+ }
1348
+ function parseSelectResult(value) {
1349
+ const outcome = value ?? {};
1350
+ if (outcome.ok === true && isLoopEngineId(outcome.engine)) {
1351
+ if (outcome.reload !== void 0 && outcome.reload !== true) {
1352
+ throw new TypeError("loop-engine: not an engine switch outcome");
1353
+ }
1354
+ return outcome.reload === true ? { ok: true, engine: outcome.engine, reload: true } : { ok: true, engine: outcome.engine };
1355
+ }
1356
+ if (outcome.ok === false && typeof outcome.reason === "string") {
1357
+ return isLoopEngineRefusalCode(outcome.code) ? { ok: false, code: outcome.code, reason: outcome.reason } : { ok: false, reason: outcome.reason };
1358
+ }
1359
+ throw new TypeError("loop-engine: not an engine switch outcome");
1360
+ }
1361
+ function descriptor(method, request, result) {
1362
+ return {
1363
+ id: `${LOOP_ENGINE_PACKAGE}#${LOOP_ENGINE_REMOTE_NAMESPACE}/${method}`,
1364
+ service: LOOP_ENGINE_REMOTE_NAMESPACE,
1365
+ namespace: LOOP_ENGINE_REMOTE_NAMESPACE,
1366
+ method,
1367
+ invocation: { kind: "direct" },
1368
+ parameters: [{
1369
+ // The wire name is the HOST method's own parameter name: the Gateway
1370
+ // derives an unregistered endpoint's signature from the live method
1371
+ // (`methodParameterNames`), and refuses args whose fields do not match it.
1372
+ name: "request",
1373
+ wire: "request",
1374
+ source: "json",
1375
+ codec: {
1376
+ mode: "strict",
1377
+ typeSymbol: `${LOOP_ENGINE_PACKAGE}#${LOOP_ENGINE_REMOTE_NAMESPACE}/${method}:request`,
1378
+ schema: request,
1379
+ create: () => request
1380
+ }
1381
+ }],
1382
+ result: {
1383
+ mode: "strict",
1384
+ typeSymbol: `${LOOP_ENGINE_PACKAGE}#${LOOP_ENGINE_REMOTE_NAMESPACE}/${method}:result`,
1385
+ schema: result,
1386
+ create: () => result
1387
+ }
1388
+ };
1389
+ }
1390
+ var LOOP_ENGINE_REMOTE_CONTRIBUTION = {
1391
+ package: LOOP_ENGINE_PACKAGE,
1392
+ descriptors: [
1393
+ descriptor(
1394
+ LOOP_ENGINE_REMOTE_METHOD,
1395
+ {
1396
+ /** Normalize the one-field request the host's method declares. */
1397
+ parse(value) {
1398
+ return { sessionId: parseSessionId(value) };
1399
+ }
1400
+ },
1401
+ { parse: parseSessionEngineReport }
1402
+ ),
1403
+ descriptor(LOOP_ENGINE_REMOTE_SELECT_METHOD, { parse: parseSelectRequest }, { parse: parseSelectResult })
1404
+ ]
1405
+ };
1406
+ var SessionEngineCache = class {
1407
+ engines = /* @__PURE__ */ new Map();
1408
+ watchers = /* @__PURE__ */ new Map();
1409
+ reading = /* @__PURE__ */ new Map();
1410
+ /**
1411
+ * Monotonic read generation per session. A read started before an
1412
+ * invalidation describes the session as it was BEFORE the switch, so however
1413
+ * late it settles it must not publish — publishing it would put the old engine
1414
+ * back on screen, which is the very misreport this cache exists to prevent.
1415
+ */
1416
+ generations = /* @__PURE__ */ new Map();
1417
+ remote;
1418
+ disposed = false;
1419
+ /**
1420
+ * Attach the mounted Remote namespace. Values asked for before the mount
1421
+ * settled are re-read, so a surface that mounted first is not left loading.
1422
+ * @param remote - the namespace, or undefined when the mount failed.
1423
+ */
1424
+ attach(remote) {
1425
+ if (this.disposed) return;
1426
+ this.remote = remote;
1427
+ for (const sessionId of [...this.watchers.keys()]) this.readIfUnknown(sessionId);
1428
+ }
1429
+ /**
1430
+ * The engine report cached for one session, or undefined while the first
1431
+ * answer is still in flight (or when the host could not answer at all).
1432
+ * @param sessionId - the session whose engine is asked for.
1433
+ * @returns the authoritative report, cached by session id.
1434
+ */
1435
+ read(sessionId) {
1436
+ return this.engines.get(sessionId);
1437
+ }
1438
+ /**
1439
+ * Follow one session's engine, re-reading it whenever a session's surfaces
1440
+ * appear again.
1441
+ *
1442
+ * A watch is a surface appearing for this session, and that is the moment the
1443
+ * cached answer can be stale in a way nothing else reports: a session whose
1444
+ * surfaces all went away and came back — the page switched to another session
1445
+ * and back — must not render what the page cached before. A re-mount is not
1446
+ * what moves a session's engine (a switch is performed on the host, and either
1447
+ * swaps the agent in place or releases the session and reloads this page), but
1448
+ * the page's answer is old by then, and a surface that rendered it as current
1449
+ * would be the one lying about what runs.
1450
+ *
1451
+ * Only the FIRST watcher of a session triggers the re-read, which is what
1452
+ * makes this "the session's surfaces appeared" rather than "a component
1453
+ * rendered": the chip and the composer are two watchers of one session, and
1454
+ * the second joins the read the first one started instead of sending its own.
1455
+ * While a session keeps its surfaces, nothing here asks again — the answer only
1456
+ * changes through {@link invalidate} (a committed switch) or a later re-mount.
1457
+ * @param sessionId - the session to follow.
1458
+ * @param listener - called whenever that session's cached state changes.
1459
+ * @returns the unsubscribe.
1460
+ */
1461
+ watch(sessionId, listener) {
1462
+ const listeners = this.watchers.get(sessionId) ?? /* @__PURE__ */ new Set();
1463
+ const appearing = listeners.size === 0;
1464
+ listeners.add(listener);
1465
+ this.watchers.set(sessionId, listeners);
1466
+ if (appearing) this.refresh(sessionId);
1467
+ return () => {
1468
+ if (this.watchers.get(sessionId) !== listeners) return;
1469
+ listeners.delete(listener);
1470
+ if (listeners.size === 0) this.watchers.delete(sessionId);
1471
+ };
1472
+ }
1473
+ /**
1474
+ * Ask the host for one session's engine report again, superseding nothing.
1475
+ *
1476
+ * This is the way a surface that has just been rendered asks "is what this page
1477
+ * cached still what runs?" — the answer that can go stale without anyone
1478
+ * invalidating it, because the page is not the only thing that can move a
1479
+ * session (another window, a hot swap elsewhere, a plugin reload).
1480
+ * A read already in flight for that session is joined rather than replaced (it
1481
+ * was started no earlier than this call and describes the same moment), and the
1482
+ * cached answer is kept until the new one lands, so a re-read never blinks the
1483
+ * chip back to "reading". Use {@link invalidate} instead for an answer that is
1484
+ * known to be WRONG.
1485
+ * @param sessionId - the session whose answer should be re-read.
1486
+ */
1487
+ refresh(sessionId) {
1488
+ if (this.disposed) return;
1489
+ if (this.reading.has(sessionId)) return;
1490
+ this.start(sessionId);
1491
+ }
1492
+ /**
1493
+ * Forget one session's cached engine and ask the host again.
1494
+ *
1495
+ * A successful engine switch changes what the host answers for that session,
1496
+ * so the picker MUST come through here: without it the composer would keep
1497
+ * showing the engine the session no longer runs. Any read already in flight
1498
+ * for that session is superseded and can no longer publish.
1499
+ * @param sessionId - the session whose answer is now stale.
1500
+ */
1501
+ invalidate(sessionId) {
1502
+ this.engines.delete(sessionId);
1503
+ this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
1504
+ this.reading.delete(sessionId);
1505
+ this.publish(sessionId);
1506
+ this.readIfUnknown(sessionId);
1507
+ }
1508
+ /** Drop every cached answer and notification (plugin unload). */
1509
+ dispose() {
1510
+ this.disposed = true;
1511
+ this.remote = void 0;
1512
+ this.engines.clear();
1513
+ this.reading.clear();
1514
+ this.generations.clear();
1515
+ this.watchers.clear();
1516
+ }
1517
+ /**
1518
+ * Ask unless the session is already answered, being read, or unanswerable.
1519
+ *
1520
+ * For the paths that need an answer and have none: the plugin's mount settling
1521
+ * (a surface asked before the Remote existed), a session whose cached answer
1522
+ * was just dropped — and never a re-read of an answer this page already holds,
1523
+ * which is {@link refresh}'s job and only a freshly rendered surface may ask
1524
+ * for.
1525
+ */
1526
+ readIfUnknown(sessionId) {
1527
+ if (this.engines.has(sessionId) || this.reading.has(sessionId)) return;
1528
+ this.start(sessionId);
1529
+ }
1530
+ /**
1531
+ * Start one read for a session, unless there is nothing to read it with.
1532
+ * @param sessionId - the session to read.
1533
+ */
1534
+ start(sessionId) {
1535
+ const remote = this.remote;
1536
+ if (this.disposed || remote === void 0) return;
1537
+ const generation = (this.generations.get(sessionId) ?? 0) + 1;
1538
+ this.generations.set(sessionId, generation);
1539
+ const read = this.ask(remote, sessionId, generation);
1540
+ this.reading.set(sessionId, read);
1541
+ void read.then(() => {
1542
+ if (this.reading.get(sessionId) === read) this.reading.delete(sessionId);
1543
+ });
1544
+ }
1545
+ /**
1546
+ * One read, quiet on failure: a session the host cannot answer for stays
1547
+ * unknown, and the surfaces keep their neutral state instead of claiming an
1548
+ * engine. A refusal is not cached as an answer, so the next watch re-asks.
1549
+ * @param remote - the mounted namespace.
1550
+ * @param sessionId - the session asked about.
1551
+ * @param generation - the read generation this answer belongs to.
1552
+ */
1553
+ async ask(remote, sessionId, generation) {
1554
+ let result;
1555
+ try {
1556
+ result = await remote.engine({ sessionId });
1557
+ } catch {
1558
+ return;
1559
+ }
1560
+ if (!result.ok || this.disposed || this.generations.get(sessionId) !== generation) return;
1561
+ let report;
1562
+ try {
1563
+ report = parseSessionEngineReport(result.value);
1564
+ } catch {
1565
+ return;
1566
+ }
1567
+ this.engines.set(sessionId, report);
1568
+ this.publish(sessionId);
1569
+ }
1570
+ /**
1571
+ * Notify one session's watchers.
1572
+ *
1573
+ * Nothing is painted here. The session's engine shows up in the chat
1574
+ * turn-status row too, but that row hangs off a DOCUMENT-level attribute, whose
1575
+ * only entitled owner is the session on screen — and a publish says nothing
1576
+ * about what is on screen (this is called for every session a surface has ever
1577
+ * watched, and for a session the user has already left, when its late answer
1578
+ * lands). So painting here let a background session take the attribute over
1579
+ * from the session on screen. The surfaces reflect instead, with the focus
1580
+ * guard in `./turn-status.ts`; this is the notification the chip and the
1581
+ * composer wake up on.
1582
+ */
1583
+ publish(sessionId) {
1584
+ for (const listener of this.watchers.get(sessionId) ?? []) listener();
1585
+ }
1586
+ };
1587
+ function createSessionEngineCache(ctx) {
1588
+ const cache = new SessionEngineCache();
1589
+ ctx.inject(["remote"], (remoteCtx) => {
1590
+ const remote = remoteCtx.get("remote");
1591
+ if (remote === void 0) return;
1592
+ remote.$mount(LOOP_ENGINE_REMOTE_CONTRIBUTION).then(
1593
+ () => {
1594
+ cache.attach(remoteCtx.get(`remote.${LOOP_ENGINE_REMOTE_NAMESPACE}`));
1595
+ },
1596
+ (error2) => {
1597
+ ctx.logger.warn(`loop-engine: could not mount the session engine Remote: ${String(error2)}`);
1598
+ }
1599
+ );
1600
+ });
1601
+ ctx.effect(() => () => {
1602
+ cache.dispose();
1603
+ }, "loop-engine: session engine cache");
1604
+ return cache;
1605
+ }
1606
+ function sessionEngineSwitcher(ctx, onSwitched, page = browserPage()) {
1607
+ return async (sessionId, engine) => {
1608
+ const remote = ctx.get(`remote.${LOOP_ENGINE_REMOTE_NAMESPACE}`);
1609
+ if (remote === void 0) return { ok: false, kind: "unavailable" };
1610
+ let result;
1611
+ try {
1612
+ result = await remote.select({ sessionId, engine });
1613
+ } catch (error2) {
1614
+ return { ok: false, kind: "refused", reason: error2 instanceof Error ? error2.message : String(error2) };
1615
+ }
1616
+ if (!result.ok) {
1617
+ return { ok: false, kind: "refused", reason: result.error.message };
1618
+ }
1619
+ if (!result.value.ok) {
1620
+ return result.value.code === void 0 ? { ok: false, kind: "refused", reason: result.value.reason } : { ok: false, kind: "refused", reason: result.value.reason, code: result.value.code };
1621
+ }
1622
+ onSwitched?.(sessionId);
1623
+ if (result.value.reload !== true) return { ok: true };
1624
+ armReloadReturn(page, sessionId);
1625
+ page.reload();
1626
+ return { ok: true, reload: true };
1627
+ };
1628
+ }
1629
+
1630
+ // src/client/LoopEngineComposerSelect.tsx
1631
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1632
+ var IconChevronDown2 = primitives2.IconChevronDownOutlineRegular ?? primitives2.IconChevronDownOutline14;
1633
+ var ENGINE_OPTIONS2 = LOOP_ENGINE_IDS.map((value) => ({ value, key: engineLabelKey(value) }));
1634
+ var READING = { label: "engineLoading", engine: void 0, selectedId: void 0 };
1635
+ function triggerFace(report) {
1636
+ const { engine: state, pending } = report;
1637
+ const face = state.kind === "engine" ? { label: engineLabelKey(state.engine), engine: state.engine, selectedId: state.engine } : { label: engineStateLabelKey(state), engine: void 0, selectedId: void 0 };
1638
+ return pending === void 0 ? face : { ...face, pending };
1639
+ }
1640
+ function defaultFace(fallback) {
1641
+ return { label: engineLabelKey(fallback), engine: fallback, selectedId: fallback };
1642
+ }
1643
+ function engineGlyph(engine, size = 16) {
1644
+ if (engine === "in-process") return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.FishLogo, { size });
1645
+ const mark = ENGINE_MARKS[engine];
1646
+ const paint = markPaint(mark, "currentColor");
1647
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: size, height: size, viewBox: mark.viewBox, fill: "none", "aria-hidden": true, children: [
1648
+ paint.mode === "fill" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { fill: paint.colour, fillRule: mark.fillRule ?? "nonzero", d: mark.d }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1649
+ "path",
1650
+ {
1651
+ fill: "none",
1652
+ stroke: paint.colour,
1653
+ strokeWidth: paint.width,
1654
+ strokeLinejoin: "round",
1655
+ strokeLinecap: "round",
1656
+ d: mark.d
1657
+ }
1658
+ ),
1659
+ mark.accent === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { fill: mark.accent.fill, d: mark.accent.d })
1660
+ ] });
1661
+ }
1662
+ var trigger2 = {
1663
+ appearance: "none",
1664
+ boxSizing: "border-box",
1665
+ display: "inline-flex",
1666
+ alignItems: "center",
1667
+ gap: 4,
1668
+ minWidth: 0,
1669
+ maxWidth: 220,
1670
+ height: 28,
1671
+ padding: "0 4px 0 8px",
1672
+ border: "none",
1673
+ borderRadius: 24,
1674
+ outline: "none",
1675
+ background: "transparent",
1676
+ color: "var(--dsw-alias-label-secondary)",
1677
+ font: "inherit",
1678
+ fontSize: 13,
1679
+ lineHeight: "20px",
1680
+ fontWeight: 500,
1681
+ cursor: "pointer"
1682
+ };
1683
+ var triggerHover = { ...trigger2, background: "var(--dsw-alias-interactive-bg-hover)" };
1684
+ var triggerDisabled2 = { ...trigger2, color: "var(--dsw-alias-label-dimmed)", cursor: "default" };
1685
+ var triggerIcon = {
1686
+ display: "inline-flex",
1687
+ flex: "0 0 auto"
1688
+ };
1689
+ var triggerLabel = {
1690
+ minWidth: 0,
1691
+ overflow: "hidden",
1692
+ textOverflow: "ellipsis",
1693
+ whiteSpace: "nowrap"
1694
+ };
1695
+ var chevron = {
1696
+ display: "inline-flex",
1697
+ flex: "0 0 auto",
1698
+ color: "var(--dsw-alias-label-caption)",
1699
+ transition: "transform 120ms ease"
1700
+ };
1701
+ var chevronOpen = { ...chevron, transform: "rotate(180deg)" };
1702
+ var rowPendingMark = {
1703
+ marginLeft: 4,
1704
+ color: "var(--dsw-alias-label-caption)",
1705
+ fontSize: 12
1706
+ };
1707
+ var noticeBody = {
1708
+ margin: 0,
1709
+ fontSize: 13,
1710
+ lineHeight: 1.55,
1711
+ color: "var(--dsw-alias-label-secondary)"
1712
+ };
1713
+ var noticeDetail = {
1714
+ margin: 0,
1715
+ fontSize: 12,
1716
+ lineHeight: 1.5,
1717
+ color: "var(--dsw-alias-label-tertiary)",
1718
+ wordBreak: "break-word"
1719
+ };
1720
+ function LoopEngineComposerSelect(props) {
1721
+ const { controller, useSnapshot, sessionId, sessionEngines, switchEngine, useSession, t } = props;
1722
+ const { status, engine: defaultEngine, showInComposer, writable } = useSnapshot((snapshot) => snapshot);
1723
+ const running = useSession?.((snapshot) => snapshot.running);
1724
+ const resolved = useEngineOfSession(sessionEngines, sessionId, running);
1725
+ const { label, engine, selectedId, pending } = sessionId === void 0 ? defaultFace(defaultEngine) : resolved === void 0 ? READING : triggerFace(resolved);
1726
+ const switchReady = sessionId === void 0 || engineSwitchReady(resolved);
1727
+ const [open, setOpen] = (0, import_react3.useState)(false);
1728
+ const [hovered, setHovered] = (0, import_react3.useState)(false);
1729
+ const [notice2, setNotice] = (0, import_react3.useState)(null);
1730
+ const [pendingReload, setPendingReload] = (0, import_react3.useState)(null);
1731
+ const triggerRef = (0, import_react3.useRef)(null);
1732
+ if (status !== "ready" || !showInComposer) return null;
1733
+ const disabled = !writable || !switchReady;
1734
+ const labelText = t(label);
1735
+ const pendingText = pending === void 0 ? void 0 : ` \xB7 ${pendingEngineText(t, pending)}`;
1736
+ const title = pending !== void 0 ? t("pendingComposerHint") : isHostedEngine(engine) ? t("hostedEngineModelNotice") : sessionId === void 0 ? t("description") : t("composerHint");
1737
+ const commitSwitch = (value) => {
1738
+ if (sessionId === void 0) {
1739
+ void controller.setEngine(value);
1740
+ return;
1741
+ }
1742
+ void switchEngine(sessionId, value).then((result) => {
1743
+ if (!result.ok) {
1744
+ if (result.kind === "unavailable") {
1745
+ setNotice({ title: "switchFailedTitle", body: t("switchUnavailable") });
1746
+ return;
1747
+ }
1748
+ const face = refusalFace(result.code);
1749
+ setNotice({
1750
+ title: "switchFailedTitle",
1751
+ body: face.body === void 0 ? result.reason : t(face.body),
1752
+ ...face.detail ? { detail: result.reason } : {}
1753
+ });
1754
+ return;
1755
+ }
1756
+ if (result.reload === true) {
1757
+ setNotice({ title: "switchReloadTitle", body: t("switchReloadBody") });
1758
+ return;
1759
+ }
1760
+ });
1761
+ };
1762
+ const onSelect = (next) => {
1763
+ setOpen(false);
1764
+ const value = next;
1765
+ if (value === selectedId) return;
1766
+ if (sessionId !== void 0) {
1767
+ if (!engineSwitchReady(resolved)) return;
1768
+ if (switchNeedsReload(resolved.engine, value)) {
1769
+ setPendingReload(value);
1770
+ return;
1771
+ }
1772
+ }
1773
+ commitSwitch(value);
1774
+ };
1775
+ const confirmReload = () => {
1776
+ const value = pendingReload;
1777
+ setPendingReload(null);
1778
+ if (value !== null) commitSwitch(value);
1779
+ };
1780
+ const cancelReload = () => {
1781
+ setPendingReload(null);
1782
+ };
1783
+ const dismissNotice = () => {
1784
+ setNotice(null);
1785
+ };
1786
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1787
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1788
+ import_dsh_client_ui_primitives2.Menu,
1789
+ {
1790
+ open,
1791
+ onClose: () => {
1792
+ setOpen(false);
1793
+ },
1794
+ items: ENGINE_OPTIONS2.map((option) => ({
1795
+ id: option.value,
1796
+ // The recorded engine's own row carries the marker too: the trigger may
1797
+ // be truncated to ellipsis, and a user who opens the list has to be
1798
+ // able to see which row was recorded instead of concluding the pick
1799
+ // did nothing. The check mark stays on the row in force (selectedId),
1800
+ // which is never the marked one.
1801
+ label: option.value === pending ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1802
+ t(option.key),
1803
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: rowPendingMark, children: t("engineMenuPendingSuffix") })
1804
+ ] }) : t(option.key),
1805
+ icon: engineGlyph(option.value)
1806
+ })),
1807
+ selectedId,
1808
+ onSelect,
1809
+ align: "start",
1810
+ portal: true,
1811
+ getAnchorRect: () => triggerRef.current?.getBoundingClientRect() ?? null,
1812
+ anchor: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1813
+ "button",
1814
+ {
1815
+ type: "button",
1816
+ ref: triggerRef,
1817
+ "aria-haspopup": "menu",
1818
+ "aria-expanded": open,
1819
+ disabled,
1820
+ style: disabled ? triggerDisabled2 : hovered ? triggerHover : trigger2,
1821
+ title,
1822
+ onMouseEnter: () => {
1823
+ setHovered(true);
1824
+ },
1825
+ onMouseLeave: () => {
1826
+ setHovered(false);
1827
+ },
1828
+ onClick: () => {
1829
+ setOpen(!open);
1830
+ },
1831
+ children: [
1832
+ engine === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: triggerIcon, "aria-hidden": true, children: engineGlyph(engine, 14) }),
1833
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: triggerLabel, children: [
1834
+ labelText,
1835
+ pendingText
1836
+ ] }),
1837
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: open ? chevronOpen : chevron, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(IconChevronDown2, { size: 14 }) })
1838
+ ]
1839
+ }
1840
+ )
1841
+ }
1842
+ ),
1843
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1844
+ import_dsh_client_ui_primitives2.Modal,
1845
+ {
1846
+ open: pendingReload !== null,
1847
+ onClose: cancelReload,
1848
+ title: t("switchReloadConfirmTitle"),
1849
+ closeLabel: t("cancelAction"),
1850
+ footer: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1851
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "outline", onClick: cancelReload, children: t("cancelAction") }),
1852
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "primary", onClick: confirmReload, children: t("switchReloadConfirmAction") })
1853
+ ] }),
1854
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: noticeBody, children: t("switchReloadConfirmBody") })
1855
+ }
1856
+ ),
1857
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1858
+ import_dsh_client_ui_primitives2.Modal,
1859
+ {
1860
+ open: notice2 !== null,
1861
+ onClose: dismissNotice,
1862
+ title: t(notice2?.title ?? "switchFailedTitle"),
1863
+ closeLabel: t("closeLabel"),
1864
+ footer: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "primary", onClick: dismissNotice, children: t("closeLabel") }),
1865
+ children: [
1866
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: noticeBody, children: notice2?.body }),
1867
+ notice2?.detail === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: noticeDetail, children: notice2.detail })
1868
+ ]
1869
+ }
1870
+ )
1871
+ ] });
1872
+ }
1873
+
1874
+ // src/client/store.ts
1875
+ var import_dsh_client_store = require("@deepseek-ai/dsh-client-store");
1876
+ function decodeLoopEngine(section) {
1877
+ if (typeof section !== "object" || section === null || Array.isArray(section)) return void 0;
1878
+ const { engine, showInComposer, showEngineBadge } = section;
1879
+ if (!isLoopEngineId(engine)) return void 0;
1880
+ return {
1881
+ engine,
1882
+ showInComposer: showInComposer !== false,
1883
+ showEngineBadge: showEngineBadge !== false
1884
+ };
1885
+ }
1886
+ var LoopEngineStore = class {
1887
+ /**
1888
+ * @param transport - the settings channel the store follows.
1889
+ */
1890
+ constructor(transport) {
1891
+ this.transport = transport;
1892
+ }
1893
+ transport;
1894
+ /** uSES-safe state source shared by the registered settings section. */
1895
+ store = (0, import_dsh_client_store.createSnapshotStore)({
1896
+ status: "loading",
1897
+ engine: "in-process",
1898
+ showInComposer: true,
1899
+ showEngineBadge: true,
1900
+ writable: false,
1901
+ error: null
1902
+ });
1903
+ following;
1904
+ saving = false;
1905
+ /** Begin following the transport and publish its current answer. */
1906
+ load() {
1907
+ this.following ??= this.transport.subscribe(() => {
1908
+ this.derive();
1909
+ });
1910
+ this.derive();
1911
+ }
1912
+ /**
1913
+ * Persist the selected engine. Success is the transport's accepted answer
1914
+ * (when it reports one) checked against the snapshot the write left behind,
1915
+ * so a refused write reports error after its recovery.
1916
+ * @param engine - the engine to select for future Agent turns.
1917
+ * @returns whether the write landed.
1918
+ */
1919
+ async setEngine(engine) {
1920
+ this.saving = true;
1921
+ this.store.update((state) => {
1922
+ state.status = "saving";
1923
+ state.error = null;
1924
+ });
1925
+ let accepted = true;
1926
+ try {
1927
+ accepted = await this.transport.set("engine", engine);
1928
+ } finally {
1929
+ this.saving = false;
1930
+ }
1931
+ this.derive();
1932
+ const landed = accepted !== false && this.store.getSnapshot().engine === engine;
1933
+ if (!landed) {
1934
+ this.store.update((state) => {
1935
+ state.status = "unavailable";
1936
+ state.error = "the loop engine selection did not persist";
1937
+ });
1938
+ }
1939
+ return landed;
1940
+ }
1941
+ /**
1942
+ * Persist whether the composer shows the engine picker. Unlike
1943
+ * {@link setEngine}, landing does not reload the page — the toggle only
1944
+ * changes composer visibility.
1945
+ * @param show - whether the chat page composer reveals the engine picker.
1946
+ * @returns whether the write landed.
1947
+ */
1948
+ async setShowInComposer(show) {
1949
+ this.saving = true;
1950
+ this.store.update((state) => {
1951
+ state.status = "saving";
1952
+ state.error = null;
1953
+ });
1954
+ let accepted = true;
1955
+ try {
1956
+ accepted = await this.transport.set("showInComposer", show);
1957
+ } finally {
1958
+ this.saving = false;
1959
+ }
1960
+ this.derive();
1961
+ const landed = accepted !== false && this.store.getSnapshot().showInComposer === show;
1962
+ if (!landed) {
1963
+ this.store.update((state) => {
1964
+ state.status = "unavailable";
1965
+ state.error = "the loop engine display setting did not persist";
1966
+ });
1967
+ }
1968
+ return landed;
1969
+ }
1970
+ /**
1971
+ * Persist whether the conversation header shows the engine badge. Unlike
1972
+ * {@link setEngine}, landing does not reload the page — the toggle only
1973
+ * changes header visibility.
1974
+ * @param show - whether the conversation header reveals the engine badge.
1975
+ * @returns whether the write landed.
1976
+ */
1977
+ async setShowEngineBadge(show) {
1978
+ this.saving = true;
1979
+ this.store.update((state) => {
1980
+ state.status = "saving";
1981
+ state.error = null;
1982
+ });
1983
+ let accepted = true;
1984
+ try {
1985
+ accepted = await this.transport.set("showEngineBadge", show);
1986
+ } finally {
1987
+ this.saving = false;
1988
+ }
1989
+ this.derive();
1990
+ const landed = accepted !== false && this.store.getSnapshot().showEngineBadge === show;
1991
+ if (!landed) {
1992
+ this.store.update((state) => {
1993
+ state.status = "unavailable";
1994
+ state.error = "the loop engine display setting did not persist";
1995
+ });
1996
+ }
1997
+ return landed;
1998
+ }
1999
+ /** Stop following the transport. */
2000
+ dispose() {
2001
+ this.following?.();
2002
+ this.following = void 0;
2003
+ }
2004
+ derive() {
2005
+ if (this.saving) return;
2006
+ const snapshot = this.transport.getSnapshot();
2007
+ switch (snapshot.status) {
2008
+ case "loading":
2009
+ this.store.update((state) => {
2010
+ state.status = "loading";
2011
+ state.error = null;
2012
+ });
2013
+ return;
2014
+ case "unavailable":
2015
+ this.store.update((state) => {
2016
+ state.status = "unavailable";
2017
+ state.engine = "in-process";
2018
+ state.showInComposer = true;
2019
+ state.showEngineBadge = true;
2020
+ state.error = null;
2021
+ });
2022
+ return;
2023
+ case "ready": {
2024
+ const engine = snapshot.value?.engine ?? "in-process";
2025
+ const showInComposer = snapshot.value?.showInComposer ?? true;
2026
+ const showEngineBadge = snapshot.value?.showEngineBadge ?? true;
2027
+ this.store.update((state) => {
2028
+ state.status = "ready";
2029
+ state.engine = engine;
2030
+ state.showInComposer = showInComposer;
2031
+ state.showEngineBadge = showEngineBadge;
2032
+ state.writable = snapshot.writable;
2033
+ state.error = null;
2034
+ });
2035
+ return;
2036
+ }
2037
+ default: {
2038
+ const exhaustive = snapshot.status;
2039
+ throw new Error(`unexpected loop engine form status: ${String(exhaustive)}`);
2040
+ }
2041
+ }
2042
+ }
2043
+ };
2044
+
2045
+ // src/namespace.ts
2046
+ var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "loop-engine";
2047
+ var LEGACY_LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
2048
+
2049
+ // src/client/index.ts
2050
+ var NS = "settings.loop-engine";
2051
+ var inject = ["slots", "locale"];
2052
+ function apply(ctx) {
2053
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "loop-engine: copy dictionaries");
2054
+ installTurnStatusStyles(ctx);
2055
+ const sessionEngines = createSessionEngineCache(ctx);
2056
+ installReloadReturn(ctx);
2057
+ const t = ctx.locale.bind(NS);
2058
+ let source;
2059
+ const listeners = /* @__PURE__ */ new Set();
2060
+ const transport = {
2061
+ getSnapshot: () => source?.getSnapshot() ?? { status: "loading", value: void 0, writable: false },
2062
+ subscribe: (listener) => {
2063
+ listeners.add(listener);
2064
+ return () => {
2065
+ listeners.delete(listener);
2066
+ };
2067
+ },
2068
+ set: (field, value) => source === void 0 ? Promise.resolve(false) : source.set(field, value)
2069
+ };
2070
+ const adopt = (next) => {
2071
+ source = next;
2072
+ const forget = next.subscribe(() => {
2073
+ for (const listener of [...listeners]) listener();
2074
+ });
2075
+ for (const listener of [...listeners]) listener();
2076
+ return () => {
2077
+ forget();
2078
+ if (source === next) source = void 0;
2079
+ };
2080
+ };
2081
+ const controller = new LoopEngineStore(transport);
2082
+ ctx.effect(() => {
2083
+ controller.load();
2084
+ return () => {
2085
+ controller.dispose();
2086
+ };
2087
+ }, "loop-engine: store lifecycle");
2088
+ const injected = () => ({
2089
+ controller,
2090
+ hooks: { snapshot: controller.store },
2091
+ t
2092
+ });
2093
+ const registerPage = () => ctx.slots.inject("settings.section", () => ctx.slots.register({
2094
+ name: "settings.section",
2095
+ id: "loop-engine",
2096
+ order: 30,
2097
+ label: () => t("nav"),
2098
+ inject: injected
2099
+ }, LoopEngineSection));
2100
+ ctx.inject(["configForms"], (scope) => {
2101
+ const form = scope.configForms.get(LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL);
2102
+ scope.effect(() => adopt({
2103
+ getSnapshot: () => form.getSnapshot(),
2104
+ subscribe: (listener) => form.subscribe(listener),
2105
+ set: (field, value) => form.set(field, value)
2106
+ }), "loop-engine: settings transport (config forms)");
2107
+ scope.effect(
2108
+ () => scope.configForms.whileServed([LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL], registerPage),
2109
+ "loop-engine: settings page"
2110
+ );
2111
+ });
2112
+ ctx.inject(["settingsScope"], (scope) => {
2113
+ const scopes = scope.settingsScope;
2114
+ const bound = scopes.bind({
2115
+ namespace: LEGACY_LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL,
2116
+ decode: decodeLoopEngine
2117
+ });
2118
+ scope.effect(() => adopt({
2119
+ getSnapshot: () => bound.getSnapshot(),
2120
+ subscribe: (listener) => bound.subscribe(listener),
2121
+ set: (field, value) => bound.set(field, value)
2122
+ }), "loop-engine: settings transport (settings scope)");
2123
+ scope.effect(registerPage, "loop-engine: settings page");
2124
+ });
2125
+ ctx.inject(["slots", "conversation"], (scope) => {
2126
+ const badgeInjected = () => ({
2127
+ sessionEngines,
2128
+ hooks: { snapshot: controller.store },
2129
+ t
2130
+ });
2131
+ scope.effect(() => {
2132
+ return scope.slots.register({
2133
+ name: "conversation.session.header.actions",
2134
+ id: "loop-engine",
2135
+ // Static session context precedes interactive actions (agent-preset's
2136
+ // label sits at -10, so the engine chip leads the header).
2137
+ order: -20,
2138
+ locale: NS,
2139
+ inject: badgeInjected
2140
+ }, LoopEngineBadge);
2141
+ }, "loop-engine: session header engine badge");
2142
+ });
2143
+ const switchEngine = sessionEngineSwitcher(ctx, (sessionId) => {
2144
+ sessionEngines.invalidate(sessionId);
2145
+ });
2146
+ ctx.inject(["slots", "conversation"], (scope) => {
2147
+ const composerInjected = () => ({
2148
+ controller,
2149
+ hooks: { snapshot: controller.store },
2150
+ sessionEngines,
2151
+ switchEngine,
2152
+ t
2153
+ });
2154
+ scope.effect(() => {
2155
+ return scope.slots.register({
2156
+ name: "conversation.input.right",
2157
+ id: "loop-engine",
2158
+ order: 0,
2159
+ locale: NS,
2160
+ inject: composerInjected
2161
+ }, LoopEngineComposerSelect);
2162
+ }, "loop-engine: composer engine select");
2163
+ });
2164
+ }
2165
+ return module.exports; } });
2166
+ //# sourceMappingURL=client.js.map