dsh-loop-engine 0.1.5-rc4 → 0.1.7-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ A **published** dsh needs no extra setup. Booting the harness from its **source
22
22
 
23
23
  ## Version compatibility
24
24
 
25
- dsh-loop-engine is versioned **in lockstep with the harness it targets**: `<harness version>-rcN`. `0.1.5-rc3` targets harness `0.1.5-rc.2`; `0.1.5-rc1`/`0.1.5-rc2` target `0.1.5-rc.1`; `1.0.0-rc8` … `1.0.0-rc15` target `0.1.2-rc.1`; `1.0.0-rc7` and earlier target `0.1.1-rc.2`. Every harness package it consumes is pinned exactly in `peerDependencies`, and the two must be matched — a mismatch fails loudly at boot or session resume. To use the plugin with an older harness, install the release matching it (each GitHub Release states the harness version it targets).
25
+ dsh-loop-engine is versioned **in lockstep with the harness it targets**: `<harness version>-rcN`. From `0.1.7-rc1` on, **one release serves two harness generations**: the 0.1.5 line (`>=0.1.5-rc.1 <0.1.6-0`, all sharing the old settings API) and `0.1.7-rc.1` (`>=0.1.7-rc.1 <0.1.8-0`). It detects the running generation at load and takes the matching code path, so a single published package installs onto either. Every harness package it consumes declares that union range in `peerDependencies`; a harness outside it fails loudly at boot or session resume. `0.1.5-rc3` … `0.1.5-rc5` target harness `0.1.5-rc.2`; `0.1.5-rc1`/`0.1.5-rc2` target `0.1.5-rc.1`; `1.0.0-rc8` … `1.0.0-rc15` target `0.1.2-rc.1`; `1.0.0-rc7` and earlier target `0.1.1-rc.2`.
26
26
 
27
27
  ### Requirements
28
28
 
package/README.zh.md CHANGED
@@ -22,7 +22,7 @@ dsh plugin --profile web add dsh-loop-engine
22
22
 
23
23
  ## 版本兼容
24
24
 
25
- `dsh-loop-engine` 与它针对的 harness **同版本对齐**:`<harness version>-rcN`。`0.1.5-rc3` 针对 harness `0.1.5-rc.2`;`0.1.5-rc1`/`0.1.5-rc2` 针对 `0.1.5-rc.1`;`1.0.0-rc8` … `1.0.0-rc15` 针对 `0.1.2-rc.1`;`1.0.0-rc7` 及更早针对 `0.1.1-rc.2`。它消费的每个 harness 包都在 `peerDependencies` 里精确钉住,两者必须匹配——不匹配会在启动或会话恢复时响亮地失败。要在更老的 harness 上使用本插件,请安装与之匹配的版本(每个 GitHub Release 正文会写明它针对的 harness 版本)。
25
+ `dsh-loop-engine` 与它针对的 harness **同版本对齐**:`<harness version>-rcN`。自 `0.1.7-rc1` 起,**一个发布版本同时服务两代 harness**:0.1.5 线(`>=0.1.5-rc.1 <0.1.6-0`,三段 `rc` 共用同一套旧 settings API)与 `0.1.7-rc.1`(`>=0.1.7-rc.1 <0.1.8-0`)。它在加载期探测当前是哪一代、走对应分支,所以同一份已发布产物在两代上都能安装运行。它消费的每个 harness 包都在 `peerDependencies` 里声明这个并集范围;超出范围会在启动或会话恢复时响亮地失败。`0.1.5-rc3` … `0.1.5-rc5` 针对 harness `0.1.5-rc.2`;`0.1.5-rc1`/`0.1.5-rc2` 针对 `0.1.5-rc.1`;`1.0.0-rc8` … `1.0.0-rc15` 针对 `0.1.2-rc.1`;`1.0.0-rc7` 及更早针对 `0.1.1-rc.2`。
26
26
 
27
27
  ### 环境要求
28
28
 
