dsh-loop-engine 0.1.5-rc2 → 0.1.5-rc4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -177
- package/README.zh.md +48 -100
- package/lib/client.js +887 -266
- package/lib/index.js +2212 -914
- package/lib/invariant.js +43 -45
- package/lib/types/agent-preset-ids.d.ts +303 -0
- package/lib/types/client/LoopEngineBadge.d.ts +44 -17
- package/lib/types/client/LoopEngineComposerSelect.d.ts +79 -13
- package/lib/types/client/LoopEngineSection.d.ts +5 -4
- package/lib/types/client/locales.d.ts +133 -7
- package/lib/types/client/reload.d.ts +135 -0
- package/lib/types/client/session-engine.d.ts +474 -0
- package/lib/types/client/store.d.ts +1 -1
- package/lib/types/client/turn-status.d.ts +112 -10
- package/lib/types/client/use-session-engine.d.ts +66 -0
- package/lib/types/commands.d.ts +11 -3
- package/lib/types/driver-core/host-servers.d.ts +106 -0
- package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
- package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +72 -0
- package/lib/types/driver-core/model-handover.d.ts +116 -0
- package/lib/types/driver-core/ownership.d.ts +6 -5
- package/lib/types/driver-core/prompt.d.ts +32 -0
- package/lib/types/driver-core/session-lifetime.d.ts +62 -0
- package/lib/types/driver-core/session-model.d.ts +82 -0
- package/lib/types/engine-claude/agent.d.ts +23 -3
- package/lib/types/engine-claude/loop.d.ts +16 -15
- package/lib/types/engine-codex/agent.d.ts +22 -3
- package/lib/types/engine-codex/appserver/client.d.ts +15 -2
- package/lib/types/engine-codex/loop.d.ts +13 -15
- package/lib/types/engine-codex/model-handover.d.ts +44 -0
- package/lib/types/engine-kimi/acp/client.d.ts +10 -0
- package/lib/types/engine-kimi/agent.d.ts +19 -2
- package/lib/types/engine-kimi/commands.d.ts +18 -14
- package/lib/types/engine-kimi/loop.d.ts +14 -16
- package/lib/types/engine-kimi/model-handover.d.ts +32 -0
- package/lib/types/engine-kimi/process.d.ts +2 -2
- package/lib/types/engine-kimi/types.d.ts +1 -1
- package/lib/types/engine-of-session.d.ts +97 -0
- package/lib/types/engine-pi/agent.d.ts +25 -23
- package/lib/types/engine-pi/loop.d.ts +13 -23
- package/lib/types/engine-pi/model-handover.d.ts +35 -0
- package/lib/types/engine-pi/types.d.ts +2 -2
- package/lib/types/engine-remote.d.ts +192 -0
- package/lib/types/engine-surface.d.ts +36 -0
- package/lib/types/index.d.ts +51 -50
- package/lib/types/invariant.d.ts +8 -5
- package/lib/types/model-selection-reset.d.ts +271 -0
- package/lib/types/patch-manager.d.ts +57 -39
- package/lib/types/preset.d.ts +39 -26
- package/lib/types/provider-route.d.ts +83 -36
- package/lib/types/router-loop.d.ts +406 -0
- package/lib/types/session-engine-store.d.ts +138 -0
- package/lib/types/settings.d.ts +12 -11
- package/package.json +109 -104
package/lib/client.js
CHANGED
|
@@ -29,6 +29,36 @@ module.exports = __toCommonJS(index_exports);
|
|
|
29
29
|
// src/client/LoopEngineSection.tsx
|
|
30
30
|
var import_react = require("react");
|
|
31
31
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
32
|
+
|
|
33
|
+
// src/agent-preset-ids.ts
|
|
34
|
+
var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi", "kimi"];
|
|
35
|
+
var HOSTED_ENGINE_IDS = LOOP_ENGINE_IDS.filter(
|
|
36
|
+
(id) => id !== "in-process"
|
|
37
|
+
);
|
|
38
|
+
function isHostedEngine(engine) {
|
|
39
|
+
return engine !== void 0 && engine !== "in-process";
|
|
40
|
+
}
|
|
41
|
+
function isLoopEngineId(value) {
|
|
42
|
+
return LOOP_ENGINE_IDS.includes(value);
|
|
43
|
+
}
|
|
44
|
+
var LOOP_ENGINE_REFUSAL_CODES = [
|
|
45
|
+
"session-closed",
|
|
46
|
+
"turn-running",
|
|
47
|
+
"subagent-session",
|
|
48
|
+
"not-driven",
|
|
49
|
+
"router-unmounted",
|
|
50
|
+
"record-failed",
|
|
51
|
+
"rebuild-failed"
|
|
52
|
+
];
|
|
53
|
+
function isLoopEngineRefusalCode(value) {
|
|
54
|
+
return LOOP_ENGINE_REFUSAL_CODES.includes(value);
|
|
55
|
+
}
|
|
56
|
+
function hostedEngineOf(session) {
|
|
57
|
+
if (session.kind !== "engine" || session.engine === "in-process") return void 0;
|
|
58
|
+
return session.engine;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/client/LoopEngineSection.tsx
|
|
32
62
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
33
63
|
var ENGINE_OPTIONS = [
|
|
34
64
|
{ value: "in-process", key: "engineInProcess" },
|
|
@@ -142,9 +172,7 @@ function LoopEngineSection(props) {
|
|
|
142
172
|
const value = pending;
|
|
143
173
|
setPending(null);
|
|
144
174
|
if (value !== null) {
|
|
145
|
-
void controller.setEngine(value)
|
|
146
|
-
if (landed) window.location.reload();
|
|
147
|
-
});
|
|
175
|
+
void controller.setEngine(value);
|
|
148
176
|
}
|
|
149
177
|
};
|
|
150
178
|
const cancelSwitch = () => {
|
|
@@ -187,7 +215,7 @@ function LoopEngineSection(props) {
|
|
|
187
215
|
}
|
|
188
216
|
),
|
|
189
217
|
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") }),
|
|
190
|
-
engine
|
|
218
|
+
isHostedEngine(engine) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: notice, children: t("hostedEngineModelNotice") }) : null,
|
|
191
219
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: toggleRow, children: [
|
|
192
220
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
193
221
|
"input",
|
|
@@ -220,6 +248,316 @@ function LoopEngineSection(props) {
|
|
|
220
248
|
] });
|
|
221
249
|
}
|
|
222
250
|
|
|
251
|
+
// src/client/use-session-engine.ts
|
|
252
|
+
var import_react2 = require("react");
|
|
253
|
+
|
|
254
|
+
// src/client/turn-status.ts
|
|
255
|
+
var ENGINE_ATTR = "data-loop-engine";
|
|
256
|
+
var PLUGIN_ID = "dsh-loop-engine";
|
|
257
|
+
var STYLESHEET = `
|
|
258
|
+
[class$="_turnStatus"]::before {
|
|
259
|
+
margin-right: 6px;
|
|
260
|
+
background: none;
|
|
261
|
+
-webkit-background-clip: border-box;
|
|
262
|
+
background-clip: border-box;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/*
|
|
266
|
+
* The row's own sweep, re-asserted for hosted engines.
|
|
267
|
+
*
|
|
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.
|
|
276
|
+
*/
|
|
277
|
+
html[${ENGINE_ATTR}] [class$="_turnStatus"] {
|
|
278
|
+
background-position: 100% 0;
|
|
279
|
+
background-size: 250% 100%;
|
|
280
|
+
animation: le-shimmer 1.8s linear infinite;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@keyframes le-shimmer {
|
|
284
|
+
to { background-position: 0 0; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
html[${ENGINE_ATTR}="claude-code"] [class$="_turnStatus"] {
|
|
288
|
+
--dsw-static-deepseek-500: #d97757;
|
|
289
|
+
--dsw-static-deepseek-200: #f5bda6;
|
|
290
|
+
}
|
|
291
|
+
html[${ENGINE_ATTR}="claude-code"] [class$="_turnStatus"]::before {
|
|
292
|
+
content: "\u273B";
|
|
293
|
+
color: #d97757;
|
|
294
|
+
-webkit-text-fill-color: #d97757;
|
|
295
|
+
animation: le-bloom 1.6s ease-in-out infinite;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
html[${ENGINE_ATTR}="codex"] [class$="_turnStatus"] {
|
|
299
|
+
--dsw-static-deepseek-500: #a9b1c0;
|
|
300
|
+
--dsw-static-deepseek-200: #e6eaf2;
|
|
301
|
+
}
|
|
302
|
+
html[${ENGINE_ATTR}="codex"] [class$="_turnStatus"]::before {
|
|
303
|
+
content: "\u2022";
|
|
304
|
+
color: #a9b1c0;
|
|
305
|
+
-webkit-text-fill-color: #a9b1c0;
|
|
306
|
+
text-shadow: 0 0 6px currentColor;
|
|
307
|
+
animation: le-pulse 1.4s ease-in-out infinite;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
html[${ENGINE_ATTR}="pi"] [class$="_turnStatus"] {
|
|
311
|
+
--dsw-static-deepseek-500: #8e4ec6;
|
|
312
|
+
--dsw-static-deepseek-200: #d6bff0;
|
|
313
|
+
}
|
|
314
|
+
html[${ENGINE_ATTR}="pi"] [class$="_turnStatus"]::before {
|
|
315
|
+
content: "\u280B";
|
|
316
|
+
color: #8e4ec6;
|
|
317
|
+
-webkit-text-fill-color: #8e4ec6;
|
|
318
|
+
font-size: 1.1em;
|
|
319
|
+
animation: le-braille 1s linear infinite;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
html[${ENGINE_ATTR}="kimi"] [class$="_turnStatus"] {
|
|
323
|
+
--dsw-static-deepseek-500: #e5484d;
|
|
324
|
+
--dsw-static-deepseek-200: #f5b2b4;
|
|
325
|
+
}
|
|
326
|
+
html[${ENGINE_ATTR}="kimi"] [class$="_turnStatus"]::before {
|
|
327
|
+
content: "\u{1F317}";
|
|
328
|
+
color: #e5484d;
|
|
329
|
+
-webkit-text-fill-color: #e5484d;
|
|
330
|
+
animation: le-moon 2.5s linear infinite;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* Grows from small to large each cycle \u2014 the opposite of a twinkle. The range
|
|
334
|
+
is deliberately wide (3x) because a bare size change on a thin glyph reads
|
|
335
|
+
weakly otherwise, and the large state is held briefly (50%-62%) so it blooms
|
|
336
|
+
rather than throbs. 1.35x extends ~2.5px per side at the 14px glyph, inside
|
|
337
|
+
the 6px ::before margin. */
|
|
338
|
+
@keyframes le-bloom {
|
|
339
|
+
0%, 100% { transform: scale(0.45); opacity: 0.45; }
|
|
340
|
+
50%, 62% { transform: scale(1.35); opacity: 1; }
|
|
341
|
+
}
|
|
342
|
+
/* A terminal braille spinner. The glyph is an ordinary text character, not an
|
|
343
|
+
emoji, so the engine color actually paints \u2014 a colored emoji silently ignores
|
|
344
|
+
color/-webkit-text-fill-color. Stepping content walks the ten dot frames. */
|
|
345
|
+
@keyframes le-braille {
|
|
346
|
+
0% { content: "\u280B"; }
|
|
347
|
+
10% { content: "\u2819"; }
|
|
348
|
+
20% { content: "\u2839"; }
|
|
349
|
+
30% { content: "\u2838"; }
|
|
350
|
+
40% { content: "\u283C"; }
|
|
351
|
+
50% { content: "\u2834"; }
|
|
352
|
+
60% { content: "\u2826"; }
|
|
353
|
+
70% { content: "\u2827"; }
|
|
354
|
+
80% { content: "\u2807"; }
|
|
355
|
+
90% { content: "\u280F"; }
|
|
356
|
+
100% { content: "\u280B"; }
|
|
357
|
+
}
|
|
358
|
+
/* A soft pulse for the codex dot: the glyph breathes between a small, dim
|
|
359
|
+
point and a larger, full-brightness one \u2014 a light dot, not a spinner. */
|
|
360
|
+
@keyframes le-pulse {
|
|
361
|
+
0%, 100% { transform: scale(0.5); opacity: 0.35; }
|
|
362
|
+
50% { transform: scale(1.3); opacity: 1; }
|
|
363
|
+
}
|
|
364
|
+
/* Moon phases rather than a rigid rotation: a spinning moon bitmap can only
|
|
365
|
+
squash and mirror itself, never show a full or a new moon. Stepping the
|
|
366
|
+
glyph through the phase set sweeps the lit edge across the disc AND actually
|
|
367
|
+
reaches \u{1F315} and \u{1F311}. The order runs waning first (full \u2192 new \u2192 full), so the lit
|
|
368
|
+
edge travels counter-clockwise, and the glyph under the row's first paint
|
|
369
|
+
(\u{1F317}) is a mid-phase rather than a full or a new moon. */
|
|
370
|
+
@keyframes le-moon {
|
|
371
|
+
0% { content: "\u{1F315}"; }
|
|
372
|
+
12.5% { content: "\u{1F316}"; }
|
|
373
|
+
25% { content: "\u{1F317}"; }
|
|
374
|
+
37.5% { content: "\u{1F318}"; }
|
|
375
|
+
50% { content: "\u{1F311}"; }
|
|
376
|
+
62.5% { content: "\u{1F312}"; }
|
|
377
|
+
75% { content: "\u{1F313}"; }
|
|
378
|
+
87.5% { content: "\u{1F314}"; }
|
|
379
|
+
100% { content: "\u{1F315}"; }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
`;
|
|
383
|
+
var focusedSessionId;
|
|
384
|
+
function reflectTurnStatusEngine(sessionId, engine) {
|
|
385
|
+
if (sessionId === void 0 || sessionId !== focusedSessionId) return;
|
|
386
|
+
writeTurnStatusEngine(engine);
|
|
387
|
+
}
|
|
388
|
+
function focusTurnStatusSession(sessionId) {
|
|
389
|
+
focusedSessionId = sessionId;
|
|
390
|
+
}
|
|
391
|
+
function blurTurnStatusSession(sessionId) {
|
|
392
|
+
if (focusedSessionId === sessionId) focusedSessionId = void 0;
|
|
393
|
+
}
|
|
394
|
+
function writeTurnStatusEngine(engine) {
|
|
395
|
+
if (typeof document === "undefined") return;
|
|
396
|
+
const root = document.documentElement;
|
|
397
|
+
if (engine === void 0 || engine === "in-process") {
|
|
398
|
+
delete root.dataset.loopEngine;
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
root.dataset.loopEngine = engine;
|
|
402
|
+
}
|
|
403
|
+
function installTurnStatusStyles(ctx) {
|
|
404
|
+
if (typeof document === "undefined") return;
|
|
405
|
+
ctx.effect(() => {
|
|
406
|
+
const tag = document.createElement("style");
|
|
407
|
+
tag.dataset.plugin = PLUGIN_ID;
|
|
408
|
+
tag.dataset.pluginCss = `${PLUGIN_ID}/turn-status.css`;
|
|
409
|
+
tag.textContent = STYLESHEET;
|
|
410
|
+
document.head.appendChild(tag);
|
|
411
|
+
return () => {
|
|
412
|
+
tag.remove();
|
|
413
|
+
delete document.documentElement.dataset.loopEngine;
|
|
414
|
+
};
|
|
415
|
+
}, "loop-engine: per-engine turn status styles");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/client/use-session-engine.ts
|
|
419
|
+
function useEngineOfSession(cache, sessionId) {
|
|
420
|
+
const [, bump] = (0, import_react2.useState)(0);
|
|
421
|
+
const report = sessionId === void 0 ? void 0 : cache.read(sessionId);
|
|
422
|
+
(0, import_react2.useEffect)(() => {
|
|
423
|
+
if (sessionId === void 0) return;
|
|
424
|
+
return cache.watch(sessionId, () => {
|
|
425
|
+
bump((count) => count + 1);
|
|
426
|
+
});
|
|
427
|
+
}, [cache, sessionId]);
|
|
428
|
+
const engine = report === void 0 ? void 0 : hostedEngineOf(report.engine);
|
|
429
|
+
(0, import_react2.useEffect)(() => {
|
|
430
|
+
if (sessionId === void 0) return;
|
|
431
|
+
focusTurnStatusSession(sessionId);
|
|
432
|
+
reflectTurnStatusEngine(sessionId, engine);
|
|
433
|
+
return () => {
|
|
434
|
+
blurTurnStatusSession(sessionId);
|
|
435
|
+
};
|
|
436
|
+
}, [sessionId, engine]);
|
|
437
|
+
return report;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/client/locales.ts
|
|
441
|
+
var zh = {
|
|
442
|
+
nav: "\u5FAA\u73AF\u5F15\u64CE",
|
|
443
|
+
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",
|
|
444
|
+
engineInProcess: "\u8FDB\u7A0B\u5185\u5F15\u64CE\uFF08\u9ED8\u8BA4\uFF09",
|
|
445
|
+
engineClaudeCode: "Claude Code CLI",
|
|
446
|
+
engineCodex: "Codex CLI",
|
|
447
|
+
enginePi: "Pi CLI",
|
|
448
|
+
engineKimi: "Kimi Code CLI",
|
|
449
|
+
engineLegacy: "\u65E7\u7248\u6258\u7BA1\u5F15\u64CE",
|
|
450
|
+
engineUnrecorded: "\u672A\u8BB0\u5F55",
|
|
451
|
+
engineLoading: "\u8BFB\u53D6\u4E2D\u2026",
|
|
452
|
+
showInComposerLabel: "\u5728\u5BF9\u8BDD\u9875\u663E\u793A\u5F15\u64CE\u9009\u62E9\u5668",
|
|
453
|
+
unavailable: "\u5FAA\u73AF\u5F15\u64CE\u8BBE\u7F6E\u4E0D\u53EF\u7528",
|
|
454
|
+
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",
|
|
455
|
+
saving: "\u4FDD\u5B58\u4E2D\u2026",
|
|
456
|
+
confirmTitle: "\u5207\u6362\u5FAA\u73AF\u5F15\u64CE\uFF1F",
|
|
457
|
+
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",
|
|
458
|
+
confirmAction: "\u5207\u6362",
|
|
459
|
+
cancelAction: "\u53D6\u6D88",
|
|
460
|
+
closeLabel: "\u5173\u95ED",
|
|
461
|
+
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\u56DB\u4E2A\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",
|
|
462
|
+
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",
|
|
463
|
+
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",
|
|
464
|
+
enginePendingPrefix: "\u5207\u5230 ",
|
|
465
|
+
enginePendingSuffix: " \xB7 \u5C1A\u672A\u63A5\u7BA1",
|
|
466
|
+
engineMenuPendingSuffix: "\uFF08\u5C1A\u672A\u63A5\u7BA1\uFF09",
|
|
467
|
+
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",
|
|
468
|
+
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",
|
|
469
|
+
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",
|
|
470
|
+
switchFailedTitle: "\u5207\u6362\u672A\u751F\u6548",
|
|
471
|
+
refusedSessionClosed: "\u8FD9\u6761\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u6253\u5F00\u3002\u5148\u6253\u5F00\u5B83\uFF0C\u518D\u5207\u6362\u5F15\u64CE\u3002",
|
|
472
|
+
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",
|
|
473
|
+
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",
|
|
474
|
+
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",
|
|
475
|
+
refusedRecordFailed: "\u5F15\u64CE\u8BB0\u5F55\u5199\u5165\u5931\u8D25\uFF0C\u5207\u6362\u6CA1\u6709\u751F\u6548\u3002",
|
|
476
|
+
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",
|
|
477
|
+
switchReloadConfirmTitle: "\u5207\u6362\u9700\u8981\u91CD\u65B0\u8F7D\u5165\u9875\u9762",
|
|
478
|
+
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",
|
|
479
|
+
switchReloadConfirmAction: "\u5207\u6362\u5E76\u91CD\u8F7D",
|
|
480
|
+
switchReloadTitle: "\u5207\u6362\u5DF2\u751F\u6548\uFF0C\u6B63\u5728\u91CD\u65B0\u8F7D\u5165\u9875\u9762",
|
|
481
|
+
switchReloadBody: "\u5DF2\u5207\u5230\u65B0\u5F15\u64CE\uFF0C\u6B63\u5728\u91CD\u65B0\u8F7D\u5165\u9875\u9762\u5E76\u56DE\u5230\u672C\u4F1A\u8BDD\u2026",
|
|
482
|
+
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"
|
|
483
|
+
};
|
|
484
|
+
var en = {
|
|
485
|
+
nav: "Loop engine",
|
|
486
|
+
description: "Choose the agent execution engine NEW sessions start on; each session can also pick its own engine on the new-session screen.",
|
|
487
|
+
engineInProcess: "In-process engine (default)",
|
|
488
|
+
engineClaudeCode: "Claude Code CLI",
|
|
489
|
+
engineCodex: "Codex CLI",
|
|
490
|
+
enginePi: "Pi CLI",
|
|
491
|
+
engineKimi: "Kimi Code CLI",
|
|
492
|
+
engineLegacy: "Legacy hosted engine",
|
|
493
|
+
engineUnrecorded: "Not recorded",
|
|
494
|
+
engineLoading: "Reading\u2026",
|
|
495
|
+
showInComposerLabel: "Show the engine selector in the chat page",
|
|
496
|
+
unavailable: "Loop engine settings are unavailable",
|
|
497
|
+
switchNotice: "This only decides what new sessions run; sessions already running keep their own engine.",
|
|
498
|
+
saving: "Saving\u2026",
|
|
499
|
+
confirmTitle: "Switch loop engine?",
|
|
500
|
+
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?",
|
|
501
|
+
confirmAction: "Switch",
|
|
502
|
+
cancelAction: "Cancel",
|
|
503
|
+
closeLabel: "Close",
|
|
504
|
+
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.`,
|
|
505
|
+
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).",
|
|
506
|
+
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.",
|
|
507
|
+
enginePendingPrefix: "\u2192 ",
|
|
508
|
+
enginePendingSuffix: " \xB7 not in force",
|
|
509
|
+
engineMenuPendingSuffix: " (not in force)",
|
|
510
|
+
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.",
|
|
511
|
+
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.",
|
|
512
|
+
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.",
|
|
513
|
+
switchFailedTitle: "Switch not applied",
|
|
514
|
+
refusedSessionClosed: "This session is not open yet. Open it, then switch its engine.",
|
|
515
|
+
refusedTurnRunning: "This session is running \u2014 switch its engine after this turn ends. The turn in flight is not interrupted.",
|
|
516
|
+
refusedSubagentSession: "This is a subagent session; its engine follows the main session that spawned it and cannot be switched on its own.",
|
|
517
|
+
refusedRouterNotReady: "This process cannot switch engines right now (the loop engine router is not ready).",
|
|
518
|
+
refusedRecordFailed: "The engine record could not be written, so the switch did not happen.",
|
|
519
|
+
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.",
|
|
520
|
+
switchReloadConfirmTitle: "This switch reloads the page",
|
|
521
|
+
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?",
|
|
522
|
+
switchReloadConfirmAction: "Switch and reload",
|
|
523
|
+
switchReloadTitle: "Switch applied \u2014 reloading the page",
|
|
524
|
+
switchReloadBody: "Switched. Reloading the page and returning to this session\u2026",
|
|
525
|
+
switchUnavailable: "This page cannot reach the engine switch endpoint, so the session engine was not switched."
|
|
526
|
+
};
|
|
527
|
+
function engineLabelKey2(engine) {
|
|
528
|
+
switch (engine) {
|
|
529
|
+
case "claude-code":
|
|
530
|
+
return "engineClaudeCode";
|
|
531
|
+
case "codex":
|
|
532
|
+
return "engineCodex";
|
|
533
|
+
case "pi":
|
|
534
|
+
return "enginePi";
|
|
535
|
+
case "kimi":
|
|
536
|
+
return "engineKimi";
|
|
537
|
+
default:
|
|
538
|
+
return "engineInProcess";
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function engineStateLabelKey(state) {
|
|
542
|
+
if (state.kind === "engine") return engineLabelKey2(state.engine);
|
|
543
|
+
return state.kind === "legacy" ? "engineLegacy" : "engineUnrecorded";
|
|
544
|
+
}
|
|
545
|
+
function pendingEngineText(t, engine) {
|
|
546
|
+
return `${t("enginePendingPrefix")}${t(engineLabelKey2(engine))}${t("enginePendingSuffix")}`;
|
|
547
|
+
}
|
|
548
|
+
var REFUSAL_FACES = {
|
|
549
|
+
"session-closed": { body: "refusedSessionClosed", detail: false },
|
|
550
|
+
"turn-running": { body: "refusedTurnRunning", detail: false },
|
|
551
|
+
"subagent-session": { body: "refusedSubagentSession", detail: false },
|
|
552
|
+
"not-driven": { body: "refusedRouterNotReady", detail: false },
|
|
553
|
+
"router-unmounted": { body: "refusedRouterNotReady", detail: false },
|
|
554
|
+
"record-failed": { body: "refusedRecordFailed", detail: true },
|
|
555
|
+
"rebuild-failed": { body: "refusedRebuildFailed", detail: true }
|
|
556
|
+
};
|
|
557
|
+
function refusalFace(code) {
|
|
558
|
+
return code === void 0 ? { detail: false } : REFUSAL_FACES[code];
|
|
559
|
+
}
|
|
560
|
+
|
|
223
561
|
// src/client/LoopEngineBadge.tsx
|
|
224
562
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
225
563
|
var pill = {
|
|
@@ -235,23 +573,431 @@ var pill = {
|
|
|
235
573
|
lineHeight: "18px",
|
|
236
574
|
whiteSpace: "nowrap"
|
|
237
575
|
};
|
|
238
|
-
function LoopEngineBadge(props) {
|
|
239
|
-
const {
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
576
|
+
function LoopEngineBadge(props) {
|
|
577
|
+
const { sessionId, sessionEngines, t } = props;
|
|
578
|
+
const report = useEngineOfSession(sessionEngines, sessionId);
|
|
579
|
+
if (sessionId === void 0 || report === void 0) return null;
|
|
580
|
+
const engine = report.engine;
|
|
581
|
+
if (engine.kind === "unset") return null;
|
|
582
|
+
const label = t(engineStateLabelKey(engine));
|
|
583
|
+
const named = engine.kind === "engine" ? engine.engine : void 0;
|
|
584
|
+
const pending = report.pending;
|
|
585
|
+
const notice2 = pending === void 0 ? named === void 0 ? t("legacySessionNotice") : t("sessionNotice") : t("pendingSessionNotice");
|
|
586
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: pill, title: notice2, children: [
|
|
587
|
+
t("nav"),
|
|
588
|
+
" \xB7 ",
|
|
589
|
+
label,
|
|
590
|
+
pending === void 0 ? null : ` \xB7 ${pendingEngineText(t, pending)}`
|
|
591
|
+
] });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/client/LoopEngineComposerSelect.tsx
|
|
595
|
+
var import_react3 = require("react");
|
|
596
|
+
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
597
|
+
|
|
598
|
+
// src/client/reload.ts
|
|
599
|
+
var RELOAD_RETURN_KEY = "dsh-loop-engine:reload-session";
|
|
600
|
+
function browserPage() {
|
|
601
|
+
const globals = globalThis;
|
|
602
|
+
return {
|
|
603
|
+
stash: globals.sessionStorage,
|
|
604
|
+
// Off a browser there is no page to reload; the stash is missing there too,
|
|
605
|
+
// so nothing is ever armed for one.
|
|
606
|
+
reload: () => {
|
|
607
|
+
globals.location?.reload();
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function armReloadReturn(page, sessionId) {
|
|
612
|
+
const stash = page.stash;
|
|
613
|
+
if (stash === void 0) return false;
|
|
614
|
+
try {
|
|
615
|
+
stash.setItem(RELOAD_RETURN_KEY, sessionId);
|
|
616
|
+
return true;
|
|
617
|
+
} catch {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function takeReloadReturn(page) {
|
|
622
|
+
const stash = page.stash;
|
|
623
|
+
if (stash === void 0) return void 0;
|
|
624
|
+
let stashed = null;
|
|
625
|
+
try {
|
|
626
|
+
stashed = stash.getItem(RELOAD_RETURN_KEY);
|
|
627
|
+
} catch {
|
|
628
|
+
return void 0;
|
|
629
|
+
}
|
|
630
|
+
if (stashed === null || stashed.length === 0) return void 0;
|
|
631
|
+
try {
|
|
632
|
+
stash.removeItem(RELOAD_RETURN_KEY);
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
return stashed;
|
|
636
|
+
}
|
|
637
|
+
function restoreReloadReturn(sessions, page, warn) {
|
|
638
|
+
const sessionId = takeReloadReturn(page);
|
|
639
|
+
if (sessionId === void 0) return () => {
|
|
640
|
+
};
|
|
641
|
+
let unsubscribe;
|
|
642
|
+
const settle = () => {
|
|
643
|
+
const snapshot = sessions.list.getSnapshot();
|
|
644
|
+
if (snapshot.phase !== "ready") return;
|
|
645
|
+
unsubscribe?.();
|
|
646
|
+
unsubscribe = void 0;
|
|
647
|
+
if (!snapshot.ids.includes(sessionId)) {
|
|
648
|
+
warn(`loop-engine: the session this page was reloaded for ("${sessionId}") is not in the session list; open it from the list`);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
sessions.open(sessionId);
|
|
653
|
+
} catch (error2) {
|
|
654
|
+
warn(`loop-engine: could not reopen session "${sessionId}" after the reload: ${String(error2)}`);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
unsubscribe = sessions.list.subscribe(settle);
|
|
658
|
+
settle();
|
|
659
|
+
return () => {
|
|
660
|
+
unsubscribe?.();
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function installReloadReturn(ctx, page = browserPage(), warn) {
|
|
664
|
+
ctx.inject(["sessions"], (scope) => {
|
|
665
|
+
const sessions = scope.get("sessions");
|
|
666
|
+
if (sessions === void 0) return;
|
|
667
|
+
const report = warn ?? ((message) => {
|
|
668
|
+
scope.logger.warn(message);
|
|
669
|
+
});
|
|
670
|
+
scope.effect(
|
|
671
|
+
() => restoreReloadReturn(sessions, page, report),
|
|
672
|
+
"loop-engine: reopen the session this page was reloaded for"
|
|
673
|
+
);
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/client/session-engine.ts
|
|
678
|
+
var LOOP_ENGINE_REMOTE_NAMESPACE = "loopEngine";
|
|
679
|
+
var LOOP_ENGINE_REMOTE_METHOD = "engine";
|
|
680
|
+
var LOOP_ENGINE_REMOTE_SELECT_METHOD = "select";
|
|
681
|
+
function engineSwitchReady(report) {
|
|
682
|
+
return report !== void 0;
|
|
683
|
+
}
|
|
684
|
+
function switchNeedsReload(current, target) {
|
|
685
|
+
const from = current.kind === "engine" ? current.engine : "in-process";
|
|
686
|
+
return from === "in-process" !== (target === "in-process");
|
|
687
|
+
}
|
|
688
|
+
var LOOP_ENGINE_PACKAGE = "dsh-loop-engine";
|
|
689
|
+
function parseSessionEngine(value) {
|
|
690
|
+
const kind = value?.kind;
|
|
691
|
+
if (kind === "legacy" || kind === "unset") return { kind };
|
|
692
|
+
const engine = value?.engine;
|
|
693
|
+
if (kind === "engine" && isLoopEngineId(engine)) return { kind, engine };
|
|
694
|
+
throw new TypeError(`loop-engine: "${String(kind)}" is not a session engine state`);
|
|
695
|
+
}
|
|
696
|
+
function parseSessionEngineReport(value) {
|
|
697
|
+
const engine = parseSessionEngine(value?.engine);
|
|
698
|
+
const pending = value?.pending;
|
|
699
|
+
if (pending === void 0) return { engine };
|
|
700
|
+
if (!isLoopEngineId(pending)) {
|
|
701
|
+
throw new TypeError(`loop-engine: "${String(pending)}" is not a session engine state`);
|
|
702
|
+
}
|
|
703
|
+
return { engine, pending };
|
|
704
|
+
}
|
|
705
|
+
function parseSessionId(value) {
|
|
706
|
+
const sessionId = value?.sessionId;
|
|
707
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
708
|
+
throw new TypeError("loop-engine: sessionId must be a non-empty string");
|
|
709
|
+
}
|
|
710
|
+
return sessionId;
|
|
711
|
+
}
|
|
712
|
+
function parseSelectRequest(value) {
|
|
713
|
+
const sessionId = parseSessionId(value);
|
|
714
|
+
const engine = value?.engine;
|
|
715
|
+
if (!isLoopEngineId(engine)) {
|
|
716
|
+
throw new TypeError(`loop-engine: engine must be one of ${LOOP_ENGINE_IDS.join(", ")}`);
|
|
717
|
+
}
|
|
718
|
+
return { sessionId, engine };
|
|
719
|
+
}
|
|
720
|
+
function parseSelectResult(value) {
|
|
721
|
+
const outcome = value ?? {};
|
|
722
|
+
if (outcome.ok === true && isLoopEngineId(outcome.engine)) {
|
|
723
|
+
if (outcome.reload !== void 0 && outcome.reload !== true) {
|
|
724
|
+
throw new TypeError("loop-engine: not an engine switch outcome");
|
|
725
|
+
}
|
|
726
|
+
return outcome.reload === true ? { ok: true, engine: outcome.engine, reload: true } : { ok: true, engine: outcome.engine };
|
|
727
|
+
}
|
|
728
|
+
if (outcome.ok === false && typeof outcome.reason === "string") {
|
|
729
|
+
return isLoopEngineRefusalCode(outcome.code) ? { ok: false, code: outcome.code, reason: outcome.reason } : { ok: false, reason: outcome.reason };
|
|
730
|
+
}
|
|
731
|
+
throw new TypeError("loop-engine: not an engine switch outcome");
|
|
732
|
+
}
|
|
733
|
+
function descriptor(method, request, result) {
|
|
734
|
+
return {
|
|
735
|
+
id: `${LOOP_ENGINE_PACKAGE}#${LOOP_ENGINE_REMOTE_NAMESPACE}/${method}`,
|
|
736
|
+
service: LOOP_ENGINE_REMOTE_NAMESPACE,
|
|
737
|
+
namespace: LOOP_ENGINE_REMOTE_NAMESPACE,
|
|
738
|
+
method,
|
|
739
|
+
invocation: { kind: "direct" },
|
|
740
|
+
parameters: [{
|
|
741
|
+
// The wire name is the HOST method's own parameter name: the Gateway
|
|
742
|
+
// derives an unregistered endpoint's signature from the live method
|
|
743
|
+
// (`methodParameterNames`), and refuses args whose fields do not match it.
|
|
744
|
+
name: "request",
|
|
745
|
+
wire: "request",
|
|
746
|
+
source: "json",
|
|
747
|
+
codec: {
|
|
748
|
+
mode: "strict",
|
|
749
|
+
typeSymbol: `${LOOP_ENGINE_PACKAGE}#${LOOP_ENGINE_REMOTE_NAMESPACE}/${method}:request`,
|
|
750
|
+
schema: request
|
|
751
|
+
}
|
|
752
|
+
}],
|
|
753
|
+
result: {
|
|
754
|
+
mode: "strict",
|
|
755
|
+
typeSymbol: `${LOOP_ENGINE_PACKAGE}#${LOOP_ENGINE_REMOTE_NAMESPACE}/${method}:result`,
|
|
756
|
+
schema: result
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
var LOOP_ENGINE_REMOTE_CONTRIBUTION = {
|
|
761
|
+
package: LOOP_ENGINE_PACKAGE,
|
|
762
|
+
descriptors: [
|
|
763
|
+
descriptor(
|
|
764
|
+
LOOP_ENGINE_REMOTE_METHOD,
|
|
765
|
+
{
|
|
766
|
+
/** Normalize the one-field request the host's method declares. */
|
|
767
|
+
parse(value) {
|
|
768
|
+
return { sessionId: parseSessionId(value) };
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
{ parse: parseSessionEngineReport }
|
|
772
|
+
),
|
|
773
|
+
descriptor(LOOP_ENGINE_REMOTE_SELECT_METHOD, { parse: parseSelectRequest }, { parse: parseSelectResult })
|
|
774
|
+
]
|
|
775
|
+
};
|
|
776
|
+
var SessionEngineCache = class {
|
|
777
|
+
engines = /* @__PURE__ */ new Map();
|
|
778
|
+
watchers = /* @__PURE__ */ new Map();
|
|
779
|
+
reading = /* @__PURE__ */ new Map();
|
|
780
|
+
/**
|
|
781
|
+
* Monotonic read generation per session. A read started before an
|
|
782
|
+
* invalidation describes the session as it was BEFORE the switch, so however
|
|
783
|
+
* late it settles it must not publish — publishing it would put the old engine
|
|
784
|
+
* back on screen, which is the very misreport this cache exists to prevent.
|
|
785
|
+
*/
|
|
786
|
+
generations = /* @__PURE__ */ new Map();
|
|
787
|
+
remote;
|
|
788
|
+
disposed = false;
|
|
789
|
+
/**
|
|
790
|
+
* Attach the mounted Remote namespace. Values asked for before the mount
|
|
791
|
+
* settled are re-read, so a surface that mounted first is not left loading.
|
|
792
|
+
* @param remote - the namespace, or undefined when the mount failed.
|
|
793
|
+
*/
|
|
794
|
+
attach(remote) {
|
|
795
|
+
if (this.disposed) return;
|
|
796
|
+
this.remote = remote;
|
|
797
|
+
for (const sessionId of [...this.watchers.keys()]) this.readIfUnknown(sessionId);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* The engine report cached for one session, or undefined while the first
|
|
801
|
+
* answer is still in flight (or when the host could not answer at all).
|
|
802
|
+
* @param sessionId - the session whose engine is asked for.
|
|
803
|
+
* @returns the authoritative report, cached by session id.
|
|
804
|
+
*/
|
|
805
|
+
read(sessionId) {
|
|
806
|
+
return this.engines.get(sessionId);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Follow one session's engine, re-reading it whenever a session's surfaces
|
|
810
|
+
* appear again.
|
|
811
|
+
*
|
|
812
|
+
* A watch is a surface appearing for this session, and that is the moment the
|
|
813
|
+
* cached answer can be stale in a way nothing else reports: a session whose
|
|
814
|
+
* surfaces all went away and came back — the page switched to another session
|
|
815
|
+
* and back — must not render what the page cached before. A re-mount is not
|
|
816
|
+
* what moves a session's engine (a switch is performed on the host, and either
|
|
817
|
+
* swaps the agent in place or releases the session and reloads this page), but
|
|
818
|
+
* the page's answer is old by then, and a surface that rendered it as current
|
|
819
|
+
* would be the one lying about what runs.
|
|
820
|
+
*
|
|
821
|
+
* Only the FIRST watcher of a session triggers the re-read, which is what
|
|
822
|
+
* makes this "the session's surfaces appeared" rather than "a component
|
|
823
|
+
* rendered": the chip and the composer are two watchers of one session, and
|
|
824
|
+
* the second joins the read the first one started instead of sending its own.
|
|
825
|
+
* While a session keeps its surfaces, nothing here asks again — the answer only
|
|
826
|
+
* changes through {@link invalidate} (a committed switch) or a later re-mount.
|
|
827
|
+
* @param sessionId - the session to follow.
|
|
828
|
+
* @param listener - called whenever that session's cached state changes.
|
|
829
|
+
* @returns the unsubscribe.
|
|
830
|
+
*/
|
|
831
|
+
watch(sessionId, listener) {
|
|
832
|
+
const listeners = this.watchers.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
833
|
+
const appearing = listeners.size === 0;
|
|
834
|
+
listeners.add(listener);
|
|
835
|
+
this.watchers.set(sessionId, listeners);
|
|
836
|
+
if (appearing) this.refresh(sessionId);
|
|
837
|
+
return () => {
|
|
838
|
+
if (this.watchers.get(sessionId) !== listeners) return;
|
|
839
|
+
listeners.delete(listener);
|
|
840
|
+
if (listeners.size === 0) this.watchers.delete(sessionId);
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Ask the host for one session's engine report again, superseding nothing.
|
|
845
|
+
*
|
|
846
|
+
* This is the way a surface that has just been rendered asks "is what this page
|
|
847
|
+
* cached still what runs?" — the answer that can go stale without anyone
|
|
848
|
+
* invalidating it, because the page is not the only thing that can move a
|
|
849
|
+
* session (another window, a hot swap elsewhere, a plugin reload).
|
|
850
|
+
* A read already in flight for that session is joined rather than replaced (it
|
|
851
|
+
* was started no earlier than this call and describes the same moment), and the
|
|
852
|
+
* cached answer is kept until the new one lands, so a re-read never blinks the
|
|
853
|
+
* chip back to "reading". Use {@link invalidate} instead for an answer that is
|
|
854
|
+
* known to be WRONG.
|
|
855
|
+
* @param sessionId - the session whose answer should be re-read.
|
|
856
|
+
*/
|
|
857
|
+
refresh(sessionId) {
|
|
858
|
+
if (this.disposed) return;
|
|
859
|
+
if (this.reading.has(sessionId)) return;
|
|
860
|
+
this.start(sessionId);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Forget one session's cached engine and ask the host again.
|
|
864
|
+
*
|
|
865
|
+
* A successful engine switch changes what the host answers for that session,
|
|
866
|
+
* so the picker MUST come through here: without it the composer would keep
|
|
867
|
+
* showing the engine the session no longer runs. Any read already in flight
|
|
868
|
+
* for that session is superseded and can no longer publish.
|
|
869
|
+
* @param sessionId - the session whose answer is now stale.
|
|
870
|
+
*/
|
|
871
|
+
invalidate(sessionId) {
|
|
872
|
+
this.engines.delete(sessionId);
|
|
873
|
+
this.generations.set(sessionId, (this.generations.get(sessionId) ?? 0) + 1);
|
|
874
|
+
this.reading.delete(sessionId);
|
|
875
|
+
this.publish(sessionId);
|
|
876
|
+
this.readIfUnknown(sessionId);
|
|
877
|
+
}
|
|
878
|
+
/** Drop every cached answer and notification (plugin unload). */
|
|
879
|
+
dispose() {
|
|
880
|
+
this.disposed = true;
|
|
881
|
+
this.remote = void 0;
|
|
882
|
+
this.engines.clear();
|
|
883
|
+
this.reading.clear();
|
|
884
|
+
this.generations.clear();
|
|
885
|
+
this.watchers.clear();
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Ask unless the session is already answered, being read, or unanswerable.
|
|
889
|
+
*
|
|
890
|
+
* For the paths that need an answer and have none: the plugin's mount settling
|
|
891
|
+
* (a surface asked before the Remote existed), a session whose cached answer
|
|
892
|
+
* was just dropped — and never a re-read of an answer this page already holds,
|
|
893
|
+
* which is {@link refresh}'s job and only a freshly rendered surface may ask
|
|
894
|
+
* for.
|
|
895
|
+
*/
|
|
896
|
+
readIfUnknown(sessionId) {
|
|
897
|
+
if (this.engines.has(sessionId) || this.reading.has(sessionId)) return;
|
|
898
|
+
this.start(sessionId);
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Start one read for a session, unless there is nothing to read it with.
|
|
902
|
+
* @param sessionId - the session to read.
|
|
903
|
+
*/
|
|
904
|
+
start(sessionId) {
|
|
905
|
+
const remote = this.remote;
|
|
906
|
+
if (this.disposed || remote === void 0) return;
|
|
907
|
+
const generation = (this.generations.get(sessionId) ?? 0) + 1;
|
|
908
|
+
this.generations.set(sessionId, generation);
|
|
909
|
+
const read = this.ask(remote, sessionId, generation);
|
|
910
|
+
this.reading.set(sessionId, read);
|
|
911
|
+
void read.then(() => {
|
|
912
|
+
if (this.reading.get(sessionId) === read) this.reading.delete(sessionId);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* One read, quiet on failure: a session the host cannot answer for stays
|
|
917
|
+
* unknown, and the surfaces keep their neutral state instead of claiming an
|
|
918
|
+
* engine. A refusal is not cached as an answer, so the next watch re-asks.
|
|
919
|
+
* @param remote - the mounted namespace.
|
|
920
|
+
* @param sessionId - the session asked about.
|
|
921
|
+
* @param generation - the read generation this answer belongs to.
|
|
922
|
+
*/
|
|
923
|
+
async ask(remote, sessionId, generation) {
|
|
924
|
+
let result;
|
|
925
|
+
try {
|
|
926
|
+
result = await remote.engine({ sessionId });
|
|
927
|
+
} catch {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (!result.ok || this.disposed || this.generations.get(sessionId) !== generation) return;
|
|
931
|
+
let report;
|
|
932
|
+
try {
|
|
933
|
+
report = parseSessionEngineReport(result.value);
|
|
934
|
+
} catch {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
this.engines.set(sessionId, report);
|
|
938
|
+
this.publish(sessionId);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Notify one session's watchers.
|
|
942
|
+
*
|
|
943
|
+
* Nothing is painted here. The session's engine shows up in the chat
|
|
944
|
+
* turn-status row too, but that row hangs off a DOCUMENT-level attribute, whose
|
|
945
|
+
* only entitled owner is the session on screen — and a publish says nothing
|
|
946
|
+
* about what is on screen (this is called for every session a surface has ever
|
|
947
|
+
* watched, and for a session the user has already left, when its late answer
|
|
948
|
+
* lands). So painting here let a background session take the attribute over
|
|
949
|
+
* from the session on screen. The surfaces reflect instead, with the focus
|
|
950
|
+
* guard in `./turn-status.ts`; this is the notification the chip and the
|
|
951
|
+
* composer wake up on.
|
|
952
|
+
*/
|
|
953
|
+
publish(sessionId) {
|
|
954
|
+
for (const listener of this.watchers.get(sessionId) ?? []) listener();
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
function createSessionEngineCache(ctx) {
|
|
958
|
+
const cache = new SessionEngineCache();
|
|
959
|
+
ctx.inject(["remote"], (remoteCtx) => {
|
|
960
|
+
const remote = remoteCtx.get("remote");
|
|
961
|
+
if (remote === void 0) return;
|
|
962
|
+
remote.$mount(LOOP_ENGINE_REMOTE_CONTRIBUTION).then(
|
|
963
|
+
() => {
|
|
964
|
+
cache.attach(remoteCtx.get(`remote.${LOOP_ENGINE_REMOTE_NAMESPACE}`));
|
|
965
|
+
},
|
|
966
|
+
(error2) => {
|
|
967
|
+
ctx.logger.warn(`loop-engine: could not mount the session engine Remote: ${String(error2)}`);
|
|
968
|
+
}
|
|
969
|
+
);
|
|
970
|
+
});
|
|
971
|
+
ctx.effect(() => () => {
|
|
972
|
+
cache.dispose();
|
|
973
|
+
}, "loop-engine: session engine cache");
|
|
974
|
+
return cache;
|
|
975
|
+
}
|
|
976
|
+
function sessionEngineSwitcher(ctx, onSwitched, page = browserPage()) {
|
|
977
|
+
return async (sessionId, engine) => {
|
|
978
|
+
const remote = ctx.get(`remote.${LOOP_ENGINE_REMOTE_NAMESPACE}`);
|
|
979
|
+
if (remote === void 0) return { ok: false, kind: "unavailable" };
|
|
980
|
+
let result;
|
|
981
|
+
try {
|
|
982
|
+
result = await remote.select({ sessionId, engine });
|
|
983
|
+
} catch (error2) {
|
|
984
|
+
return { ok: false, kind: "refused", reason: error2 instanceof Error ? error2.message : String(error2) };
|
|
985
|
+
}
|
|
986
|
+
if (!result.ok) {
|
|
987
|
+
return { ok: false, kind: "refused", reason: result.error.message };
|
|
988
|
+
}
|
|
989
|
+
if (!result.value.ok) {
|
|
990
|
+
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 };
|
|
991
|
+
}
|
|
992
|
+
onSwitched?.(sessionId);
|
|
993
|
+
if (result.value.reload !== true) return { ok: true };
|
|
994
|
+
armReloadReturn(page, sessionId);
|
|
995
|
+
page.reload();
|
|
996
|
+
return { ok: true, reload: true };
|
|
997
|
+
};
|
|
250
998
|
}
|
|
251
999
|
|
|
252
1000
|
// src/client/LoopEngineComposerSelect.tsx
|
|
253
|
-
var import_react2 = require("react");
|
|
254
|
-
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
255
1001
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
256
1002
|
var ENGINE_OPTIONS2 = [
|
|
257
1003
|
{ value: "in-process", key: "engineInProcess" },
|
|
@@ -260,19 +1006,14 @@ var ENGINE_OPTIONS2 = [
|
|
|
260
1006
|
{ value: "pi", key: "enginePi" },
|
|
261
1007
|
{ value: "kimi", key: "engineKimi" }
|
|
262
1008
|
];
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
case "kimi":
|
|
272
|
-
return "engineKimi";
|
|
273
|
-
default:
|
|
274
|
-
return "engineInProcess";
|
|
275
|
-
}
|
|
1009
|
+
var READING = { label: "engineLoading", engine: void 0, selectedId: void 0 };
|
|
1010
|
+
function triggerFace(report) {
|
|
1011
|
+
const { engine: state, pending } = report;
|
|
1012
|
+
const face = state.kind === "engine" ? { label: engineLabelKey2(state.engine), engine: state.engine, selectedId: state.engine } : { label: engineStateLabelKey(state), engine: void 0, selectedId: void 0 };
|
|
1013
|
+
return pending === void 0 ? face : { ...face, pending };
|
|
1014
|
+
}
|
|
1015
|
+
function defaultFace(fallback) {
|
|
1016
|
+
return { label: engineLabelKey2(fallback), engine: fallback, selectedId: fallback };
|
|
276
1017
|
}
|
|
277
1018
|
function engineGlyph(engine, size = 16) {
|
|
278
1019
|
switch (engine) {
|
|
@@ -331,40 +1072,88 @@ var chevron = {
|
|
|
331
1072
|
transition: "transform 120ms ease"
|
|
332
1073
|
};
|
|
333
1074
|
var chevronOpen = { ...chevron, transform: "rotate(180deg)" };
|
|
334
|
-
var
|
|
1075
|
+
var rowPendingMark = {
|
|
1076
|
+
marginLeft: 4,
|
|
1077
|
+
color: "var(--dsw-alias-label-caption)",
|
|
1078
|
+
fontSize: 12
|
|
1079
|
+
};
|
|
1080
|
+
var noticeBody = {
|
|
335
1081
|
margin: 0,
|
|
336
1082
|
fontSize: 13,
|
|
337
1083
|
lineHeight: 1.55,
|
|
338
1084
|
color: "var(--dsw-alias-label-secondary)"
|
|
339
1085
|
};
|
|
1086
|
+
var noticeDetail = {
|
|
1087
|
+
margin: 0,
|
|
1088
|
+
fontSize: 12,
|
|
1089
|
+
lineHeight: 1.5,
|
|
1090
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
1091
|
+
wordBreak: "break-word"
|
|
1092
|
+
};
|
|
340
1093
|
function LoopEngineComposerSelect(props) {
|
|
341
|
-
const { controller, useSnapshot, t } = props;
|
|
342
|
-
const { status, engine, showInComposer, writable } = useSnapshot((snapshot) => snapshot);
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
const
|
|
1094
|
+
const { controller, useSnapshot, sessionId, sessionEngines, switchEngine, t } = props;
|
|
1095
|
+
const { status, engine: defaultEngine, showInComposer, writable } = useSnapshot((snapshot) => snapshot);
|
|
1096
|
+
const resolved = useEngineOfSession(sessionEngines, sessionId);
|
|
1097
|
+
const { label, engine, selectedId, pending } = sessionId === void 0 ? defaultFace(defaultEngine) : resolved === void 0 ? READING : triggerFace(resolved);
|
|
1098
|
+
const switchReady = sessionId === void 0 || engineSwitchReady(resolved);
|
|
1099
|
+
const [open, setOpen] = (0, import_react3.useState)(false);
|
|
1100
|
+
const [hovered, setHovered] = (0, import_react3.useState)(false);
|
|
1101
|
+
const [notice2, setNotice] = (0, import_react3.useState)(null);
|
|
1102
|
+
const [pendingReload, setPendingReload] = (0, import_react3.useState)(null);
|
|
1103
|
+
const triggerRef = (0, import_react3.useRef)(null);
|
|
347
1104
|
if (status !== "ready" || !showInComposer) return null;
|
|
348
|
-
const disabled = !writable;
|
|
349
|
-
const
|
|
350
|
-
const
|
|
1105
|
+
const disabled = !writable || !switchReady;
|
|
1106
|
+
const labelText = t(label);
|
|
1107
|
+
const pendingText = pending === void 0 ? void 0 : ` \xB7 ${pendingEngineText(t, pending)}`;
|
|
1108
|
+
const title = pending !== void 0 ? t("pendingComposerHint") : isHostedEngine(engine) ? t("hostedEngineModelNotice") : sessionId === void 0 ? t("description") : t("composerHint");
|
|
1109
|
+
const commitSwitch = (value) => {
|
|
1110
|
+
if (sessionId === void 0) {
|
|
1111
|
+
void controller.setEngine(value);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
void switchEngine(sessionId, value).then((result) => {
|
|
1115
|
+
if (!result.ok) {
|
|
1116
|
+
if (result.kind === "unavailable") {
|
|
1117
|
+
setNotice({ title: "switchFailedTitle", body: t("switchUnavailable") });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const face = refusalFace(result.code);
|
|
1121
|
+
setNotice({
|
|
1122
|
+
title: "switchFailedTitle",
|
|
1123
|
+
body: face.body === void 0 ? result.reason : t(face.body),
|
|
1124
|
+
...face.detail ? { detail: result.reason } : {}
|
|
1125
|
+
});
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (result.reload === true) {
|
|
1129
|
+
setNotice({ title: "switchReloadTitle", body: t("switchReloadBody") });
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
};
|
|
351
1134
|
const onSelect = (next) => {
|
|
352
1135
|
setOpen(false);
|
|
353
1136
|
const value = next;
|
|
354
|
-
if (value ===
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
void controller.setEngine(value).then((landed) => {
|
|
362
|
-
if (landed) window.location.reload();
|
|
363
|
-
});
|
|
1137
|
+
if (value === selectedId) return;
|
|
1138
|
+
if (sessionId !== void 0) {
|
|
1139
|
+
if (!engineSwitchReady(resolved)) return;
|
|
1140
|
+
if (switchNeedsReload(resolved.engine, value)) {
|
|
1141
|
+
setPendingReload(value);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
364
1144
|
}
|
|
1145
|
+
commitSwitch(value);
|
|
365
1146
|
};
|
|
366
|
-
const
|
|
367
|
-
|
|
1147
|
+
const confirmReload = () => {
|
|
1148
|
+
const value = pendingReload;
|
|
1149
|
+
setPendingReload(null);
|
|
1150
|
+
if (value !== null) commitSwitch(value);
|
|
1151
|
+
};
|
|
1152
|
+
const cancelReload = () => {
|
|
1153
|
+
setPendingReload(null);
|
|
1154
|
+
};
|
|
1155
|
+
const dismissNotice = () => {
|
|
1156
|
+
setNotice(null);
|
|
368
1157
|
};
|
|
369
1158
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
370
1159
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -376,10 +1165,18 @@ function LoopEngineComposerSelect(props) {
|
|
|
376
1165
|
},
|
|
377
1166
|
items: ENGINE_OPTIONS2.map((option) => ({
|
|
378
1167
|
id: option.value,
|
|
379
|
-
|
|
1168
|
+
// The recorded engine's own row carries the marker too: the trigger may
|
|
1169
|
+
// be truncated to ellipsis, and a user who opens the list has to be
|
|
1170
|
+
// able to see which row was recorded instead of concluding the pick
|
|
1171
|
+
// did nothing. The check mark stays on the row in force (selectedId),
|
|
1172
|
+
// which is never the marked one.
|
|
1173
|
+
label: option.value === pending ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1174
|
+
t(option.key),
|
|
1175
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: rowPendingMark, children: t("engineMenuPendingSuffix") })
|
|
1176
|
+
] }) : t(option.key),
|
|
380
1177
|
icon: engineGlyph(option.value)
|
|
381
1178
|
})),
|
|
382
|
-
selectedId
|
|
1179
|
+
selectedId,
|
|
383
1180
|
onSelect,
|
|
384
1181
|
align: "start",
|
|
385
1182
|
portal: true,
|
|
@@ -404,8 +1201,11 @@ function LoopEngineComposerSelect(props) {
|
|
|
404
1201
|
setOpen(!open);
|
|
405
1202
|
},
|
|
406
1203
|
children: [
|
|
407
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: triggerIcon, "aria-hidden": true, children: engineGlyph(engine, 14) }),
|
|
408
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
1204
|
+
engine === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: triggerIcon, "aria-hidden": true, children: engineGlyph(engine, 14) }),
|
|
1205
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: triggerLabel, children: [
|
|
1206
|
+
labelText,
|
|
1207
|
+
pendingText
|
|
1208
|
+
] }),
|
|
409
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 }) })
|
|
410
1210
|
]
|
|
411
1211
|
}
|
|
@@ -415,15 +1215,29 @@ function LoopEngineComposerSelect(props) {
|
|
|
415
1215
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
416
1216
|
import_dsh_client_ui_primitives2.Modal,
|
|
417
1217
|
{
|
|
418
|
-
open:
|
|
419
|
-
onClose:
|
|
420
|
-
title: t("
|
|
421
|
-
closeLabel: t("
|
|
1218
|
+
open: pendingReload !== null,
|
|
1219
|
+
onClose: cancelReload,
|
|
1220
|
+
title: t("switchReloadConfirmTitle"),
|
|
1221
|
+
closeLabel: t("cancelAction"),
|
|
422
1222
|
footer: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
423
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "outline", onClick:
|
|
424
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "primary", onClick:
|
|
1223
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "outline", onClick: cancelReload, children: t("cancelAction") }),
|
|
1224
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "primary", onClick: confirmReload, children: t("switchReloadConfirmAction") })
|
|
425
1225
|
] }),
|
|
426
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style:
|
|
1226
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: noticeBody, children: t("switchReloadConfirmBody") })
|
|
1227
|
+
}
|
|
1228
|
+
),
|
|
1229
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1230
|
+
import_dsh_client_ui_primitives2.Modal,
|
|
1231
|
+
{
|
|
1232
|
+
open: notice2 !== null,
|
|
1233
|
+
onClose: dismissNotice,
|
|
1234
|
+
title: t(notice2?.title ?? "switchFailedTitle"),
|
|
1235
|
+
closeLabel: t("closeLabel"),
|
|
1236
|
+
footer: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_dsh_client_ui_primitives2.Button, { variant: "primary", onClick: dismissNotice, children: t("closeLabel") }),
|
|
1237
|
+
children: [
|
|
1238
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: noticeBody, children: notice2?.body }),
|
|
1239
|
+
notice2?.detail === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: noticeDetail, children: notice2.detail })
|
|
1240
|
+
]
|
|
427
1241
|
}
|
|
428
1242
|
)
|
|
429
1243
|
] });
|
|
@@ -565,203 +1379,6 @@ var LoopEngineStore = class {
|
|
|
565
1379
|
}
|
|
566
1380
|
};
|
|
567
1381
|
|
|
568
|
-
// src/client/turn-status.ts
|
|
569
|
-
var ENGINE_ATTR = "data-loop-engine";
|
|
570
|
-
var PLUGIN_ID = "dsh-loop-engine";
|
|
571
|
-
var STYLESHEET = `
|
|
572
|
-
[class$="_turnStatus"]::before {
|
|
573
|
-
margin-right: 6px;
|
|
574
|
-
background: none;
|
|
575
|
-
-webkit-background-clip: border-box;
|
|
576
|
-
background-clip: border-box;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/*
|
|
580
|
-
* The row's own sweep, re-asserted for hosted engines.
|
|
581
|
-
*
|
|
582
|
-
* ui-chat disables that animation under \`prefers-reduced-motion: reduce\`
|
|
583
|
-
* (ChatView.module.css), and a media query carries no specificity \u2014 so this
|
|
584
|
-
* attribute-gated rule outranks it. That is deliberate here: deployment images
|
|
585
|
-
* ship with Windows' client-area animation off (SPI_GETCLIENTAREAANIMATION
|
|
586
|
-
* false), and with the guard in force EVERY indicator on this row is frozen \u2014
|
|
587
|
-
* the sweep and the glyph alike. Scoped to hosted engines, so in-process
|
|
588
|
-
* sessions keep the stock reduced-motion behaviour; delete this rule and
|
|
589
|
-
* restore the guard at the foot of the sheet to hand the decision back to the OS.
|
|
590
|
-
*/
|
|
591
|
-
html[${ENGINE_ATTR}] [class$="_turnStatus"] {
|
|
592
|
-
background-position: 100% 0;
|
|
593
|
-
background-size: 250% 100%;
|
|
594
|
-
animation: le-shimmer 1.8s linear infinite;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
@keyframes le-shimmer {
|
|
598
|
-
to { background-position: 0 0; }
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
html[${ENGINE_ATTR}="claude-code"] [class$="_turnStatus"] {
|
|
602
|
-
--dsw-static-deepseek-500: #d97757;
|
|
603
|
-
--dsw-static-deepseek-200: #f5bda6;
|
|
604
|
-
}
|
|
605
|
-
html[${ENGINE_ATTR}="claude-code"] [class$="_turnStatus"]::before {
|
|
606
|
-
content: "\u273B";
|
|
607
|
-
color: #d97757;
|
|
608
|
-
-webkit-text-fill-color: #d97757;
|
|
609
|
-
animation: le-bloom 1.6s ease-in-out infinite;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
html[${ENGINE_ATTR}="codex"] [class$="_turnStatus"] {
|
|
613
|
-
--dsw-static-deepseek-500: #a9b1c0;
|
|
614
|
-
--dsw-static-deepseek-200: #e6eaf2;
|
|
615
|
-
}
|
|
616
|
-
html[${ENGINE_ATTR}="codex"] [class$="_turnStatus"]::before {
|
|
617
|
-
content: "\u2022";
|
|
618
|
-
color: #a9b1c0;
|
|
619
|
-
-webkit-text-fill-color: #a9b1c0;
|
|
620
|
-
text-shadow: 0 0 6px currentColor;
|
|
621
|
-
animation: le-pulse 1.4s ease-in-out infinite;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
html[${ENGINE_ATTR}="pi"] [class$="_turnStatus"] {
|
|
625
|
-
--dsw-static-deepseek-500: #8e4ec6;
|
|
626
|
-
--dsw-static-deepseek-200: #d6bff0;
|
|
627
|
-
}
|
|
628
|
-
html[${ENGINE_ATTR}="pi"] [class$="_turnStatus"]::before {
|
|
629
|
-
content: "\u280B";
|
|
630
|
-
color: #8e4ec6;
|
|
631
|
-
-webkit-text-fill-color: #8e4ec6;
|
|
632
|
-
font-size: 1.1em;
|
|
633
|
-
animation: le-braille 1s linear infinite;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
html[${ENGINE_ATTR}="kimi"] [class$="_turnStatus"] {
|
|
637
|
-
--dsw-static-deepseek-500: #e5484d;
|
|
638
|
-
--dsw-static-deepseek-200: #f5b2b4;
|
|
639
|
-
}
|
|
640
|
-
html[${ENGINE_ATTR}="kimi"] [class$="_turnStatus"]::before {
|
|
641
|
-
content: "\u{1F317}";
|
|
642
|
-
color: #e5484d;
|
|
643
|
-
-webkit-text-fill-color: #e5484d;
|
|
644
|
-
animation: le-moon 2.5s linear infinite;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/* Grows from small to large each cycle \u2014 the opposite of a twinkle. The range
|
|
648
|
-
is deliberately wide (3x) because a bare size change on a thin glyph reads
|
|
649
|
-
weakly otherwise, and the large state is held briefly (50%-62%) so it blooms
|
|
650
|
-
rather than throbs. 1.35x extends ~2.5px per side at the 14px glyph, inside
|
|
651
|
-
the 6px ::before margin. */
|
|
652
|
-
@keyframes le-bloom {
|
|
653
|
-
0%, 100% { transform: scale(0.45); opacity: 0.45; }
|
|
654
|
-
50%, 62% { transform: scale(1.35); opacity: 1; }
|
|
655
|
-
}
|
|
656
|
-
/* A terminal braille spinner. The glyph is an ordinary text character, not an
|
|
657
|
-
emoji, so the engine color actually paints \u2014 a colored emoji silently ignores
|
|
658
|
-
color/-webkit-text-fill-color. Stepping content walks the ten dot frames. */
|
|
659
|
-
@keyframes le-braille {
|
|
660
|
-
0% { content: "\u280B"; }
|
|
661
|
-
10% { content: "\u2819"; }
|
|
662
|
-
20% { content: "\u2839"; }
|
|
663
|
-
30% { content: "\u2838"; }
|
|
664
|
-
40% { content: "\u283C"; }
|
|
665
|
-
50% { content: "\u2834"; }
|
|
666
|
-
60% { content: "\u2826"; }
|
|
667
|
-
70% { content: "\u2827"; }
|
|
668
|
-
80% { content: "\u2807"; }
|
|
669
|
-
90% { content: "\u280F"; }
|
|
670
|
-
100% { content: "\u280B"; }
|
|
671
|
-
}
|
|
672
|
-
/* A soft pulse for the codex dot: the glyph breathes between a small, dim
|
|
673
|
-
point and a larger, full-brightness one \u2014 a light dot, not a spinner. */
|
|
674
|
-
@keyframes le-pulse {
|
|
675
|
-
0%, 100% { transform: scale(0.5); opacity: 0.35; }
|
|
676
|
-
50% { transform: scale(1.3); opacity: 1; }
|
|
677
|
-
}
|
|
678
|
-
/* Moon phases rather than a rigid rotation: a spinning moon bitmap can only
|
|
679
|
-
squash and mirror itself, never show a full or a new moon. Stepping the
|
|
680
|
-
glyph through the phase set sweeps the lit edge across the disc AND actually
|
|
681
|
-
reaches \u{1F315} and \u{1F311}. The order runs waning (full \u2192 new), so the lit edge
|
|
682
|
-
retreats right-to-left \u2014 the direction the user picked. */
|
|
683
|
-
@keyframes le-moon {
|
|
684
|
-
0% { content: "\u{1F315}"; }
|
|
685
|
-
12.5% { content: "\u{1F314}"; }
|
|
686
|
-
25% { content: "\u{1F313}"; }
|
|
687
|
-
37.5% { content: "\u{1F312}"; }
|
|
688
|
-
50% { content: "\u{1F311}"; }
|
|
689
|
-
62.5% { content: "\u{1F318}"; }
|
|
690
|
-
75% { content: "\u{1F317}"; }
|
|
691
|
-
87.5% { content: "\u{1F316}"; }
|
|
692
|
-
100% { content: "\u{1F315}"; }
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
`;
|
|
696
|
-
function reflect(engine, settled) {
|
|
697
|
-
const root = document.documentElement;
|
|
698
|
-
if (!settled || engine === "in-process") {
|
|
699
|
-
delete root.dataset.loopEngine;
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
root.dataset.loopEngine = engine;
|
|
703
|
-
}
|
|
704
|
-
function installTurnStatusStyles(ctx, store) {
|
|
705
|
-
if (typeof document === "undefined") return;
|
|
706
|
-
ctx.effect(() => {
|
|
707
|
-
const tag = document.createElement("style");
|
|
708
|
-
tag.dataset.plugin = PLUGIN_ID;
|
|
709
|
-
tag.dataset.pluginCss = `${PLUGIN_ID}/turn-status.css`;
|
|
710
|
-
tag.textContent = STYLESHEET;
|
|
711
|
-
document.head.appendChild(tag);
|
|
712
|
-
return () => {
|
|
713
|
-
tag.remove();
|
|
714
|
-
delete document.documentElement.dataset.loopEngine;
|
|
715
|
-
};
|
|
716
|
-
}, "loop-engine: per-engine turn status styles");
|
|
717
|
-
const sync = () => {
|
|
718
|
-
const { status, engine } = store.getSnapshot();
|
|
719
|
-
reflect(engine, status === "ready");
|
|
720
|
-
};
|
|
721
|
-
ctx.effect(() => store.subscribe(sync), "loop-engine: turn status engine reflection");
|
|
722
|
-
sync();
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// src/client/locales.ts
|
|
726
|
-
var zh = {
|
|
727
|
-
nav: "\u5FAA\u73AF\u5F15\u64CE",
|
|
728
|
-
description: "\u9009\u62E9\u9A71\u52A8\u4F1A\u8BDD\u7684 Agent \u6267\u884C\u5F15\u64CE\uFF0C\u5207\u6362\u5BF9\u540E\u7EED\u4F1A\u8BDD\u751F\u6548\u3002",
|
|
729
|
-
engineInProcess: "\u8FDB\u7A0B\u5185\u5F15\u64CE\uFF08\u9ED8\u8BA4\uFF09",
|
|
730
|
-
engineClaudeCode: "Claude Code CLI",
|
|
731
|
-
engineCodex: "Codex CLI",
|
|
732
|
-
enginePi: "Pi CLI",
|
|
733
|
-
engineKimi: "Kimi Code CLI",
|
|
734
|
-
showInComposerLabel: "\u5728\u5BF9\u8BDD\u9875\u663E\u793A\u5F15\u64CE\u9009\u62E9\u5668",
|
|
735
|
-
unavailable: "\u5FAA\u73AF\u5F15\u64CE\u8BBE\u7F6E\u4E0D\u53EF\u7528",
|
|
736
|
-
switchNotice: "\u5207\u6362\u5F15\u64CE\u4F1A\u4E2D\u65AD\u5F53\u524D\u4F7F\u7528\u65E7\u5F15\u64CE\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u3002",
|
|
737
|
-
saving: "\u4FDD\u5B58\u4E2D\u2026",
|
|
738
|
-
confirmTitle: "\u5207\u6362\u5FAA\u73AF\u5F15\u64CE\uFF1F",
|
|
739
|
-
confirmBody: "\u5207\u6362\u4F1A\u4E2D\u65AD\u5F53\u524D\u4F7F\u7528\u65E7\u5F15\u64CE\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\uFF0C\u786E\u8BA4\u540E\u9875\u9762\u5C06\u81EA\u52A8\u5237\u65B0\uFF0C\u65B0\u5BF9\u8BDD\u4F7F\u7528\u65B0\u5F15\u64CE\u3002\u786E\u8BA4\u5207\u6362\u5417\uFF1F",
|
|
740
|
-
confirmAction: "\u5207\u6362",
|
|
741
|
-
cancelAction: "\u53D6\u6D88",
|
|
742
|
-
closeLabel: "\u5173\u95ED",
|
|
743
|
-
claudeModelNotice: "\u5F53\u524D\u4F7F\u7528 Claude Code \u5F15\u64CE\uFF1A\u5B9E\u9645\u6A21\u578B\u7531 Claude Code \u539F\u751F\u51B3\u5B9A\uFF0C\u9875\u9762\u4E0A\u7684\u6A21\u578B\u9009\u62E9\u4E0D\u751F\u6548\u3002"
|
|
744
|
-
};
|
|
745
|
-
var en = {
|
|
746
|
-
nav: "Loop engine",
|
|
747
|
-
description: "Choose the agent execution engine that drives sessions; the switch applies to new turns.",
|
|
748
|
-
engineInProcess: "In-process engine (default)",
|
|
749
|
-
engineClaudeCode: "Claude Code CLI",
|
|
750
|
-
engineCodex: "Codex CLI",
|
|
751
|
-
enginePi: "Pi CLI",
|
|
752
|
-
engineKimi: "Kimi Code CLI",
|
|
753
|
-
showInComposerLabel: "Show the engine selector in the chat page",
|
|
754
|
-
unavailable: "Loop engine settings are unavailable",
|
|
755
|
-
switchNotice: "Switching engines interrupts sessions currently running on the previous engine.",
|
|
756
|
-
saving: "Saving\u2026",
|
|
757
|
-
confirmTitle: "Switch loop engine?",
|
|
758
|
-
confirmBody: "Switching interrupts sessions currently running on the previous engine; the page reloads after the switch and new turns use the new engine. Switch now?",
|
|
759
|
-
confirmAction: "Switch",
|
|
760
|
-
cancelAction: "Cancel",
|
|
761
|
-
closeLabel: "Close",
|
|
762
|
-
claudeModelNotice: "Claude Code engine active: the actual model is decided natively by Claude Code; the model selector in this session has no effect."
|
|
763
|
-
};
|
|
764
|
-
|
|
765
1382
|
// src/namespace.ts
|
|
766
1383
|
var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
|
|
767
1384
|
|
|
@@ -781,7 +1398,9 @@ function apply(ctx) {
|
|
|
781
1398
|
controller.dispose();
|
|
782
1399
|
};
|
|
783
1400
|
}, "loop-engine: store lifecycle");
|
|
784
|
-
installTurnStatusStyles(ctx
|
|
1401
|
+
installTurnStatusStyles(ctx);
|
|
1402
|
+
const sessionEngines = createSessionEngineCache(ctx);
|
|
1403
|
+
installReloadReturn(ctx);
|
|
785
1404
|
const t = ctx.locale.bind(NS);
|
|
786
1405
|
const injected = () => ({
|
|
787
1406
|
controller,
|
|
@@ -796,10 +1415,7 @@ function apply(ctx) {
|
|
|
796
1415
|
inject: injected
|
|
797
1416
|
}, LoopEngineSection));
|
|
798
1417
|
ctx.inject(["slots", "conversation"], (scope2) => {
|
|
799
|
-
const badgeInjected = () => ({
|
|
800
|
-
hooks: { snapshot: controller.store },
|
|
801
|
-
t
|
|
802
|
-
});
|
|
1418
|
+
const badgeInjected = () => ({ sessionEngines, t });
|
|
803
1419
|
scope2.effect(() => {
|
|
804
1420
|
return scope2.slots.register({
|
|
805
1421
|
name: "conversation.session.header.actions",
|
|
@@ -812,10 +1428,15 @@ function apply(ctx) {
|
|
|
812
1428
|
}, LoopEngineBadge);
|
|
813
1429
|
}, "loop-engine: session header engine badge");
|
|
814
1430
|
});
|
|
1431
|
+
const switchEngine = sessionEngineSwitcher(ctx, (sessionId) => {
|
|
1432
|
+
sessionEngines.invalidate(sessionId);
|
|
1433
|
+
});
|
|
815
1434
|
ctx.inject(["slots", "conversation"], (scope2) => {
|
|
816
1435
|
const composerInjected = () => ({
|
|
817
1436
|
controller,
|
|
818
1437
|
hooks: { snapshot: controller.store },
|
|
1438
|
+
sessionEngines,
|
|
1439
|
+
switchEngine,
|
|
819
1440
|
t
|
|
820
1441
|
});
|
|
821
1442
|
scope2.effect(() => {
|