package/lib/client.js CHANGED
@@ -1,8 +1,10 @@
1
1
  var module = { exports: {} }; var exports = module.exports; window.__ModuleLoader__.load({ id: "dsh-loop-engine", factory: (require) => {
2
2
  "use strict";
3
+ var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
6
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
9
  var __export = (target, all) => {
8
10
  for (var name in all)
@@ -16,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
16
18
  }
17
19
  return to;
18
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
+ ));
19
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
30
 
21
31
  // src/client/index.ts
@@ -28,6 +38,7 @@ module.exports = __toCommonJS(index_exports);
28
38
 
29
39
  // src/client/LoopEngineSection.tsx
30
40
  var import_react = require("react");
41
+ var primitives = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
31
42
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
32
43
 
33
44
  // src/agent-preset-ids.ts
@@ -60,6 +71,7 @@ function hostedEngineOf(session) {
60
71
 
61
72
  // src/client/LoopEngineSection.tsx
62
73
  var import_jsx_runtime = require("react/jsx-runtime");
74
+ var IconChevronDown = primitives.IconChevronDownOutlineRegular ?? primitives.IconChevronDownOutline14;
63
75
  var ENGINE_OPTIONS = [
64
76
  { value: "in-process", key: "engineInProcess" },
65
77
  { value: "claude-code", key: "engineClaudeCode" },
@@ -208,7 +220,7 @@ function LoopEngineSection(props) {
208
220
  },
209
221
  children: [
210
222
  label,
211
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 })
223
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconChevronDown, { size: 14 })
212
224
  ]
213
225
  }
214
226
  )
@@ -253,9 +265,18 @@ var import_react2 = require("react");
253
265
 
254
266
  // src/client/turn-status.ts
255
267
  var ENGINE_ATTR = "data-loop-engine";
268
+ var RUNNING_ATTR = "data-loop-engine-running";
256
269
  var PLUGIN_ID = "dsh-loop-engine";
257
- var STYLESHEET = `
258
- [class$="_turnStatus"]::before {
270
+ var ROW_SELECTORS = [
271
+ /** 0.1.5 line: the turn-status row, whose class name ends `_turnStatus`. */
272
+ '[class$="_turnStatus"]',
273
+ /** 0.1.7 line: the turn-process button (`data-turn-process`); its status text is the label span. */
274
+ 'button[data-turn-process] [class$="_label"]'
275
+ ];
276
+ var sheetFor = (ROW) => {
277
+ const GATE = `html[${ENGINE_ATTR}][${RUNNING_ATTR}]`;
278
+ return `
279
+ ${GATE} ${ROW}::before {
259
280
  margin-right: 6px;
260
281
  background: none;
261
282
  -webkit-background-clip: border-box;
@@ -263,18 +284,34 @@ var STYLESHEET = `
263
284
  }
264
285
 
265
286
  /*
266
- * The row's own sweep, re-asserted for hosted engines.
287
+ * The engine-colored sweep \u2014 and, on the 0.1.7 line, the sweep itself.
267
288
  *
268
- * ui-chat disables that animation under \`prefers-reduced-motion: reduce\`
269
- * (ChatView.module.css), and a media query carries no specificity \u2014 so this
270
- * attribute-gated rule outranks it. That is deliberate here: deployment images
271
- * ship with Windows' client-area animation off (SPI_GETCLIENTAREAANIMATION
272
- * false), and with the guard in force EVERY indicator on this row is frozen \u2014
273
- * the sweep and the glyph alike. Scoped to hosted engines, so in-process
274
- * sessions keep the stock reduced-motion behaviour; delete this rule and
275
- * restore the guard at the foot of the sheet to hand the decision back to the OS.
289
+ * The 0.1.5 row paints its own gradient (a \`linear-gradient\` over
290
+ * --dsw-static-deepseek-*, clipped to the text) and only needs its animation
291
+ * re-asserted; this rule declares that same gradient so the 0.1.7 row, whose
292
+ * turn-process button is plain tertiary text, gets the sweep back. Either way
293
+ * the per-engine override below recolors it by swapping the two custom
294
+ * properties. ui-chat disables the animation under
295
+ * \`prefers-reduced-motion: reduce\`, and a media query carries no specificity \u2014
296
+ * so this attribute-gated rule outranks it. That is deliberate here:
297
+ * deployment images ship with Windows' client-area animation off
298
+ * (SPI_GETCLIENTAREAANIMATION false), and with the guard in force EVERY
299
+ * indicator on this row is frozen \u2014 the sweep and the glyph alike. Scoped to
300
+ * hosted engines, so in-process sessions keep the stock reduced-motion
301
+ * behaviour; delete this rule and restore the guard at the foot of the sheet to
302
+ * hand the decision back to the OS.
276
303
  */
277
- html[${ENGINE_ATTR}] [class$="_turnStatus"] {
304
+ ${GATE} ${ROW} {
305
+ background: linear-gradient(90deg,
306
+ var(--dsw-static-deepseek-500) 0%,
307
+ var(--dsw-static-deepseek-500) 40%,
308
+ var(--dsw-static-deepseek-200) 50%,
309
+ var(--dsw-static-deepseek-500) 60%,
310
+ var(--dsw-static-deepseek-500) 100%);
311
+ color: #0000;
312
+ -webkit-text-fill-color: transparent;
313
+ -webkit-background-clip: text;
314
+ background-clip: text;
278
315
  background-position: 100% 0;
279
316
  background-size: 250% 100%;
280
317
  animation: le-shimmer 1.8s linear infinite;
@@ -284,22 +321,22 @@ html[${ENGINE_ATTR}] [class$="_turnStatus"] {
284
321
  to { background-position: 0 0; }
285
322
  }
286
323
 
287
- html[${ENGINE_ATTR}="claude-code"] [class$="_turnStatus"] {
324
+ html[${ENGINE_ATTR}="claude-code"][${RUNNING_ATTR}] ${ROW} {
288
325
  --dsw-static-deepseek-500: #d97757;
289
326
  --dsw-static-deepseek-200: #f5bda6;
290
327
  }
291
- html[${ENGINE_ATTR}="claude-code"] [class$="_turnStatus"]::before {
328
+ html[${ENGINE_ATTR}="claude-code"][${RUNNING_ATTR}] ${ROW}::before {
292
329
  content: "\u273B";
293
330
  color: #d97757;
294
331
  -webkit-text-fill-color: #d97757;
295
332
  animation: le-bloom 1.6s ease-in-out infinite;
296
333
  }
297
334
 
298
- html[${ENGINE_ATTR}="codex"] [class$="_turnStatus"] {
335
+ html[${ENGINE_ATTR}="codex"][${RUNNING_ATTR}] ${ROW} {
299
336
  --dsw-static-deepseek-500: #a9b1c0;
300
337
  --dsw-static-deepseek-200: #e6eaf2;
301
338
  }
302
- html[${ENGINE_ATTR}="codex"] [class$="_turnStatus"]::before {
339
+ html[${ENGINE_ATTR}="codex"][${RUNNING_ATTR}] ${ROW}::before {
303
340
  content: "\u2022";
304
341
  color: #a9b1c0;
305
342
  -webkit-text-fill-color: #a9b1c0;
@@ -307,11 +344,11 @@ html[${ENGINE_ATTR}="codex"] [class$="_turnStatus"]::before {
307
344
  animation: le-pulse 1.4s ease-in-out infinite;
308
345
  }
309
346
 
310
- html[${ENGINE_ATTR}="pi"] [class$="_turnStatus"] {
347
+ html[${ENGINE_ATTR}="pi"][${RUNNING_ATTR}] ${ROW} {
311
348
  --dsw-static-deepseek-500: #8e4ec6;
312
349
  --dsw-static-deepseek-200: #d6bff0;
313
350
  }
314
- html[${ENGINE_ATTR}="pi"] [class$="_turnStatus"]::before {
351
+ html[${ENGINE_ATTR}="pi"][${RUNNING_ATTR}] ${ROW}::before {
315
352
  content: "\u280B";
316
353
  color: #8e4ec6;
317
354
  -webkit-text-fill-color: #8e4ec6;
@@ -319,11 +356,11 @@ html[${ENGINE_ATTR}="pi"] [class$="_turnStatus"]::before {
319
356
  animation: le-braille 1s linear infinite;
320
357
  }
321
358
 
322
- html[${ENGINE_ATTR}="kimi"] [class$="_turnStatus"] {
359
+ html[${ENGINE_ATTR}="kimi"][${RUNNING_ATTR}] ${ROW} {
323
360
  --dsw-static-deepseek-500: #e5484d;
324
361
  --dsw-static-deepseek-200: #f5b2b4;
325
362
  }
326
- html[${ENGINE_ATTR}="kimi"] [class$="_turnStatus"]::before {
363
+ html[${ENGINE_ATTR}="kimi"][${RUNNING_ATTR}] ${ROW}::before {
327
364
  content: "\u{1F317}";
328
365
  color: #e5484d;
329
366
  -webkit-text-fill-color: #e5484d;
@@ -380,10 +417,13 @@ html[${ENGINE_ATTR}="kimi"] [class$="_turnStatus"]::before {
380
417
  }
381
418
 
382
419
  `;
420
+ };
421
+ var STYLESHEET = ROW_SELECTORS.map(sheetFor).join("\n");
383
422
  var focusedSessionId;
384
- function reflectTurnStatusEngine(sessionId, engine) {
423
+ function reflectTurnStatusEngine(sessionId, engine, running) {
385
424
  if (sessionId === void 0 || sessionId !== focusedSessionId) return;
386
425
  writeTurnStatusEngine(engine);
426
+ if (running !== void 0) writeTurnStatusRunning(running);
387
427
  }
388
428
  function focusTurnStatusSession(sessionId) {
389
429
  focusedSessionId = sessionId;
@@ -400,6 +440,12 @@ function writeTurnStatusEngine(engine) {
400
440
  }
401
441
  root.dataset.loopEngine = engine;
402
442
  }
443
+ function writeTurnStatusRunning(running) {
444
+ if (typeof document === "undefined") return;
445
+ const root = document.documentElement;
446
+ if (running) root.dataset.loopEngineRunning = "";
447
+ else delete root.dataset.loopEngineRunning;
448
+ }
403
449
  function installTurnStatusStyles(ctx) {
404
450
  if (typeof document === "undefined") return;
405
451
  ctx.effect(() => {
@@ -411,12 +457,13 @@ function installTurnStatusStyles(ctx) {
411
457
  return () => {
412
458
  tag.remove();
413
459
  delete document.documentElement.dataset.loopEngine;
460
+ delete document.documentElement.dataset.loopEngineRunning;
414
461
  };
415
462
  }, "loop-engine: per-engine turn status styles");
416
463
  }
417
464
 
418
465
  // src/client/use-session-engine.ts
419
- function useEngineOfSession(cache, sessionId) {
466
+ function useEngineOfSession(cache, sessionId, running) {
420
467
  const [, bump] = (0, import_react2.useState)(0);
421
468
  const report = sessionId === void 0 ? void 0 : cache.read(sessionId);
422
469
  (0, import_react2.useEffect)(() => {
@@ -429,11 +476,11 @@ function useEngineOfSession(cache, sessionId) {
429
476
  (0, import_react2.useEffect)(() => {
430
477
  if (sessionId === void 0) return;
431
478
  focusTurnStatusSession(sessionId);
432
- reflectTurnStatusEngine(sessionId, engine);
479
+ reflectTurnStatusEngine(sessionId, engine, running);
433
480
  return () => {
434
481
  blurTurnStatusSession(sessionId);
435
482
  };
436
- }, [sessionId, engine]);
483
+ }, [sessionId, engine, running]);
437
484
  return report;
438
485
  }
439
486
 
@@ -574,8 +621,9 @@ var pill = {
574
621
  whiteSpace: "nowrap"
575
622
  };
576
623
  function LoopEngineBadge(props) {
577
- const { sessionId, sessionEngines, t } = props;
578
- const report = useEngineOfSession(sessionEngines, sessionId);
624
+ const { sessionId, sessionEngines, useSession, t } = props;
625
+ const running = useSession?.((snapshot) => snapshot.running);
626
+ const report = useEngineOfSession(sessionEngines, sessionId, running);
579
627
  if (sessionId === void 0 || report === void 0) return null;
580
628
  const engine = report.engine;
581
629
  if (engine.kind === "unset") return null;
@@ -593,6 +641,7 @@ function LoopEngineBadge(props) {
593
641
 
594
642
  // src/client/LoopEngineComposerSelect.tsx
595
643
  var import_react3 = require("react");
644
+ var primitives2 = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
596
645
  var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
597
646
 
598
647
  // src/client/reload.ts
@@ -747,13 +796,15 @@ function descriptor(method, request, result) {
747
796
  codec: {
748
797
  mode: "strict",
749
798
  typeSymbol: `${LOOP_ENGINE_PACKAGE}#${LOOP_ENGINE_REMOTE_NAMESPACE}/${method}:request`,
750
- schema: request
799
+ schema: request,
800
+ create: () => request
751
801
  }
752
802
  }],
753
803
  result: {
754
804
  mode: "strict",
755
805
  typeSymbol: `${LOOP_ENGINE_PACKAGE}#${LOOP_ENGINE_REMOTE_NAMESPACE}/${method}:result`,
756
- schema: result
806
+ schema: result,
807
+ create: () => result
757
808
  }
758
809
  };
759
810
  }
@@ -999,6 +1050,7 @@ function sessionEngineSwitcher(ctx, onSwitched, page = browserPage()) {
999
1050
 
1000
1051
  // src/client/LoopEngineComposerSelect.tsx
1001
1052
  var import_jsx_runtime3 = require("react/jsx-runtime");
1053
+ var IconChevronDown2 = primitives2.IconChevronDownOutlineRegular ?? primitives2.IconChevronDownOutline14;
1002
1054
  var ENGINE_OPTIONS2 = [
1003
1055
  { value: "in-process", key: "engineInProcess" },
1004
1056
  { value: "claude-code", key: "engineClaudeCode" },
@@ -1091,9 +1143,10 @@ var noticeDetail = {
1091
1143
  wordBreak: "break-word"
1092
1144
  };
1093
1145
  function LoopEngineComposerSelect(props) {
1094
- const { controller, useSnapshot, sessionId, sessionEngines, switchEngine, t } = props;
1146
+ const { controller, useSnapshot, sessionId, sessionEngines, switchEngine, useSession, t } = props;
1095
1147
  const { status, engine: defaultEngine, showInComposer, writable } = useSnapshot((snapshot) => snapshot);
1096
- const resolved = useEngineOfSession(sessionEngines, sessionId);
1148
+ const running = useSession?.((snapshot) => snapshot.running);
1149
+ const resolved = useEngineOfSession(sessionEngines, sessionId, running);
1097
1150
  const { label, engine, selectedId, pending } = sessionId === void 0 ? defaultFace(defaultEngine) : resolved === void 0 ? READING : triggerFace(resolved);
1098
1151
  const switchReady = sessionId === void 0 || engineSwitchReady(resolved);
1099
1152
  const [open, setOpen] = (0, import_react3.useState)(false);
@@ -1206,7 +1259,7 @@ function LoopEngineComposerSelect(props) {
1206
1259
  labelText,
1207
1260
  pendingText
1208
1261
  ] }),
1209
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: open ? chevronOpen : chevron, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.IconChevronDownOutline14, { size: 14 }) })
1262
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: open ? chevronOpen : chevron, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(IconChevronDown2, { size: 14 }) })
1210
1263
  ]
1211
1264
  }
1212
1265
  )
@@ -1255,12 +1308,12 @@ function decodeLoopEngine(section) {
1255
1308
  }
1256
1309
  var LoopEngineStore = class {
1257
1310
  /**
1258
- * @param scope - the loop engine settings namespace scope.
1311
+ * @param transport - the settings channel the store follows.
1259
1312
  */
1260
- constructor(scope) {
1261
- this.scope = scope;
1313
+ constructor(transport) {
1314
+ this.transport = transport;
1262
1315
  }
1263
- scope;
1316
+ transport;
1264
1317
  /** uSES-safe state source shared by the registered settings section. */
1265
1318
  store = (0, import_dsh_client_store.createSnapshotStore)({
1266
1319
  status: "loading",
@@ -1271,16 +1324,17 @@ var LoopEngineStore = class {
1271
1324
  });
1272
1325
  following;
1273
1326
  saving = false;
1274
- /** Begin following the bound scope and publish its current answer. */
1327
+ /** Begin following the transport and publish its current answer. */
1275
1328
  load() {
1276
- this.following ??= this.scope.subscribe(() => {
1329
+ this.following ??= this.transport.subscribe(() => {
1277
1330
  this.derive();
1278
1331
  });
1279
1332
  this.derive();
1280
1333
  }
1281
1334
  /**
1282
- * Persist the selected engine. Success is judged against the snapshot the
1283
- * write left behind, so a refused write reports error after its recovery.
1335
+ * Persist the selected engine. Success is the transport's accepted answer
1336
+ * (when it reports one) checked against the snapshot the write left behind,
1337
+ * so a refused write reports error after its recovery.
1284
1338
  * @param engine - the engine to select for future Agent turns.
1285
1339
  * @returns whether the write landed.
1286
1340
  */
@@ -1290,14 +1344,14 @@ var LoopEngineStore = class {
1290
1344
  state.status = "saving";
1291
1345
  state.error = null;
1292
1346
  });
1347
+ let accepted = true;
1293
1348
  try {
1294
- await this.scope.set("engine", engine);
1349
+ accepted = await this.transport.set("engine", engine);
1295
1350
  } finally {
1296
1351
  this.saving = false;
1297
1352
  }
1298
1353
  this.derive();
1299
- const { engine: settled } = this.store.getSnapshot();
1300
- const landed = settled === engine;
1354
+ const landed = accepted !== false && this.store.getSnapshot().engine === engine;
1301
1355
  if (!landed) {
1302
1356
  this.store.update((state) => {
1303
1357
  state.status = "unavailable";
@@ -1307,10 +1361,9 @@ var LoopEngineStore = class {
1307
1361
  return landed;
1308
1362
  }
1309
1363
  /**
1310
- * Persist whether the composer shows the engine picker. Success is judged
1311
- * against the snapshot the write left behind, so a refused write reports
1312
- * error after its recovery. Unlike {@link setEngine}, landing does not reload
1313
- * the page — the toggle only changes composer visibility.
1364
+ * Persist whether the composer shows the engine picker. Unlike
1365
+ * {@link setEngine}, landing does not reload the page — the toggle only
1366
+ * changes composer visibility.
1314
1367
  * @param show - whether the chat page composer reveals the engine picker.
1315
1368
  * @returns whether the write landed.
1316
1369
  */
@@ -1320,14 +1373,14 @@ var LoopEngineStore = class {
1320
1373
  state.status = "saving";
1321
1374
  state.error = null;
1322
1375
  });
1376
+ let accepted = true;
1323
1377
  try {
1324
- await this.scope.set("showInComposer", show);
1378
+ accepted = await this.transport.set("showInComposer", show);
1325
1379
  } finally {
1326
1380
  this.saving = false;
1327
1381
  }
1328
1382
  this.derive();
1329
- const { showInComposer: settled } = this.store.getSnapshot();
1330
- const landed = settled === show;
1383
+ const landed = accepted !== false && this.store.getSnapshot().showInComposer === show;
1331
1384
  if (!landed) {
1332
1385
  this.store.update((state) => {
1333
1386
  state.status = "unavailable";
@@ -1336,15 +1389,15 @@ var LoopEngineStore = class {
1336
1389
  }
1337
1390
  return landed;
1338
1391
  }
1339
- /** Stop following the scope. */
1392
+ /** Stop following the transport. */
1340
1393
  dispose() {
1341
1394
  this.following?.();
1342
1395
  this.following = void 0;
1343
1396
  }
1344
1397
  derive() {
1345
1398
  if (this.saving) return;
1346
- const scope = this.scope.getSnapshot();
1347
- switch (scope.status) {
1399
+ const snapshot = this.transport.getSnapshot();
1400
+ switch (snapshot.status) {
1348
1401
  case "loading":
1349
1402
  this.store.update((state) => {
1350
1403
  state.status = "loading";
@@ -1360,64 +1413,109 @@ var LoopEngineStore = class {
1360
1413
  });
1361
1414
  return;
1362
1415
  case "ready": {
1363
- const engine = scope.value?.engine ?? "in-process";
1364
- const showInComposer = scope.value?.showInComposer ?? true;
1416
+ const engine = snapshot.value?.engine ?? "in-process";
1417
+ const showInComposer = snapshot.value?.showInComposer ?? true;
1365
1418
  this.store.update((state) => {
1366
1419
  state.status = "ready";
1367
1420
  state.engine = engine;
1368
1421
  state.showInComposer = showInComposer;
1369
- state.writable = scope.writable;
1422
+ state.writable = snapshot.writable;
1370
1423
  state.error = null;
1371
1424
  });
1372
1425
  return;
1373
1426
  }
1374
1427
  default: {
1375
- const exhaustive = scope.status;
1376
- throw new Error(`unexpected loop engine scope status: ${String(exhaustive)}`);
1428
+ const exhaustive = snapshot.status;
1429
+ throw new Error(`unexpected loop engine form status: ${String(exhaustive)}`);
1377
1430
  }
1378
1431
  }
1379
1432
  }
1380
1433
  };
1381
1434
 
1382
1435
  // src/namespace.ts
1383
- var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
1436
+ var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "loop-engine";
1437
+ var LEGACY_LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
1384
1438
 
1385
1439
  // src/client/index.ts
1386
1440
  var NS = "settings.loop-engine";
1387
- var inject = ["slots", "locale", "settingsScope"];
1441
+ var inject = ["slots", "locale"];
1388
1442
  function apply(ctx) {
1389
1443
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "loop-engine: copy dictionaries");
1390
- const scope = ctx.settingsScope.bind({
1391
- namespace: LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL,
1392
- decode: decodeLoopEngine
1393
- });
1394
- const controller = new LoopEngineStore(scope);
1444
+ installTurnStatusStyles(ctx);
1445
+ const sessionEngines = createSessionEngineCache(ctx);
1446
+ installReloadReturn(ctx);
1447
+ const t = ctx.locale.bind(NS);
1448
+ let source;
1449
+ const listeners = /* @__PURE__ */ new Set();
1450
+ const transport = {
1451
+ getSnapshot: () => source?.getSnapshot() ?? { status: "loading", value: void 0, writable: false },
1452
+ subscribe: (listener) => {
1453
+ listeners.add(listener);
1454
+ return () => {
1455
+ listeners.delete(listener);
1456
+ };
1457
+ },
1458
+ set: (field, value) => source === void 0 ? Promise.resolve(false) : source.set(field, value)
1459
+ };
1460
+ const adopt = (next) => {
1461
+ source = next;
1462
+ const forget = next.subscribe(() => {
1463
+ for (const listener of [...listeners]) listener();
1464
+ });
1465
+ for (const listener of [...listeners]) listener();
1466
+ return () => {
1467
+ forget();
1468
+ if (source === next) source = void 0;
1469
+ };
1470
+ };
1471
+ const controller = new LoopEngineStore(transport);
1395
1472
  ctx.effect(() => {
1396
1473
  controller.load();
1397
1474
  return () => {
1398
1475
  controller.dispose();
1399
1476
  };
1400
1477
  }, "loop-engine: store lifecycle");
1401
- installTurnStatusStyles(ctx);
1402
- const sessionEngines = createSessionEngineCache(ctx);
1403
- installReloadReturn(ctx);
1404
- const t = ctx.locale.bind(NS);
1405
1478
  const injected = () => ({
1406
1479
  controller,
1407
1480
  hooks: { snapshot: controller.store },
1408
1481
  t
1409
1482
  });
1410
- ctx.slots.inject("settings.section", () => ctx.slots.register({
1483
+ const registerPage = () => ctx.slots.inject("settings.section", () => ctx.slots.register({
1411
1484
  name: "settings.section",
1412
1485
  id: "loop-engine",
1413
1486
  order: 30,
1414
1487
  label: () => t("nav"),
1415
1488
  inject: injected
1416
1489
  }, LoopEngineSection));
1417
- ctx.inject(["slots", "conversation"], (scope2) => {
1490
+ ctx.inject(["configForms"], (scope) => {
1491
+ const form = scope.configForms.get(LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL);
1492
+ scope.effect(() => adopt({
1493
+ getSnapshot: () => form.getSnapshot(),
1494
+ subscribe: (listener) => form.subscribe(listener),
1495
+ set: (field, value) => form.set(field, value)
1496
+ }), "loop-engine: settings transport (config forms)");
1497
+ scope.effect(
1498
+ () => scope.configForms.whileServed([LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL], registerPage),
1499
+ "loop-engine: settings page"
1500
+ );
1501
+ });
1502
+ ctx.inject(["settingsScope"], (scope) => {
1503
+ const scopes = scope.settingsScope;
1504
+ const bound = scopes.bind({
1505
+ namespace: LEGACY_LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL,
1506
+ decode: decodeLoopEngine
1507
+ });
1508
+ scope.effect(() => adopt({
1509
+ getSnapshot: () => bound.getSnapshot(),
1510
+ subscribe: (listener) => bound.subscribe(listener),
1511
+ set: (field, value) => bound.set(field, value)
1512
+ }), "loop-engine: settings transport (settings scope)");
1513
+ scope.effect(registerPage, "loop-engine: settings page");
1514
+ });
1515
+ ctx.inject(["slots", "conversation"], (scope) => {
1418
1516
  const badgeInjected = () => ({ sessionEngines, t });
1419
- scope2.effect(() => {
1420
- return scope2.slots.register({
1517
+ scope.effect(() => {
1518
+ return scope.slots.register({
1421
1519
  name: "conversation.session.header.actions",
1422
1520
  id: "loop-engine",
1423
1521
  // Static session context precedes interactive actions (agent-preset's
@@ -1431,7 +1529,7 @@ function apply(ctx) {
1431
1529
  const switchEngine = sessionEngineSwitcher(ctx, (sessionId) => {
1432
1530
  sessionEngines.invalidate(sessionId);
1433
1531
  });
1434
- ctx.inject(["slots", "conversation"], (scope2) => {
1532
+ ctx.inject(["slots", "conversation"], (scope) => {
1435
1533
  const composerInjected = () => ({
1436
1534
  controller,
1437
1535
  hooks: { snapshot: controller.store },
@@ -1439,8 +1537,8 @@ function apply(ctx) {
1439
1537
  switchEngine,
1440
1538
  t
1441
1539
  });
1442
- scope2.effect(() => {
1443
- return scope2.slots.register({
1540
+ scope.effect(() => {
1541
+ return scope.slots.register({
1444
1542
  name: "conversation.input.right",
1445
1543
  id: "loop-engine",
1446
1544
  order: 0,