dsh-ntr 0.3.1 → 0.3.3
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.en.md +3 -1
- package/README.md +3 -1
- package/cordis.patch.yml +17 -1
- package/lib/bridge.js +2436 -82
- package/lib/client/acp-ui.js +103 -3
- package/lib/client/acp-view.js +24 -4
- package/lib/client/activity-pill.js +156 -0
- package/lib/client/branch-action.js +149 -0
- package/lib/client/composer-mode.js +48 -13
- package/lib/client/context-meter.js +86 -0
- package/lib/client/index.js +279 -16
- package/lib/client/model-seat.js +8 -2
- package/lib/client/panel.js +96 -29
- package/lib/client/released-banner.js +66 -0
- package/lib/client/session-controls.js +38 -13
- package/lib/client/slash-isolation.js +2 -2
- package/lib/client/store.js +380 -13
- package/lib/client.js +1364 -70
- package/lib/client.js.map +1 -1
- package/lib/impl.js +1229 -8
- package/lib/index.js +34 -0
- package/lib/protocol.js +19 -0
- package/lib/tps.js +32 -0
- package/lib/types/bridge.d.ts +850 -18
- package/lib/types/client/acp-ui.d.ts +34 -2
- package/lib/types/client/acp-view.d.ts +13 -2
- package/lib/types/client/activity-pill.d.ts +81 -0
- package/lib/types/client/branch-action.d.ts +47 -0
- package/lib/types/client/composer-mode.d.ts +28 -0
- package/lib/types/client/context-meter.d.ts +51 -0
- package/lib/types/client/model-seat.d.ts +21 -0
- package/lib/types/client/released-banner.d.ts +34 -0
- package/lib/types/client/slash-isolation.d.ts +1 -1
- package/lib/types/client/store.d.ts +108 -1
- package/lib/types/impl.d.ts +94 -7
- package/lib/types/protocol.d.ts +145 -0
- package/lib/types/tps.d.ts +17 -0
- package/package.json +2 -1
- package/src/bridge.ts +2754 -86
- package/src/client/acp-ui.tsx +123 -2
- package/src/client/acp-view.tsx +30 -5
- package/src/client/activity-pill.tsx +201 -0
- package/src/client/branch-action.tsx +178 -0
- package/src/client/composer-mode.tsx +59 -13
- package/src/client/context-meter.tsx +128 -0
- package/src/client/index.tsx +298 -15
- package/src/client/model-seat.tsx +28 -3
- package/src/client/panel.tsx +157 -40
- package/src/client/primitives.d.ts +27 -0
- package/src/client/released-banner.tsx +97 -0
- package/src/client/session-controls.tsx +34 -11
- package/src/client/slash-isolation.ts +2 -1
- package/src/client/store.ts +412 -14
- package/src/impl.ts +1251 -13
- package/src/index.ts +33 -0
- package/src/protocol.ts +145 -0
- package/src/tps.ts +40 -0
package/lib/client.js
CHANGED
|
@@ -4,14 +4,61 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
16
|
+
key = keys[i];
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
18
|
+
get: ((k) => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
//#endregion
|
|
7
29
|
let react = require("react");
|
|
8
30
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
31
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
32
|
+
_deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
|
|
9
33
|
//#region src/protocol.ts
|
|
10
34
|
/** Default when nothing was ever saved: the breathing halo at normal speed. */
|
|
11
35
|
const DEFAULT_TAG_HALO = {
|
|
12
36
|
mode: "breath",
|
|
13
37
|
speed: "normal"
|
|
14
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* The idle-release windows the settings page offers, in MILLISECONDS.
|
|
41
|
+
*
|
|
42
|
+
* ONE ladder, shared by both halves: the browser snaps its slider to these
|
|
43
|
+
* values and the host accepts only these values, so a window the slider can
|
|
44
|
+
* produce is always a window the host runs. The first rung is 1 minute (an
|
|
45
|
+
* aggressive release), the rest are 5-minute steps up to an hour.
|
|
46
|
+
*/
|
|
47
|
+
const IDLE_RELEASE_MS = [
|
|
48
|
+
6e4,
|
|
49
|
+
3e5,
|
|
50
|
+
6e5,
|
|
51
|
+
9e5,
|
|
52
|
+
12e5,
|
|
53
|
+
15e5,
|
|
54
|
+
18e5,
|
|
55
|
+
21e5,
|
|
56
|
+
24e5,
|
|
57
|
+
27e5,
|
|
58
|
+
3e6,
|
|
59
|
+
33e5,
|
|
60
|
+
36e5
|
|
61
|
+
];
|
|
15
62
|
//#endregion
|
|
16
63
|
//#region src/client/store.ts
|
|
17
64
|
/** Returned by {@link deadlineRemainingLabel} once the deadline has passed. */
|
|
@@ -26,6 +73,29 @@ window.__ModuleLoader__.load({
|
|
|
26
73
|
return permissions.filter((permission) => permission.dshSessionId === dshSessionId);
|
|
27
74
|
}
|
|
28
75
|
/**
|
|
76
|
+
* The branch button's state for one conversation, in the precedence it renders:
|
|
77
|
+
* the host service missing beats everything (nothing can work), then the
|
|
78
|
+
* agent's fork capability, then the session's own state (a parked session
|
|
79
|
+
* before a running one), then the DOM's tail verdict, and only then ready.
|
|
80
|
+
*
|
|
81
|
+
* Every input is a fact someone else owns — the row's `fork` verdict is the
|
|
82
|
+
* host's, `hostFork` is the structural probe of the runtime's `sessions.fork`,
|
|
83
|
+
* `isTail` is read from the host's own `[data-turn-tail]` marker, and
|
|
84
|
+
* `liveRunning` is the SSE-driven running flag (the row's own `running` only
|
|
85
|
+
* refreshes on a chat-list read, so it would answer last turn). `undefined`
|
|
86
|
+
* facts degrade CLOSED: no row and no `fork` verdict both read as not-ready,
|
|
87
|
+
* never as a click-through.
|
|
88
|
+
*/
|
|
89
|
+
function forkStateOf(row, hostFork, isTail, liveRunning) {
|
|
90
|
+
if (!hostFork) return "hostUnavailable";
|
|
91
|
+
if (row === void 0) return "released";
|
|
92
|
+
if (row.fork === void 0) return "unsupported";
|
|
93
|
+
if (row.fork.supported !== true) return row.fork.reason ?? "unsupported";
|
|
94
|
+
if (liveRunning === true) return "busy";
|
|
95
|
+
if (!isTail) return "notTail";
|
|
96
|
+
return "ready";
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
29
99
|
* Compact countdown for one ask, or `undefined` when there is nothing to count
|
|
30
100
|
* down (`wait` mode, or a frame without a deadline). Pure so the formatting is
|
|
31
101
|
* testable without a clock: the dock passes `Date.now()` on its tick.
|
|
@@ -331,8 +401,10 @@ window.__ModuleLoader__.load({
|
|
|
331
401
|
probingModels: {},
|
|
332
402
|
probeErrors: {},
|
|
333
403
|
chatSessions: [],
|
|
404
|
+
burstStats: {},
|
|
334
405
|
tagHalo: { ...DEFAULT_TAG_HALO },
|
|
335
406
|
approvalDeadlines: void 0,
|
|
407
|
+
idleRelease: void 0,
|
|
336
408
|
chatTranscripts: {},
|
|
337
409
|
acpMode: {},
|
|
338
410
|
chatBoundAgent: {},
|
|
@@ -353,6 +425,13 @@ window.__ModuleLoader__.load({
|
|
|
353
425
|
/** In-flight `session/chat-list` reads, keyed by chat session id. */
|
|
354
426
|
chatSessionReads = /* @__PURE__ */ new Map();
|
|
355
427
|
/**
|
|
428
|
+
* ACP session ids whose chat-list row was requested mid-turn (one read each).
|
|
429
|
+
* A conversation's first ACP session does not exist when the composer's
|
|
430
|
+
* mount/running read fires, so its first `usage_update` would find no row to
|
|
431
|
+
* patch; `turn/start` asks again once the host has materialized the session.
|
|
432
|
+
*/
|
|
433
|
+
chatSessionRowReads = /* @__PURE__ */ new Set();
|
|
434
|
+
/**
|
|
356
435
|
* Chat sessions whose list has answered at least once in this page. The
|
|
357
436
|
* FIRST answer only seeds the lock; a lock appearing on a later answer is a
|
|
358
437
|
* transition this page actually witnessed, and only that one is announced via
|
|
@@ -471,17 +550,25 @@ window.__ModuleLoader__.load({
|
|
|
471
550
|
else list.push(item);
|
|
472
551
|
}
|
|
473
552
|
let chatTranscripts = this.state.chatTranscripts;
|
|
553
|
+
let chatSessions = this.state.chatSessions;
|
|
474
554
|
let blocksChanged = false;
|
|
475
555
|
for (const [sessionId, items] of bySession) {
|
|
476
556
|
const last = items[items.length - 1];
|
|
557
|
+
const usage = usageOfBatch(items);
|
|
558
|
+
if (usage !== void 0) chatSessions = withSessionUsage(chatSessions, sessionId, usage);
|
|
477
559
|
const record = chatTranscripts[sessionId];
|
|
478
560
|
if (record !== void 0) {
|
|
479
561
|
let changed = false;
|
|
480
562
|
for (const item of items) if (foldUpdate(record.blocks, item.update)) changed = true;
|
|
563
|
+
const session = usage !== void 0 && !sameUsage(record.session.usage, usage) ? {
|
|
564
|
+
...record.session,
|
|
565
|
+
usage
|
|
566
|
+
} : record.session;
|
|
481
567
|
chatTranscripts = {
|
|
482
568
|
...chatTranscripts,
|
|
483
569
|
[sessionId]: {
|
|
484
570
|
...record,
|
|
571
|
+
session,
|
|
485
572
|
seq: last.seq,
|
|
486
573
|
...changed ? { blocks: [...record.blocks] } : {}
|
|
487
574
|
}
|
|
@@ -502,6 +589,7 @@ window.__ModuleLoader__.load({
|
|
|
502
589
|
}
|
|
503
590
|
const patch = {};
|
|
504
591
|
if (chatTranscripts !== this.state.chatTranscripts) patch.chatTranscripts = chatTranscripts;
|
|
592
|
+
if (chatSessions !== this.state.chatSessions) patch.chatSessions = chatSessions;
|
|
505
593
|
if (blocksChanged) patch.blocks = [...this.state.blocks];
|
|
506
594
|
if (Object.keys(patch).length > 0) this.mutate(patch);
|
|
507
595
|
}
|
|
@@ -550,7 +638,7 @@ window.__ModuleLoader__.load({
|
|
|
550
638
|
/** Snapshot refresh used on panel open and after a stream outage. */
|
|
551
639
|
async resync() {
|
|
552
640
|
try {
|
|
553
|
-
const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult, versionResult] = await Promise.all([
|
|
641
|
+
const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult, idleReleaseResult, versionResult] = await Promise.all([
|
|
554
642
|
this.call("agents/list"),
|
|
555
643
|
this.call("sessions/list"),
|
|
556
644
|
this.call("default/agent").catch(() => ({})),
|
|
@@ -558,6 +646,7 @@ window.__ModuleLoader__.load({
|
|
|
558
646
|
this.call("preset/status").catch(() => null),
|
|
559
647
|
this.call("ui/tag-halo").catch(() => ({ halo: { ...DEFAULT_TAG_HALO } })),
|
|
560
648
|
this.call("ui/approval-deadlines").catch(() => ({ deadlines: void 0 })),
|
|
649
|
+
this.call("ui/idle-release").catch(() => ({ idleRelease: void 0 })),
|
|
561
650
|
this.call("plugin/version").catch(() => null)
|
|
562
651
|
]);
|
|
563
652
|
const detectedAgents = detectResult && detectResult.agents.length > 0 ? detectResult.agents : this.state.detectedAgents.length > 0 ? this.state.detectedAgents : [...DEFAULT_KNOWN_PRESETS];
|
|
@@ -570,8 +659,10 @@ window.__ModuleLoader__.load({
|
|
|
570
659
|
preset: presetResult?.preset ?? void 0,
|
|
571
660
|
tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
|
|
572
661
|
approvalDeadlines: deadlinesResult.deadlines,
|
|
662
|
+
idleRelease: idleReleaseResult.idleRelease,
|
|
573
663
|
pluginVersion: versionResult?.version !== void 0 && versionResult.version !== "" ? versionResult.version : void 0
|
|
574
664
|
});
|
|
665
|
+
if (this.state.chatSessionsFor !== void 0) this.loadChatSessions(this.state.chatSessionsFor);
|
|
575
666
|
const active = this.state.activeSessionId ?? sessions[0]?.sessionId;
|
|
576
667
|
if (active !== void 0) await this.loadTranscript(active);
|
|
577
668
|
} catch (error) {
|
|
@@ -995,7 +1086,7 @@ window.__ModuleLoader__.load({
|
|
|
995
1086
|
async readChatSessionList(dshSessionId, options, read) {
|
|
996
1087
|
const settle = options?.settleVerdict === true || !this.acpModeLive.has(dshSessionId);
|
|
997
1088
|
try {
|
|
998
|
-
const { sessions, acpMode, boundAgentId, lockedAgentId, choice } = await this.call("session/chat-list", {
|
|
1089
|
+
const { sessions = [], acpMode, boundAgentId, lockedAgentId, choice } = await this.call("session/chat-list", {
|
|
999
1090
|
dshSessionId,
|
|
1000
1091
|
refresh: true
|
|
1001
1092
|
});
|
|
@@ -1103,6 +1194,13 @@ window.__ModuleLoader__.load({
|
|
|
1103
1194
|
* being the one authorized exception. Same no-op-when-unchanged discipline as
|
|
1104
1195
|
* the other accessors, so a frame restating the held verdict costs no
|
|
1105
1196
|
* listener churn.
|
|
1197
|
+
*
|
|
1198
|
+
* The seat gate (`syncAcpMode`) is the SECOND caller: it folds the runtime
|
|
1199
|
+
* row's own `agentPreset` — the same session-log source the frames announce —
|
|
1200
|
+
* through here, in both directions. That hands the row the frame's authority
|
|
1201
|
+
* on purpose (one provenance, one rank): a stale projection is corrected by
|
|
1202
|
+
* the gate's next evaluation with fresher row data, and only HOST READS are
|
|
1203
|
+
* barred from regressing what the runtime itself already said.
|
|
1106
1204
|
*/
|
|
1107
1205
|
noteAcpMode(dshSessionId, acpMode) {
|
|
1108
1206
|
this.acpModeLive.add(dshSessionId);
|
|
@@ -1186,12 +1284,99 @@ window.__ModuleLoader__.load({
|
|
|
1186
1284
|
modeId,
|
|
1187
1285
|
...agentId === void 0 ? {} : { agentId }
|
|
1188
1286
|
});
|
|
1287
|
+
const safeAgent = agentId && agentId.trim() || void 0;
|
|
1288
|
+
this.writeChoice(dshSessionId, (choice) => ({
|
|
1289
|
+
...choice,
|
|
1290
|
+
mode: modeId
|
|
1291
|
+
}), safeAgent);
|
|
1292
|
+
if (this.state.chatSessionsFor === dshSessionId) {
|
|
1293
|
+
const owner = safeAgent ?? this.state.chatChoice[dshSessionId]?.agentId ?? this.state.chatBoundAgent[dshSessionId] ?? this.state.defaultAgent;
|
|
1294
|
+
let changed = false;
|
|
1295
|
+
const chatSessions = this.state.chatSessions.map((s) => {
|
|
1296
|
+
if (s.agentId !== owner || !s.modes || s.modes.currentModeId === modeId) return s;
|
|
1297
|
+
changed = true;
|
|
1298
|
+
return {
|
|
1299
|
+
...s,
|
|
1300
|
+
modes: {
|
|
1301
|
+
...s.modes,
|
|
1302
|
+
currentModeId: modeId
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
});
|
|
1306
|
+
if (changed) this.mutate({ chatSessions });
|
|
1307
|
+
}
|
|
1189
1308
|
} catch (error) {
|
|
1190
1309
|
this.flashError(error);
|
|
1191
1310
|
}
|
|
1192
1311
|
await this.loadChatSessions(dshSessionId);
|
|
1193
1312
|
}
|
|
1194
1313
|
/**
|
|
1314
|
+
* Bring the conversation's released ACP session back on a fresh process —
|
|
1315
|
+
* the released banner's explicit form of the "next message resumes it"
|
|
1316
|
+
* promise (`session/chat-resume`). A failure is reported through the notice
|
|
1317
|
+
* AND rethrown (the `respondPermission` contract), so the banner can pin
|
|
1318
|
+
* the message beside the button the user just pressed; the binding is
|
|
1319
|
+
* re-read either way so the row follows the host's real answer.
|
|
1320
|
+
*/
|
|
1321
|
+
async resumeChatSession(dshSessionId) {
|
|
1322
|
+
let failure;
|
|
1323
|
+
try {
|
|
1324
|
+
await this.call("session/chat-resume", { dshSessionId });
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
this.flashError(error);
|
|
1327
|
+
failure = error;
|
|
1328
|
+
}
|
|
1329
|
+
await this.loadChatSessions(dshSessionId);
|
|
1330
|
+
if (failure !== void 0) throw failure;
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Branch one conversation (the NTR branch button's click): the fixed
|
|
1334
|
+
* two-stage order — the HOST forks the DSH session tree first (copied
|
|
1335
|
+
* history, `(1)` title, workspace), then the plugin adopts the ACP side via
|
|
1336
|
+
* `session/chat-fork-adopt`, which runs the agent's own `session/fork` so
|
|
1337
|
+
* the child actually remembers the parent context. Host first is structural:
|
|
1338
|
+
* ACP fork has no cut-point parameter and nothing to return to if the host
|
|
1339
|
+
* half failed second (see the plan's §D7).
|
|
1340
|
+
*
|
|
1341
|
+
* Resolves `{ childId }` on success, or `{ failure }` with one actionable
|
|
1342
|
+
* line — the reason is ALSO flashed into the notice banner (a user-action
|
|
1343
|
+
* failure, so the banner is its surface), and the local line is what the
|
|
1344
|
+
* conversation renders beside the button (the user is not on the settings
|
|
1345
|
+
* page). A failed adopt leaves the child an ordinary copied-history
|
|
1346
|
+
* conversation (its first message starts a fresh `session/new` — today's
|
|
1347
|
+
* behavior), never a half-bound one, and NOTHING is opened.
|
|
1348
|
+
*
|
|
1349
|
+
* `forkHost` is the runtime's `sessions.fork` face, resolved structurally by
|
|
1350
|
+
* the entry and injected here: the store stays transport-free.
|
|
1351
|
+
*/
|
|
1352
|
+
async forkChatSession(dshSessionId, forkHost) {
|
|
1353
|
+
let childId;
|
|
1354
|
+
try {
|
|
1355
|
+
childId = await forkHost(dshSessionId);
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
const failure = error instanceof Error ? error.message : String(error);
|
|
1358
|
+
this.flashError(failure);
|
|
1359
|
+
return { failure };
|
|
1360
|
+
}
|
|
1361
|
+
if (typeof childId !== "string" || childId === "") {
|
|
1362
|
+
const failure = this.locale("宿主没有返回分支会话,分支未完成。", "The host returned no branched session; the branch did not complete.");
|
|
1363
|
+
this.flashError(new Error(failure));
|
|
1364
|
+
return { failure };
|
|
1365
|
+
}
|
|
1366
|
+
try {
|
|
1367
|
+
await this.call("session/chat-fork-adopt", {
|
|
1368
|
+
dshSessionId: childId,
|
|
1369
|
+
parentDshSessionId: dshSessionId
|
|
1370
|
+
});
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1373
|
+
const failure = this.locale(`分支会话已创建,但没有接上 agent 的上下文(${reason});在子会话里发送的第一条消息会开启全新会话。`, `The branched session was created but the agent context was not attached (${reason}); its first message will start a fresh session.`);
|
|
1374
|
+
this.flashError(new Error(failure));
|
|
1375
|
+
return { failure };
|
|
1376
|
+
}
|
|
1377
|
+
return { childId };
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1195
1380
|
* Start (or refresh) observation of every ACP transcript behind one DSH chat
|
|
1196
1381
|
* session. Blocks are rebuilt from scratch — the host buffer is the record,
|
|
1197
1382
|
* and this view is the only consumer that needs the raw agent stream.
|
|
@@ -1358,6 +1543,33 @@ window.__ModuleLoader__.load({
|
|
|
1358
1543
|
async resetApprovalDeadlines() {
|
|
1359
1544
|
await this.writeApprovalDeadlines({ reset: true });
|
|
1360
1545
|
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Write the idle-release window (in ms).
|
|
1548
|
+
*
|
|
1549
|
+
* The host answer — the same route returns the window now in force — is
|
|
1550
|
+
* the authority, so an optimistic write that the host trims self-corrects
|
|
1551
|
+
* instead of drifting from what runs.
|
|
1552
|
+
*/
|
|
1553
|
+
async setIdleRelease(idleTimeoutMs) {
|
|
1554
|
+
await this.writeIdleRelease({ idleTimeoutMs });
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Drop the persisted override: the config file (or the built-in default)
|
|
1558
|
+
* decides again.
|
|
1559
|
+
*/
|
|
1560
|
+
async resetIdleRelease() {
|
|
1561
|
+
await this.writeIdleRelease({ reset: true });
|
|
1562
|
+
}
|
|
1563
|
+
async writeIdleRelease(payload) {
|
|
1564
|
+
const previous = this.state.idleRelease;
|
|
1565
|
+
try {
|
|
1566
|
+
const result = await this.call("ui/idle-release-set", payload);
|
|
1567
|
+
this.mutate({ idleRelease: result.idleRelease });
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
this.mutate({ idleRelease: previous });
|
|
1570
|
+
this.flashError(error);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1361
1573
|
async writeApprovalDeadlines(payload) {
|
|
1362
1574
|
const previous = this.state.approvalDeadlines;
|
|
1363
1575
|
if (previous !== void 0) this.mutate({ approvalDeadlines: {
|
|
@@ -1404,10 +1616,24 @@ window.__ModuleLoader__.load({
|
|
|
1404
1616
|
dshSessionId,
|
|
1405
1617
|
modelId
|
|
1406
1618
|
});
|
|
1619
|
+
const targetAgent = (agentId && agentId.trim() || void 0) ?? (modelId ? splitModelAgentId(modelId) : void 0);
|
|
1407
1620
|
this.writeChoice(dshSessionId, (choice) => ({
|
|
1408
1621
|
...choice,
|
|
1409
1622
|
modelId
|
|
1410
|
-
}),
|
|
1623
|
+
}), targetAgent);
|
|
1624
|
+
if (this.state.chatSessionsFor === dshSessionId) {
|
|
1625
|
+
const owner = targetAgent ?? this.state.chatChoice[dshSessionId]?.agentId ?? this.state.chatBoundAgent[dshSessionId] ?? this.state.defaultAgent;
|
|
1626
|
+
let changed = false;
|
|
1627
|
+
const chatSessions = this.state.chatSessions.map((s) => {
|
|
1628
|
+
if (s.agentId !== owner || s.modelId === modelId) return s;
|
|
1629
|
+
changed = true;
|
|
1630
|
+
return {
|
|
1631
|
+
...s,
|
|
1632
|
+
modelId
|
|
1633
|
+
};
|
|
1634
|
+
});
|
|
1635
|
+
if (changed) this.mutate({ chatSessions });
|
|
1636
|
+
}
|
|
1411
1637
|
} catch (error) {
|
|
1412
1638
|
this.flashError(error);
|
|
1413
1639
|
throw error;
|
|
@@ -1421,13 +1647,27 @@ window.__ModuleLoader__.load({
|
|
|
1421
1647
|
...effort === void 0 ? {} : { effort },
|
|
1422
1648
|
...agentId === void 0 ? {} : { agentId }
|
|
1423
1649
|
});
|
|
1650
|
+
const targetAgent = agentId && agentId.trim() || void 0;
|
|
1424
1651
|
this.writeChoice(dshSessionId, (choice) => {
|
|
1425
1652
|
const { effort: _dropped, ...rest } = choice;
|
|
1426
1653
|
return effort === void 0 ? rest : {
|
|
1427
1654
|
...rest,
|
|
1428
1655
|
effort
|
|
1429
1656
|
};
|
|
1430
|
-
},
|
|
1657
|
+
}, targetAgent);
|
|
1658
|
+
if (this.state.chatSessionsFor === dshSessionId) {
|
|
1659
|
+
const owner = targetAgent ?? this.state.chatChoice[dshSessionId]?.agentId ?? this.state.chatBoundAgent[dshSessionId] ?? this.state.defaultAgent;
|
|
1660
|
+
let changed = false;
|
|
1661
|
+
const chatSessions = this.state.chatSessions.map((s) => {
|
|
1662
|
+
if (s.agentId !== owner || s.reasoningEffort === effort) return s;
|
|
1663
|
+
changed = true;
|
|
1664
|
+
return {
|
|
1665
|
+
...s,
|
|
1666
|
+
reasoningEffort: effort
|
|
1667
|
+
};
|
|
1668
|
+
});
|
|
1669
|
+
if (changed) this.mutate({ chatSessions });
|
|
1670
|
+
}
|
|
1431
1671
|
} catch (error) {
|
|
1432
1672
|
this.flashError(error);
|
|
1433
1673
|
throw error;
|
|
@@ -1536,8 +1776,11 @@ window.__ModuleLoader__.load({
|
|
|
1536
1776
|
}
|
|
1537
1777
|
case "session/meta": {
|
|
1538
1778
|
const sessions = this.state.sessions.some((s) => s.sessionId === event.sessionId) ? this.state.sessions.map((s) => s.sessionId === event.sessionId ? event.session : s) : this.state.sessions;
|
|
1779
|
+
let chatSessions = withSessionUsage(this.state.chatSessions, event.sessionId, event.session.usage);
|
|
1780
|
+
chatSessions = withSessionModes(chatSessions, event.sessionId, event.session.modes);
|
|
1539
1781
|
this.mutate({
|
|
1540
1782
|
sessions,
|
|
1783
|
+
...chatSessions === this.state.chatSessions ? {} : { chatSessions },
|
|
1541
1784
|
...this.watchedSessionPatch(event.sessionId, event.session)
|
|
1542
1785
|
});
|
|
1543
1786
|
return Promise.resolve();
|
|
@@ -1550,11 +1793,18 @@ window.__ModuleLoader__.load({
|
|
|
1550
1793
|
const closedTranscripts = { ...this.state.chatTranscripts };
|
|
1551
1794
|
delete closedTranscripts[event.sessionId];
|
|
1552
1795
|
const chatSessions = this.state.chatSessions.filter((s) => s.sessionId !== event.sessionId);
|
|
1796
|
+
const hadBurst = this.state.burstStats[event.sessionId] !== void 0;
|
|
1797
|
+
let burstStats = this.state.burstStats;
|
|
1798
|
+
if (hadBurst) {
|
|
1799
|
+
burstStats = { ...burstStats };
|
|
1800
|
+
delete burstStats[event.sessionId];
|
|
1801
|
+
}
|
|
1553
1802
|
this.mutate({
|
|
1554
1803
|
sessions,
|
|
1555
1804
|
activeSessionId: active,
|
|
1556
1805
|
chatTranscripts: closedTranscripts,
|
|
1557
1806
|
...chatSessions.length !== this.state.chatSessions.length ? { chatSessions } : {},
|
|
1807
|
+
...hadBurst ? { burstStats } : {},
|
|
1558
1808
|
permissions: this.state.permissions.filter((p) => p.sessionId !== event.sessionId),
|
|
1559
1809
|
...event.reason === "agent-died" ? this.logPatch("error", this.locale("会话因 agent 进程退出而终止", "session ended: agent process died"), closingSession?.agentId) : {}
|
|
1560
1810
|
});
|
|
@@ -1563,10 +1813,7 @@ window.__ModuleLoader__.load({
|
|
|
1563
1813
|
case "session/released": {
|
|
1564
1814
|
const sessions = this.state.sessions.some((s) => s.sessionId === event.sessionId) ? this.state.sessions.map((s) => s.sessionId === event.sessionId ? event.session : s) : [...this.state.sessions, event.session];
|
|
1565
1815
|
this.openTurns.delete(event.sessionId);
|
|
1566
|
-
const chatSessions = this.state.chatSessions
|
|
1567
|
-
...s,
|
|
1568
|
-
released: true
|
|
1569
|
-
} : s) : this.state.chatSessions;
|
|
1816
|
+
const chatSessions = withSessionReleased(this.state.chatSessions, event.sessionId, true);
|
|
1570
1817
|
const cause = event.cause ?? "died";
|
|
1571
1818
|
let log = {};
|
|
1572
1819
|
if (cause === "workspace") log = this.logPatch("info", this.locale("agent 为切换工作区重启,会话已挂起;再次发送消息将自动恢复。", "agent restarted for its workspace; the session is paused and will resume on your next message."), event.session.agentId);
|
|
@@ -1588,8 +1835,10 @@ window.__ModuleLoader__.load({
|
|
|
1588
1835
|
const isActive = event.sessionId === this.state.activeSessionId;
|
|
1589
1836
|
const sessions = this.state.sessions.some((s) => s.sessionId === event.sessionId) ? this.state.sessions.map((s) => s.sessionId === event.sessionId ? event.session : s) : [...this.state.sessions, event.session];
|
|
1590
1837
|
const watched = this.state.chatTranscripts[event.sessionId];
|
|
1838
|
+
const chatSessions = withSessionReleased(this.state.chatSessions, event.sessionId, false);
|
|
1591
1839
|
this.mutate({
|
|
1592
1840
|
sessions,
|
|
1841
|
+
...chatSessions === this.state.chatSessions ? {} : { chatSessions },
|
|
1593
1842
|
...isActive ? { blocks: [] } : {},
|
|
1594
1843
|
...watched === void 0 ? {} : { chatTranscripts: {
|
|
1595
1844
|
...this.state.chatTranscripts,
|
|
@@ -1677,9 +1926,28 @@ window.__ModuleLoader__.load({
|
|
|
1677
1926
|
});
|
|
1678
1927
|
return Promise.resolve();
|
|
1679
1928
|
}
|
|
1680
|
-
case "turn/
|
|
1929
|
+
case "turn/metrics": {
|
|
1930
|
+
const prior = this.state.burstStats[event.sessionId];
|
|
1931
|
+
if (event.final === false && prior !== void 0 && prior.final === true && prior.turnId === event.turnId) return Promise.resolve();
|
|
1932
|
+
this.mutate({ burstStats: {
|
|
1933
|
+
...this.state.burstStats,
|
|
1934
|
+
[event.sessionId]: {
|
|
1935
|
+
outputTokens: event.outputTokens,
|
|
1936
|
+
burstMs: event.burstMs,
|
|
1937
|
+
turnId: event.turnId,
|
|
1938
|
+
final: event.final
|
|
1939
|
+
}
|
|
1940
|
+
} });
|
|
1941
|
+
return Promise.resolve();
|
|
1942
|
+
}
|
|
1943
|
+
case "turn/start": {
|
|
1681
1944
|
this.openTurns.add(event.sessionId);
|
|
1682
1945
|
this.markWatchedRunning(event.sessionId, true);
|
|
1946
|
+
const chatSessionId = this.state.chatSessionsFor;
|
|
1947
|
+
if (chatSessionId !== void 0 && !this.state.chatSessions.some((row) => row.sessionId === event.sessionId) && !this.chatSessionRowReads.has(event.sessionId)) {
|
|
1948
|
+
this.chatSessionRowReads.add(event.sessionId);
|
|
1949
|
+
this.loadChatSessions(chatSessionId);
|
|
1950
|
+
}
|
|
1683
1951
|
if (event.sessionId === this.state.activeSessionId) {
|
|
1684
1952
|
this.state.blocks.push({
|
|
1685
1953
|
kind: "user",
|
|
@@ -1692,6 +1960,7 @@ window.__ModuleLoader__.load({
|
|
|
1692
1960
|
} : s) });
|
|
1693
1961
|
}
|
|
1694
1962
|
return Promise.resolve();
|
|
1963
|
+
}
|
|
1695
1964
|
case "turn/end":
|
|
1696
1965
|
if (!this.openTurns.has(event.sessionId)) return Promise.resolve();
|
|
1697
1966
|
this.openTurns.delete(event.sessionId);
|
|
@@ -1873,6 +2142,113 @@ window.__ModuleLoader__.load({
|
|
|
1873
2142
|
"usage_update",
|
|
1874
2143
|
"session_info_update"
|
|
1875
2144
|
]);
|
|
2145
|
+
/**
|
|
2146
|
+
* Context occupancy carried by one raw `usage_update` frame, in the shape the
|
|
2147
|
+
* session snapshots already use. A malformed frame is ignored (the last good
|
|
2148
|
+
* sample stays on screen) instead of zeroing the meter.
|
|
2149
|
+
*/
|
|
2150
|
+
function usageFromUpdate(update) {
|
|
2151
|
+
const used = update.used;
|
|
2152
|
+
const size = update.size;
|
|
2153
|
+
if (typeof used !== "number" || !Number.isFinite(used)) return void 0;
|
|
2154
|
+
if (typeof size !== "number" || !Number.isFinite(size)) return void 0;
|
|
2155
|
+
const cost = update.cost;
|
|
2156
|
+
if (cost !== null && typeof cost === "object" && !Array.isArray(cost)) {
|
|
2157
|
+
const row = cost;
|
|
2158
|
+
if (typeof row.amount === "number" && Number.isFinite(row.amount) && typeof row.currency === "string" && row.currency !== "") return {
|
|
2159
|
+
used,
|
|
2160
|
+
size,
|
|
2161
|
+
cost: {
|
|
2162
|
+
amount: row.amount,
|
|
2163
|
+
currency: row.currency
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
return {
|
|
2168
|
+
used,
|
|
2169
|
+
size
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function sameUsage(left, right) {
|
|
2173
|
+
if (left === right) return true;
|
|
2174
|
+
if (left === void 0 || right === void 0) return false;
|
|
2175
|
+
if (left.used !== right.used || left.size !== right.size) return false;
|
|
2176
|
+
const leftCost = left.cost;
|
|
2177
|
+
const rightCost = right.cost;
|
|
2178
|
+
if (leftCost === void 0 || rightCost === void 0) return leftCost === rightCost;
|
|
2179
|
+
return leftCost.amount === rightCost.amount && leftCost.currency === rightCost.currency;
|
|
2180
|
+
}
|
|
2181
|
+
/** Patch one session row's usage; other rows and an unchanged value keep their identities. */
|
|
2182
|
+
function withSessionUsage(rows, sessionId, usage) {
|
|
2183
|
+
let changed = false;
|
|
2184
|
+
const next = rows.map((row) => {
|
|
2185
|
+
if (row.sessionId !== sessionId || sameUsage(row.usage, usage)) return row;
|
|
2186
|
+
changed = true;
|
|
2187
|
+
return {
|
|
2188
|
+
...row,
|
|
2189
|
+
usage
|
|
2190
|
+
};
|
|
2191
|
+
});
|
|
2192
|
+
return changed ? next : rows;
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Flip one chat row's `released` flag (release/resume bookkeeping). The
|
|
2196
|
+
* composer's own list (`session/chat-list` rows) has no other live writer —
|
|
2197
|
+
* `session/released` must mark it, `session/reset` must un-mark it, or the
|
|
2198
|
+
* agent tag's halo and the released banner read yesterday's process state.
|
|
2199
|
+
*/
|
|
2200
|
+
function withSessionReleased(rows, sessionId, released) {
|
|
2201
|
+
let changed = false;
|
|
2202
|
+
const next = rows.map((row) => {
|
|
2203
|
+
if (row.sessionId !== sessionId || row.released === true === released) return row;
|
|
2204
|
+
changed = true;
|
|
2205
|
+
return {
|
|
2206
|
+
...row,
|
|
2207
|
+
released
|
|
2208
|
+
};
|
|
2209
|
+
});
|
|
2210
|
+
return changed ? next : rows;
|
|
2211
|
+
}
|
|
2212
|
+
function sameModes(a, b) {
|
|
2213
|
+
if (a === b) return true;
|
|
2214
|
+
if (!a || !b) return false;
|
|
2215
|
+
if (a.currentModeId !== b.currentModeId) return false;
|
|
2216
|
+
const aModes = a.availableModes ?? [];
|
|
2217
|
+
const bModes = b.availableModes ?? [];
|
|
2218
|
+
if (aModes.length !== bModes.length) return false;
|
|
2219
|
+
return aModes.every((m, i) => {
|
|
2220
|
+
const o = bModes[i];
|
|
2221
|
+
return m != null && o != null && m.id === o.id && m.name === o.name && m.description === o.description;
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
function withSessionModes(rows, sessionId, modes) {
|
|
2225
|
+
if (!modes) return rows;
|
|
2226
|
+
let changed = false;
|
|
2227
|
+
const next = rows.map((row) => {
|
|
2228
|
+
if (row.sessionId !== sessionId || sameModes(row.modes, modes)) return row;
|
|
2229
|
+
changed = true;
|
|
2230
|
+
return {
|
|
2231
|
+
...row,
|
|
2232
|
+
modes
|
|
2233
|
+
};
|
|
2234
|
+
});
|
|
2235
|
+
return changed ? next : rows;
|
|
2236
|
+
}
|
|
2237
|
+
function splitModelAgentId(modelId) {
|
|
2238
|
+
const at = modelId.indexOf("@");
|
|
2239
|
+
return (at === -1 ? modelId.trim() : at === 0 ? "" : modelId.slice(0, at).trim()) || void 0;
|
|
2240
|
+
}
|
|
2241
|
+
/** Last well-formed `usage_update` in one coalesced batch (later ticks win). */
|
|
2242
|
+
function usageOfBatch(items) {
|
|
2243
|
+
let usage;
|
|
2244
|
+
for (const item of items) {
|
|
2245
|
+
const update = item.update;
|
|
2246
|
+
if (update.sessionUpdate !== "usage_update") continue;
|
|
2247
|
+
const parsed = usageFromUpdate(update);
|
|
2248
|
+
if (parsed !== void 0) usage = parsed;
|
|
2249
|
+
}
|
|
2250
|
+
return usage;
|
|
2251
|
+
}
|
|
1876
2252
|
function trimBlocks(blocks) {
|
|
1877
2253
|
if (blocks.length > MAX_BLOCKS) blocks.splice(0, blocks.length - MAX_BLOCKS);
|
|
1878
2254
|
}
|
|
@@ -1992,6 +2368,13 @@ window.__ModuleLoader__.load({
|
|
|
1992
2368
|
/**
|
|
1993
2369
|
* Which session the composer is currently staged on.
|
|
1994
2370
|
*
|
|
2371
|
+
* The renderer's own current scope binding is the first answer when the caller
|
|
2372
|
+
* passes it (`currentSessionId`, read from `uiSession.adapter.current` in the
|
|
2373
|
+
* browser entry): that is the exact session the host has selected, while the
|
|
2374
|
+
* list can retain more than one row in a multi-view layout. Falling back to
|
|
2375
|
+
* the list is still required — a host without that scope adapter, and the old
|
|
2376
|
+
* snapshot shape below, have no better source.
|
|
2377
|
+
*
|
|
1995
2378
|
* Old runtimes answer from `snapshot.current` (with `currentAddress` as the
|
|
1996
2379
|
* subagent marker). rc.2 has no `current`: the staged session is the row the
|
|
1997
2380
|
* main view retains (`retainedBy.mainView > 0`) — the exact rule
|
|
@@ -1999,8 +2382,13 @@ window.__ModuleLoader__.load({
|
|
|
1999
2382
|
* row with `origin === "subagent"` is an addressed subagent route. `undefined`
|
|
2000
2383
|
* when nothing is staged yet (the list has not loaded, or no row holds the main
|
|
2001
2384
|
* view): callers must stand down rather than guess.
|
|
2385
|
+
*
|
|
2386
|
+
* A current id the list has no row for is ALSO `undefined`: the selection may
|
|
2387
|
+
* be a subagent address, and guessing "not a subagent" there would let the
|
|
2388
|
+
* gate install ACP chrome on a route the plugin must not drive. The next list
|
|
2389
|
+
* notification retries.
|
|
2002
2390
|
*/
|
|
2003
|
-
function stagedSessionOf(snapshot) {
|
|
2391
|
+
function stagedSessionOf(snapshot, currentSessionId) {
|
|
2004
2392
|
if (snapshot === void 0) return void 0;
|
|
2005
2393
|
if (snapshot.current !== void 0) return {
|
|
2006
2394
|
sessionId: snapshot.current,
|
|
@@ -2008,6 +2396,15 @@ window.__ModuleLoader__.load({
|
|
|
2008
2396
|
subagent: snapshot.currentAddress !== void 0
|
|
2009
2397
|
};
|
|
2010
2398
|
const byId = snapshot.byId ?? {};
|
|
2399
|
+
if (currentSessionId !== void 0) {
|
|
2400
|
+
const row = byId[currentSessionId];
|
|
2401
|
+
if (row === void 0) return void 0;
|
|
2402
|
+
return {
|
|
2403
|
+
sessionId: currentSessionId,
|
|
2404
|
+
row,
|
|
2405
|
+
subagent: row.origin === "subagent"
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2011
2408
|
const order = /* @__PURE__ */ new Map();
|
|
2012
2409
|
for (const [index, id] of (snapshot.ids ?? []).entries()) order.set(id, index);
|
|
2013
2410
|
const rank = (id) => order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
@@ -2021,8 +2418,8 @@ window.__ModuleLoader__.load({
|
|
|
2021
2418
|
};
|
|
2022
2419
|
}
|
|
2023
2420
|
/** Whether the staged session runs the ACP agent preset. */
|
|
2024
|
-
function stagedIsAcpMode(snapshot, acpMode) {
|
|
2025
|
-
const staged = stagedSessionOf(snapshot);
|
|
2421
|
+
function stagedIsAcpMode(snapshot, acpMode, currentSessionId) {
|
|
2422
|
+
const staged = stagedSessionOf(snapshot, currentSessionId);
|
|
2026
2423
|
if (staged === void 0 || staged.subagent) return false;
|
|
2027
2424
|
const listed = rowAgentPreset(staged.row);
|
|
2028
2425
|
if (listed !== void 0) return listed === "acp";
|
|
@@ -2514,6 +2911,15 @@ window.__ModuleLoader__.load({
|
|
|
2514
2911
|
.ntr-acp-chip.is-static:hover { background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
|
|
2515
2912
|
.ntr-acp-chip.is-active { border-color: var(--dsw-alias-state-business-primary, #2563eb); color: var(--dsw-alias-state-business-primary, #2563eb); background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #2563eb) 10%, transparent); }
|
|
2516
2913
|
.ntr-acp-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2914
|
+
/* Composer context-occupancy ring (conversation.composer.dock,
|
|
2915
|
+
context-meter.tsx): the agent's own prompt-side used/size in the host
|
|
2916
|
+
ContextMeter's ring geometry, pill and tokens, so it reads as that meter's
|
|
2917
|
+
sibling in the bottom info row. Gated inside the component; absent until the
|
|
2918
|
+
agent reports a sample. */
|
|
2919
|
+
.ntr-acp-meter { display: inline-flex; align-items: center; gap: 6px; padding: 1px 8px; border-radius: 24px; color: var(--dsw-alias-label-tertiary, #9ca3af); font-family: inherit; font-size: var(--dsh-content-font-size-secondary, 13px); font-variant-numeric: tabular-nums; line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px)); white-space: nowrap; flex: none; }
|
|
2920
|
+
.ntr-acp-meter-track { fill: none; stroke: var(--dsw-alias-border-l3, #e5e7eb); stroke-width: 2px; }
|
|
2921
|
+
.ntr-acp-meter-fill { fill: none; stroke: var(--dsw-alias-label-tertiary, #9ca3af); stroke-width: 2px; stroke-linecap: round; }
|
|
2922
|
+
.ntr-acp-meter-percent { color: inherit; }
|
|
2517
2923
|
/* The lock hand-off (src/client/agent-lock-anim.ts): the outgoing agent base
|
|
2518
2924
|
breaks into debris for 460ms, then the edge tag condenses from the same
|
|
2519
2925
|
particles. Both halves read one shared clock in that module; these durations
|
|
@@ -2689,6 +3095,79 @@ window.__ModuleLoader__.load({
|
|
|
2689
3095
|
.ntr-permission-option:focus-visible, .ntr-permission-cancel:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #2563eb); outline-offset: 2px; }
|
|
2690
3096
|
.ntr-permission-cancel { margin-left: auto; color: var(--dsw-alias-label-secondary, #6b7280); }
|
|
2691
3097
|
.ntr-permission-error { padding: 4px 8px; border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #b91c1c) 12%, transparent); color: var(--dsw-alias-state-error-primary, #b91c1c); font-size: 11px; word-break: break-word; }
|
|
3098
|
+
/* ---- Released-session banner (conversation.input.dock, released-banner.tsx) ---- */
|
|
3099
|
+
/* Same composer-width formula as the permission dock above: the slot spans
|
|
3100
|
+
the column, the card lines up with the input box. One row of state + one
|
|
3101
|
+
action; the error pins under it, beside the button that failed. */
|
|
3102
|
+
.ntr-released-banner {
|
|
3103
|
+
box-sizing: border-box;
|
|
3104
|
+
width: calc(100% - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-side-clearance, 16px));
|
|
3105
|
+
max-width: var(--dsh-composer-card-max-width, 920px);
|
|
3106
|
+
margin: 0 auto;
|
|
3107
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
3108
|
+
padding: 8px 12px;
|
|
3109
|
+
border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
|
|
3110
|
+
border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706);
|
|
3111
|
+
border-radius: 10px;
|
|
3112
|
+
background: var(--dsw-alias-bg-layer-1, #fff);
|
|
3113
|
+
font-size: 12px; line-height: 1.5;
|
|
3114
|
+
}
|
|
3115
|
+
.ntr-released-banner-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3116
|
+
.ntr-released-banner-text { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
|
|
3117
|
+
.ntr-released-banner-title { font-weight: 600; }
|
|
3118
|
+
.ntr-released-banner-body { color: var(--dsw-alias-label-secondary, #6b7280); }
|
|
3119
|
+
.ntr-released-banner .ntr-acp-more { flex: none; white-space: nowrap; }
|
|
3120
|
+
/* ---- NTR message actions (conversation.chat.assistant-actions) ---- */
|
|
3121
|
+
/* In an NTR conversation the host's like / dislike / branch icons are the
|
|
3122
|
+
plugin's business (the agent decides its own feedback surface, and the
|
|
3123
|
+
host's branch would fork a conversation the agent does not remember). The
|
|
3124
|
+
three icons hide under the html[data-ntr-acp] scope marker, by their
|
|
3125
|
+
LOCALIZED aria-labels — the only stable handles the host leaves (its button
|
|
3126
|
+
classes are CSS-module hashes and there is no data-* hook). Hiding the
|
|
3127
|
+
button is the WHOLE job: the host's Tooltip clones its child without a
|
|
3128
|
+
wrapper element, so there is no container left behind to plug the row gap
|
|
3129
|
+
— and any :has(> button…) rule would match the actions row itself (and the
|
|
3130
|
+
list slot's shared display:contents anchor), hiding the entire row. The
|
|
3131
|
+
:not([data-ntr-branch]) exemption is the pass our OWN button carries —
|
|
3132
|
+
same label, same row. Known fragility: a host copy change or a third
|
|
3133
|
+
language un-hides the icons; the entry probes the labels once per page and
|
|
3134
|
+
warns in the console. */
|
|
3135
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="在新对话中分支"]:not([data-ntr-branch]),
|
|
3136
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="Branch into a new conversation"]:not([data-ntr-branch]),
|
|
3137
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="好的回答"],
|
|
3138
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="有问题的回答"],
|
|
3139
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="取消标记"],
|
|
3140
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="Good response"],
|
|
3141
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="Bad response"],
|
|
3142
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
3143
|
+
display: none !important;
|
|
3144
|
+
}
|
|
3145
|
+
/* The plugin's own branch button, dressed exactly like the host's .action
|
|
3146
|
+
class (the values are copied from the host's compiled CSS, token colors
|
|
3147
|
+
only). It rides the same actions row, so the host's row gap, font-delta
|
|
3148
|
+
sizing and the [data-actions-reveal] hover rules already govern it. */
|
|
3149
|
+
.ntr-branch-action {
|
|
3150
|
+
width: calc(28px + var(--dsh-content-font-delta, 0px));
|
|
3151
|
+
height: calc(28px + var(--dsh-content-font-delta, 0px));
|
|
3152
|
+
color: var(--dsw-alias-label-tertiary, #9ca3af);
|
|
3153
|
+
cursor: pointer;
|
|
3154
|
+
background: none;
|
|
3155
|
+
border: none;
|
|
3156
|
+
border-radius: 28px;
|
|
3157
|
+
justify-content: center;
|
|
3158
|
+
align-items: center;
|
|
3159
|
+
padding: 6px;
|
|
3160
|
+
display: inline-flex;
|
|
3161
|
+
}
|
|
3162
|
+
.ntr-branch-action svg { width: calc(15px + var(--dsh-content-font-delta, 0px)); height: calc(15px + var(--dsh-content-font-delta, 0px)); }
|
|
3163
|
+
.ntr-branch-action:hover { background: var(--dsw-alias-interactive-bg-hover, #0000000d); color: var(--dsw-alias-label-secondary, #6b7280); }
|
|
3164
|
+
.ntr-branch-action[data-unavailable] { cursor: default; opacity: 0.4; }
|
|
3165
|
+
.ntr-branch-action[data-unavailable]:hover { color: var(--dsw-alias-label-tertiary, #9ca3af); background: none; }
|
|
3166
|
+
.ntr-branch-action:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #2563eb); outline-offset: 2px; }
|
|
3167
|
+
/* A failed branch click, pinned in the row beside the button (the conversation
|
|
3168
|
+
surface of the settings banner the store also writes). One line, truncated;
|
|
3169
|
+
the full text is the title tooltip and the banner. */
|
|
3170
|
+
.ntr-branch-error { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; line-height: calc(28px + var(--dsh-content-font-delta, 0px)); color: var(--dsw-alias-state-error-primary, #b91c1c); }
|
|
2692
3171
|
/* ---- NTR settings dialog (shell.overlay) ---- */
|
|
2693
3172
|
/* Frame-wide surface opened from the agent base's empty-roster menu. The
|
|
2694
3173
|
overlay layer itself is click-through, so the layer opts back in and blocks
|
|
@@ -3146,22 +3625,14 @@ window.__ModuleLoader__.load({
|
|
|
3146
3625
|
* def (env / npmPackage / exposedModels included); the synthesized fallback
|
|
3147
3626
|
* covers only the fields an `AgentView` carries, so anything else compares as
|
|
3148
3627
|
* "changed" — the honest answer when the host never reported it.
|
|
3628
|
+
*
|
|
3629
|
+
* The idle-release field is compared AS STORED: an absent per-agent field
|
|
3630
|
+
* means "follow the global window", and the host's resolved value must not be
|
|
3631
|
+
* written back into it — that would freeze the global window into this agent's
|
|
3632
|
+
* definition and the global slider would then never reach it.
|
|
3149
3633
|
*/
|
|
3150
|
-
/**
|
|
3151
|
-
* The idle-release window in effect for an agent: an unset per-agent field is
|
|
3152
|
-
* resolved by the HOST and reported back on `AgentView`, so filling it from
|
|
3153
|
-
* there keeps "unset" and "explicitly the host default" the same state —
|
|
3154
|
-
* merely opening the form must never read as an unsaved change.
|
|
3155
|
-
*/
|
|
3156
|
-
function resolvedIdleTimeoutMs(def, activeAgent) {
|
|
3157
|
-
if (def.idleTimeoutMs !== void 0 || activeAgent?.idleTimeoutMs === void 0) return def;
|
|
3158
|
-
return {
|
|
3159
|
-
...def,
|
|
3160
|
-
idleTimeoutMs: activeAgent.idleTimeoutMs
|
|
3161
|
-
};
|
|
3162
|
-
}
|
|
3163
3634
|
function persistedDef(detected, activeAgent) {
|
|
3164
|
-
if (detected.currentConfig !== void 0) return
|
|
3635
|
+
if (detected.currentConfig !== void 0) return detected.currentConfig;
|
|
3165
3636
|
if (activeAgent === void 0) return void 0;
|
|
3166
3637
|
return {
|
|
3167
3638
|
id: activeAgent.id,
|
|
@@ -3170,8 +3641,7 @@ window.__ModuleLoader__.load({
|
|
|
3170
3641
|
args: [...activeAgent.args],
|
|
3171
3642
|
...activeAgent.cwd === void 0 ? {} : { cwd: activeAgent.cwd },
|
|
3172
3643
|
allowedCwdRoots: [...activeAgent.allowedCwdRoots],
|
|
3173
|
-
env: {}
|
|
3174
|
-
...activeAgent.idleTimeoutMs === void 0 ? {} : { idleTimeoutMs: activeAgent.idleTimeoutMs }
|
|
3644
|
+
env: {}
|
|
3175
3645
|
};
|
|
3176
3646
|
}
|
|
3177
3647
|
/**
|
|
@@ -3192,7 +3662,7 @@ window.__ModuleLoader__.load({
|
|
|
3192
3662
|
if (exposureKey(payload.exposedModels) !== exposureKey(current.exposedModels)) return false;
|
|
3193
3663
|
if (mcpKey(payload.mcpServers) !== mcpKey(current.mcpServers)) return false;
|
|
3194
3664
|
if (payload.autoDiscoverModels !== false !== (current.autoDiscoverModels !== false)) return false;
|
|
3195
|
-
if (payload.idleTimeoutMs !==
|
|
3665
|
+
if (payload.idleTimeoutMs !== current.idleTimeoutMs) return false;
|
|
3196
3666
|
return true;
|
|
3197
3667
|
}
|
|
3198
3668
|
/**
|
|
@@ -3501,6 +3971,118 @@ window.__ModuleLoader__.load({
|
|
|
3501
3971
|
});
|
|
3502
3972
|
}
|
|
3503
3973
|
/**
|
|
3974
|
+
* The idle-release window, as the settings page edits it.
|
|
3975
|
+
*
|
|
3976
|
+
* ONE control, because there is ONE window: how long a chat process may sit
|
|
3977
|
+
* idle before it is parked. The slider snaps to the shared ladder
|
|
3978
|
+
* ({@link IDLE_RELEASE_MS}), so every rung it offers is a window the host can
|
|
3979
|
+
* run.
|
|
3980
|
+
*
|
|
3981
|
+
* The card reports the deployment baseline as the fact being overridden, and
|
|
3982
|
+
* offers a reset ONLY when something is really overridden — the same rule the
|
|
3983
|
+
* approval deadline card follows.
|
|
3984
|
+
*/
|
|
3985
|
+
function IdleReleaseCard(props) {
|
|
3986
|
+
const { acp, ui } = props;
|
|
3987
|
+
const release = ui.idleRelease;
|
|
3988
|
+
if (release === void 0) return null;
|
|
3989
|
+
const baseline = release.defaultIdleTimeoutMs;
|
|
3990
|
+
const overridden = release.idleTimeoutMs !== baseline;
|
|
3991
|
+
const index = IDLE_RELEASE_MS.indexOf(release.idleTimeoutMs);
|
|
3992
|
+
const exact = index >= 0;
|
|
3993
|
+
const shown = exact ? release.idleTimeoutMs : nearestIdleRung(release.idleTimeoutMs);
|
|
3994
|
+
const note = release.configuredIdleTimeoutMs === void 0 || release.configuredIdleTimeoutMs === release.idleTimeoutMs ? void 0 : acp.label(`配置文件(cordis.patch.yml)设为 ${idleWindowText(release.configuredIdleTimeoutMs, acp)},此处设置优先。`, `The config file (cordis.patch.yml) asks for ${idleWindowText(release.configuredIdleTimeoutMs, acp)}; the value set here wins.`);
|
|
3995
|
+
const resetLabel = release.configuredIdleTimeoutMs === void 0 ? acp.label("恢复默认值", "Restore default") : acp.label("恢复配置文件的值", "Use config file value");
|
|
3996
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3997
|
+
className: "dwa-section",
|
|
3998
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3999
|
+
className: "dwa-section-heading",
|
|
4000
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
4001
|
+
className: "dwa-section-title",
|
|
4002
|
+
children: acp.label("空闲释放", "Idle release")
|
|
4003
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4004
|
+
className: "dwa-section-copy",
|
|
4005
|
+
children: acp.label("空闲多久后挂起聊天进程。", "How long an idle chat process is kept before it is parked.")
|
|
4006
|
+
})] })
|
|
4007
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4008
|
+
className: "dwa-preferences",
|
|
4009
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4010
|
+
className: "dwa-preference-block",
|
|
4011
|
+
children: [
|
|
4012
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4013
|
+
className: "dwa-preference-label",
|
|
4014
|
+
children: acp.label("释放窗口", "Release window")
|
|
4015
|
+
}),
|
|
4016
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4017
|
+
className: "dwa-deadline-value",
|
|
4018
|
+
children: idleWindowText(release.idleTimeoutMs, acp)
|
|
4019
|
+
}),
|
|
4020
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4021
|
+
className: "dwa-deadline-range",
|
|
4022
|
+
type: "range",
|
|
4023
|
+
min: 0,
|
|
4024
|
+
max: IDLE_RELEASE_MS.length - 1,
|
|
4025
|
+
step: 1,
|
|
4026
|
+
value: exact ? index : IDLE_RELEASE_MS.indexOf(shown),
|
|
4027
|
+
"aria-label": acp.label("释放窗口", "Release window"),
|
|
4028
|
+
onChange: (event) => {
|
|
4029
|
+
const next = IDLE_RELEASE_MS[Number(event.target.value)];
|
|
4030
|
+
if (next !== void 0 && next !== release.idleTimeoutMs) acp.setIdleRelease(next);
|
|
4031
|
+
}
|
|
4032
|
+
}),
|
|
4033
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4034
|
+
className: "dwa-deadline-ticks",
|
|
4035
|
+
children: IDLE_RELEASE_MS.filter((_, i) => i % 2 === 0).map((ms) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: idleWindowText(ms, acp) }, ms))
|
|
4036
|
+
}),
|
|
4037
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4038
|
+
className: "dwa-preference-copy",
|
|
4039
|
+
children: acp.label("挂起后会话仍保留,下一条消息自动恢复。", "A parked session is kept; the next message resumes it.")
|
|
4040
|
+
}),
|
|
4041
|
+
note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4042
|
+
className: "dwa-deadline-flag",
|
|
4043
|
+
children: ["⚠ ", note]
|
|
4044
|
+
})
|
|
4045
|
+
]
|
|
4046
|
+
}), overridden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4047
|
+
className: "dwa-deadline-reset",
|
|
4048
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4049
|
+
type: "button",
|
|
4050
|
+
className: "dwa-btn dwa-btn-secondary",
|
|
4051
|
+
onClick: () => void acp.resetIdleRelease(),
|
|
4052
|
+
children: resetLabel
|
|
4053
|
+
})
|
|
4054
|
+
})]
|
|
4055
|
+
})]
|
|
4056
|
+
});
|
|
4057
|
+
}
|
|
4058
|
+
/**
|
|
4059
|
+
* The rung nearest a window that is not on the ladder — used only to park the
|
|
4060
|
+
* thumb; the readout always prints the exact value.
|
|
4061
|
+
*/
|
|
4062
|
+
function nearestIdleRung(ms) {
|
|
4063
|
+
let best = IDLE_RELEASE_MS[0];
|
|
4064
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
4065
|
+
for (const rung of IDLE_RELEASE_MS) {
|
|
4066
|
+
const distance = Math.abs(rung - ms);
|
|
4067
|
+
if (distance < bestDistance) {
|
|
4068
|
+
bestDistance = distance;
|
|
4069
|
+
best = rung;
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
return best;
|
|
4073
|
+
}
|
|
4074
|
+
/**
|
|
4075
|
+
* `30 分钟` — the readout and the tick label, one formatter.
|
|
4076
|
+
*
|
|
4077
|
+
* ALWAYS minutes, even at the top of the ladder: a scale that runs "1 分钟"
|
|
4078
|
+
* to "50 分钟" and then "1 小时" reads as two different scales.
|
|
4079
|
+
*/
|
|
4080
|
+
function idleWindowText(ms, acp) {
|
|
4081
|
+
if (ms <= 0) return acp.label("永不释放", "Never");
|
|
4082
|
+
const minutes = Math.round(ms / 6e4);
|
|
4083
|
+
return acp.label(`${String(minutes)} 分钟`, `${String(minutes)}m`);
|
|
4084
|
+
}
|
|
4085
|
+
/**
|
|
3504
4086
|
* Local wall-clock `HH:MM:SS` for one runtime-log line. Hand-built on purpose:
|
|
3505
4087
|
* `toLocaleTimeString` may drop seconds or pick a 12-hour clock depending on
|
|
3506
4088
|
* the browser locale, which makes the column ragged and ambiguous.
|
|
@@ -3826,6 +4408,12 @@ window.__ModuleLoader__.load({
|
|
|
3826
4408
|
onClick: () => setPage("approvals"),
|
|
3827
4409
|
children: acp.label("审批时限", "Approval deadlines")
|
|
3828
4410
|
}),
|
|
4411
|
+
ui.idleRelease !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4412
|
+
type: "button",
|
|
4413
|
+
"aria-pressed": page === "processes",
|
|
4414
|
+
onClick: () => setPage("processes"),
|
|
4415
|
+
children: acp.label("进程管理", "Processes")
|
|
4416
|
+
}),
|
|
3829
4417
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3830
4418
|
type: "button",
|
|
3831
4419
|
"aria-pressed": page === "appearance",
|
|
@@ -4029,6 +4617,13 @@ window.__ModuleLoader__.load({
|
|
|
4029
4617
|
ui
|
|
4030
4618
|
})
|
|
4031
4619
|
}),
|
|
4620
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4621
|
+
hidden: page !== "processes",
|
|
4622
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IdleReleaseCard, {
|
|
4623
|
+
acp,
|
|
4624
|
+
ui
|
|
4625
|
+
})
|
|
4626
|
+
}),
|
|
4032
4627
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4033
4628
|
className: "dwa-section",
|
|
4034
4629
|
hidden: page !== "appearance",
|
|
@@ -4421,13 +5016,6 @@ window.__ModuleLoader__.load({
|
|
|
4421
5016
|
});
|
|
4422
5017
|
}
|
|
4423
5018
|
/**
|
|
4424
|
-
* Hand copy of `DEFAULT_TUNING.idleTimeoutMs` (src/bridge.ts): the browser
|
|
4425
|
-
* cannot import the host plane, and this number is only ever placeholder text
|
|
4426
|
-
* for a NOT-yet-configured agent (a configured one renders the host's resolved
|
|
4427
|
-
* value from `AgentView.idleTimeoutMs`). Keep the two in sync.
|
|
4428
|
-
*/
|
|
4429
|
-
const DEFAULT_IDLE_RELEASE_MS = 18e5;
|
|
4430
|
-
/**
|
|
4431
5019
|
* Hand copy of `DEFAULT_TUNING.probeIdleTimeoutMs` (src/bridge.ts). A process
|
|
4432
5020
|
* that has never hosted a session — the model probe / post-save probe — lives
|
|
4433
5021
|
* by THIS window whatever the agent's own field says, so the placeholder has to
|
|
@@ -4451,7 +5039,7 @@ window.__ModuleLoader__.load({
|
|
|
4451
5039
|
})),
|
|
4452
5040
|
exposed: config?.exposedModels === void 0 ? void 0 : [...config.exposedModels],
|
|
4453
5041
|
autoDiscoverModels: config?.autoDiscoverModels !== false,
|
|
4454
|
-
idleStr: idleText(
|
|
5042
|
+
idleStr: idleText(config?.idleTimeoutMs),
|
|
4455
5043
|
mcpText: config?.mcpServers === void 0 ? "" : JSON.stringify(config.mcpServers, null, 2)
|
|
4456
5044
|
};
|
|
4457
5045
|
}
|
|
@@ -4652,18 +5240,18 @@ window.__ModuleLoader__.load({
|
|
|
4652
5240
|
children: [
|
|
4653
5241
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
4654
5242
|
className: "dwa-form-label",
|
|
4655
|
-
children: acp.label("
|
|
5243
|
+
children: acp.label("空闲释放覆盖(毫秒)", "Idle Release Override (ms)")
|
|
4656
5244
|
}),
|
|
4657
5245
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4658
5246
|
className: "dwa-input dwa-input-mono",
|
|
4659
|
-
"aria-label": acp.label("
|
|
5247
|
+
"aria-label": acp.label("空闲释放覆盖(毫秒)", "Idle Release Override (ms)"),
|
|
4660
5248
|
value: form.idleStr,
|
|
4661
5249
|
onChange: (e) => patch({ idleStr: e.target.value }),
|
|
4662
|
-
placeholder: acp.label(`留空 =
|
|
5250
|
+
placeholder: acp.label(`留空 = 跟随全局设置(探测进程固定 ${String(DEFAULT_PROBE_RELEASE_MS / 6e4)} 分钟)`, `Blank = follow the global setting (probes always release after ${String(DEFAULT_PROBE_RELEASE_MS / 6e4)} min)`)
|
|
4663
5251
|
}),
|
|
4664
5252
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4665
5253
|
className: idleInvalid ? "dwa-error" : "dwa-hint",
|
|
4666
|
-
children: idleInvalid ? acp.label("必须是 0(永不释放)或 [1000, 86400000] 之间的整数毫秒。", "Must be 0 (never release) or an integer in [1000, 86400000] ms.") : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: acp.label("
|
|
5254
|
+
children: idleInvalid ? acp.label("必须是 0(永不释放)或 [1000, 86400000] 之间的整数毫秒。", "Must be 0 (never release) or an integer in [1000, 86400000] ms.") : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: acp.label("仅覆盖此智能体;留空即跟随「进程管理」的全局设置。", "Overrides this agent only; leave blank to follow the global setting in “Processes”.") })
|
|
4667
5255
|
})
|
|
4668
5256
|
]
|
|
4669
5257
|
})
|
|
@@ -5478,6 +6066,34 @@ window.__ModuleLoader__.load({
|
|
|
5478
6066
|
function agentModeTitle(agentId, acp) {
|
|
5479
6067
|
return acp.label(`agent 模式(${agentId ?? "agent"}):由 agent 自己定义。NTR 会话里 DSH 的 Access mode 不显示,模式就在此切换。`, `agent mode (${agentId ?? "agent"}): defined by the agent. In an NTR conversation the DSH access mode is not shown, so this is where the mode is switched.`);
|
|
5480
6068
|
}
|
|
6069
|
+
/**
|
|
6070
|
+
* Whether this conversation is one the agent-mode control may serve.
|
|
6071
|
+
*
|
|
6072
|
+
* An EXPLICIT non-ACP verdict outranks a lingering session row: switching a
|
|
6073
|
+
* conversation off the NTR preset does not close its ACP session, so "a row
|
|
6074
|
+
* exists" alone would keep this control on an ordinary conversation — the same
|
|
6075
|
+
* precedence the agent tag, the context ring and the released banner already
|
|
6076
|
+
* apply. Only while the host has not answered (`undefined`) does a bound
|
|
6077
|
+
* session count as proof, which is what keeps the control available before the
|
|
6078
|
+
* host's first `session/chat-list` answer lands.
|
|
6079
|
+
*/
|
|
6080
|
+
function agentModeVisible(acpMode, hasBoundSession) {
|
|
6081
|
+
if (acpMode === false) return false;
|
|
6082
|
+
return acpMode === true || hasBoundSession;
|
|
6083
|
+
}
|
|
6084
|
+
/**
|
|
6085
|
+
* The mode the select shows as in force.
|
|
6086
|
+
*
|
|
6087
|
+
* A LIVE session is the authority (it is what the agent runs right now). A
|
|
6088
|
+
* RELEASED row only describes the mode the agent was parked ON, so a newer
|
|
6089
|
+
* pick — which `applyChatChoice` will apply on the next resume — outranks it
|
|
6090
|
+
* (and the parked row still beats the catalog default). Same precedence as
|
|
6091
|
+
* the model and thought level in `describeRoute`: a pre-switch on a parked
|
|
6092
|
+
* session must be visible or the control reads as dead.
|
|
6093
|
+
*/
|
|
6094
|
+
function modeInForce(target, persisted, fallback) {
|
|
6095
|
+
return (target !== void 0 && target.released !== true ? target.modes?.currentModeId : void 0) ?? persisted ?? target?.modes?.currentModeId ?? fallback ?? "";
|
|
6096
|
+
}
|
|
5481
6097
|
function AcpComposerMode({ acp, sessionId, useSession }) {
|
|
5482
6098
|
const ui = useUi(acp);
|
|
5483
6099
|
const dshSessionId = String(sessionId);
|
|
@@ -5492,7 +6108,7 @@ window.__ModuleLoader__.load({
|
|
|
5492
6108
|
]);
|
|
5493
6109
|
const bound = ui.chatSessionsFor === dshSessionId ? ui.chatSessions : [];
|
|
5494
6110
|
const target = bound.find((session) => !session.released) ?? bound[0];
|
|
5495
|
-
const isAcp =
|
|
6111
|
+
const isAcp = agentModeVisible(ui.acpMode[dshSessionId], target !== void 0);
|
|
5496
6112
|
const agentId = target?.agentId ?? ui.chatLockedAgent[dshSessionId] ?? ui.chatBoundAgent[dshSessionId] ?? ui.chatChoice[dshSessionId]?.agentId ?? (isAcp ? ui.defaultAgent : void 0);
|
|
5497
6113
|
const catalog = agentId === void 0 ? void 0 : ui.seatCatalogs[agentId];
|
|
5498
6114
|
(0, react.useEffect)(() => {
|
|
@@ -5504,15 +6120,15 @@ window.__ModuleLoader__.load({
|
|
|
5504
6120
|
isAcp,
|
|
5505
6121
|
target
|
|
5506
6122
|
]);
|
|
6123
|
+
if (!isAcp) return null;
|
|
5507
6124
|
const live = target?.modes?.availableModes ?? [];
|
|
5508
|
-
const modes = live.length > 0 ? live : (
|
|
6125
|
+
const modes = live.length > 0 ? live : (catalog?.mode?.choices ?? []).map((choice) => ({
|
|
5509
6126
|
id: choice.id,
|
|
5510
6127
|
name: choice.name,
|
|
5511
6128
|
description: choice.description
|
|
5512
6129
|
}));
|
|
5513
6130
|
if (modes.length === 0) return null;
|
|
5514
|
-
const
|
|
5515
|
-
const current = target?.modes?.currentModeId ?? persisted ?? catalog?.mode?.currentValue ?? modes[0]?.id ?? "";
|
|
6131
|
+
const current = modeInForce(target, ui.chatChoice[dshSessionId]?.agentId === agentId ? ui.chatChoice[dshSessionId]?.mode : void 0, catalog?.mode?.currentValue ?? modes[0]?.id);
|
|
5516
6132
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
5517
6133
|
style: {
|
|
5518
6134
|
display: "inline-flex",
|
|
@@ -5548,6 +6164,109 @@ window.__ModuleLoader__.load({
|
|
|
5548
6164
|
})
|
|
5549
6165
|
});
|
|
5550
6166
|
}
|
|
6167
|
+
//#endregion
|
|
6168
|
+
//#region src/client/context-meter.tsx
|
|
6169
|
+
/**
|
|
6170
|
+
* Composer context-occupancy ring at `conversation.composer.dock` — the bottom
|
|
6171
|
+
* info row below the input card, where the host also renders its ContextMeter.
|
|
6172
|
+
*
|
|
6173
|
+
* WHY A PLUGIN-OWNED RING: the host's meter reads its `contextPressure`
|
|
6174
|
+
* projection, folded from DSH provider-usage samples plus a route capacity. An
|
|
6175
|
+
* ACP conversation produces neither — the agent owns the request and the host
|
|
6176
|
+
* never sees its prompt — so the native meter renders nothing there. This ring
|
|
6177
|
+
* reports the agent's OWN `usage_update` used/size instead: occupancy only. ACP
|
|
6178
|
+
* declares no system/tools/messages split, so no breakdown is invented.
|
|
6179
|
+
*
|
|
6180
|
+
* Registered for every conversation and gating itself: it renders only for an
|
|
6181
|
+
* ACP conversation whose session row carries usage. The row comes from
|
|
6182
|
+
* `session/chat-list`; the store live-patches it from the coalesced
|
|
6183
|
+
* `usage_update` frames (see `AcpStore.flushUpdates`).
|
|
6184
|
+
*/
|
|
6185
|
+
/**
|
|
6186
|
+
* The sample this conversation's ring should show, or `undefined` when the
|
|
6187
|
+
* entry must render nothing. An explicit `acpMode === false` outranks a stale
|
|
6188
|
+
* `chatSessions` row: the conversation's preset left NTR while its ACP session
|
|
6189
|
+
* was still alive, and the ring must leave with it (same precedence as the
|
|
6190
|
+
* agent tag's `agentTagVisible`). While the verdict is unknown, a LIVE row is
|
|
6191
|
+
* proof; a PARKED one is not — it can outlive the preset switch, and the ring
|
|
6192
|
+
* must not appear on an ordinary conversation during the answer window.
|
|
6193
|
+
*/
|
|
6194
|
+
function acpMeterUsage(ui, dshSessionId) {
|
|
6195
|
+
if (ui.acpMode[dshSessionId] === false) return void 0;
|
|
6196
|
+
if (ui.chatSessionsFor !== dshSessionId) return void 0;
|
|
6197
|
+
const live = ui.chatSessions.find((row) => !row.released);
|
|
6198
|
+
if (live === void 0 && ui.acpMode[dshSessionId] !== true) return void 0;
|
|
6199
|
+
return (live ?? ui.chatSessions[0])?.usage;
|
|
6200
|
+
}
|
|
6201
|
+
/**
|
|
6202
|
+
* Bounded display occupancy from one agent sample. `undefined` (no meter) for
|
|
6203
|
+
* an absent / malformed / zero-capacity sample rather than a made-up 0%.
|
|
6204
|
+
*/
|
|
6205
|
+
function contextOccupancy(usage) {
|
|
6206
|
+
if (usage === void 0) return void 0;
|
|
6207
|
+
const { used, size } = usage;
|
|
6208
|
+
if (!Number.isFinite(used) || !Number.isFinite(size) || used < 0 || size <= 0) return void 0;
|
|
6209
|
+
return {
|
|
6210
|
+
percent: Math.min(100, Math.round(used / size * 100)),
|
|
6211
|
+
used,
|
|
6212
|
+
size
|
|
6213
|
+
};
|
|
6214
|
+
}
|
|
6215
|
+
/** Compact token count for the tooltip, in the host meter's K/M shape. */
|
|
6216
|
+
function compactTokenCount(value) {
|
|
6217
|
+
if (value < 1e3) return String(Math.round(value));
|
|
6218
|
+
const thousands = value / 1e3;
|
|
6219
|
+
if (value < 1e6) return `${thousands >= 100 ? String(Math.round(thousands)) : String(Math.round(thousands * 10) / 10)}K`;
|
|
6220
|
+
const millions = value / 1e6;
|
|
6221
|
+
return `${millions >= 100 ? String(Math.round(millions)) : String(Math.round(millions * 10) / 10)}M`;
|
|
6222
|
+
}
|
|
6223
|
+
/** Ring geometry matches the host ContextMeter: 14px viewBox, 2px stroke. */
|
|
6224
|
+
const RADIUS = 5.5;
|
|
6225
|
+
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
|
6226
|
+
function AcpContextMeter({ acp, sessionId, useSession }) {
|
|
6227
|
+
ensureAcpStyle();
|
|
6228
|
+
const ui = useUi$2(acp);
|
|
6229
|
+
const dshSessionId = String(sessionId);
|
|
6230
|
+
const running = useSession((snapshot) => snapshot.running);
|
|
6231
|
+
(0, react.useEffect)(() => {
|
|
6232
|
+
acp.loadChatSessions(dshSessionId);
|
|
6233
|
+
}, [
|
|
6234
|
+
acp,
|
|
6235
|
+
dshSessionId,
|
|
6236
|
+
running
|
|
6237
|
+
]);
|
|
6238
|
+
const context = contextOccupancy(acpMeterUsage(ui, dshSessionId));
|
|
6239
|
+
if (context === void 0) return null;
|
|
6240
|
+
const title = acp.label(`上下文已用 ${String(context.percent)}% · ${compactTokenCount(context.used)} / ${compactTokenCount(context.size)}`, `${String(context.percent)}% of context used · ${compactTokenCount(context.used)} / ${compactTokenCount(context.size)}`);
|
|
6241
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6242
|
+
className: "ntr-acp-meter",
|
|
6243
|
+
role: "img",
|
|
6244
|
+
"aria-label": title,
|
|
6245
|
+
title,
|
|
6246
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
6247
|
+
viewBox: "0 0 14 14",
|
|
6248
|
+
width: "14",
|
|
6249
|
+
height: "14",
|
|
6250
|
+
"aria-hidden": "true",
|
|
6251
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
6252
|
+
className: "ntr-acp-meter-track",
|
|
6253
|
+
cx: "7",
|
|
6254
|
+
cy: "7",
|
|
6255
|
+
r: RADIUS
|
|
6256
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
6257
|
+
className: "ntr-acp-meter-fill",
|
|
6258
|
+
cx: "7",
|
|
6259
|
+
cy: "7",
|
|
6260
|
+
r: RADIUS,
|
|
6261
|
+
strokeDasharray: `${String(CIRCUMFERENCE * context.percent / 100)} ${String(CIRCUMFERENCE)}`,
|
|
6262
|
+
transform: "rotate(-90 7 7)"
|
|
6263
|
+
})]
|
|
6264
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6265
|
+
className: "ntr-acp-meter-percent",
|
|
6266
|
+
children: [context.percent, "%"]
|
|
6267
|
+
})]
|
|
6268
|
+
});
|
|
6269
|
+
}
|
|
5551
6270
|
const phases = /* @__PURE__ */ new Map();
|
|
5552
6271
|
/** The reveal serial whose ceremony has already played (or was skipped). */
|
|
5553
6272
|
const consumed = /* @__PURE__ */ new Map();
|
|
@@ -5840,8 +6559,8 @@ window.__ModuleLoader__.load({
|
|
|
5840
6559
|
if (agentId === void 0) return { efforts: [] };
|
|
5841
6560
|
const agentTitle = agents.find((agent) => agent.id === agentId)?.title ?? bound?.agentTitle ?? catalog?.title ?? agentId;
|
|
5842
6561
|
const pick = choice !== void 0 && choice.agentId === agentId ? choice : void 0;
|
|
5843
|
-
const modelId = bound?.modelId ?? pick?.modelId;
|
|
5844
|
-
const effort = bound?.reasoningEffort ?? pick?.effort;
|
|
6562
|
+
const modelId = bound?.released === true ? pick?.modelId ?? bound?.modelId : bound?.modelId ?? pick?.modelId;
|
|
6563
|
+
const effort = bound?.released === true ? pick?.effort ?? bound?.reasoningEffort : bound?.reasoningEffort ?? pick?.effort;
|
|
5845
6564
|
const reasoning = catalog?.reasoning;
|
|
5846
6565
|
const agentModel = catalog?.currentModel;
|
|
5847
6566
|
if (bound === void 0 && pick === void 0 && agentModel === void 0) return {
|
|
@@ -6020,9 +6739,6 @@ window.__ModuleLoader__.load({
|
|
|
6020
6739
|
agentListed,
|
|
6021
6740
|
agentId
|
|
6022
6741
|
]);
|
|
6023
|
-
const route = describeRoute(agents, bound, lockedTo === void 0 ? ui.defaultAgent : void 0, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
|
|
6024
|
-
if (!agentBaseHasData(bound, agents, ui.agentsLoaded)) return null;
|
|
6025
|
-
const routed = route.agentId !== void 0;
|
|
6026
6742
|
(0, react.useEffect)(() => {
|
|
6027
6743
|
if (lockedTo === void 0 || menu === null) return;
|
|
6028
6744
|
if (menu.sessionId === sessionId && menu.kind === "agent") closeAcpMenu();
|
|
@@ -6032,6 +6748,10 @@ window.__ModuleLoader__.load({
|
|
|
6032
6748
|
menu?.kind,
|
|
6033
6749
|
sessionId
|
|
6034
6750
|
]);
|
|
6751
|
+
const route = describeRoute(agents, bound, lockedTo === void 0 ? ui.defaultAgent : void 0, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
|
|
6752
|
+
if (!agentBaseHasData(bound, agents, ui.agentsLoaded)) return null;
|
|
6753
|
+
const routed = route.agentId !== void 0;
|
|
6754
|
+
if (ui.acpMode[sessionId] !== true) return null;
|
|
6035
6755
|
const shattering = lockPhase === "shatter" && lockedTo !== void 0;
|
|
6036
6756
|
const showAgentBase = !agentBaseHidden(lockedTo) || shattering;
|
|
6037
6757
|
const modelLabel = routed ? route.modelLabel : void 0;
|
|
@@ -6337,6 +7057,7 @@ window.__ModuleLoader__.load({
|
|
|
6337
7057
|
agentId
|
|
6338
7058
|
]);
|
|
6339
7059
|
if (menu === null || menu.sessionId !== sessionId) return null;
|
|
7060
|
+
if (ui.acpMode[sessionId] !== true) return null;
|
|
6340
7061
|
const route = describeRoute(agents, bound, lockedTo === void 0 ? ui.defaultAgent : void 0, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
|
|
6341
7062
|
let model;
|
|
6342
7063
|
try {
|
|
@@ -6781,6 +7502,19 @@ window.__ModuleLoader__.load({
|
|
|
6781
7502
|
return list.find((session) => !session.released) ?? list[0];
|
|
6782
7503
|
}
|
|
6783
7504
|
/**
|
|
7505
|
+
* Whether an empty stream means "history exists but is not shown yet": the
|
|
7506
|
+
* record was restored from disk after a host restart (the agent's replay has
|
|
7507
|
+
* not landed), or a resume cleared the buffer ahead of the replay. The view
|
|
7508
|
+
* then promises the replay instead of claiming the conversation never
|
|
7509
|
+
* happened — "还没有消息" to someone whose chat plane still shows yesterday's
|
|
7510
|
+
* turns is exactly the kind of contradiction this file's copy rules exist to
|
|
7511
|
+
* prevent. In-memory released sessions keep their buffer, so this only fires
|
|
7512
|
+
* when the buffer really is absent.
|
|
7513
|
+
*/
|
|
7514
|
+
function historyAwaitsReplay(session) {
|
|
7515
|
+
return session.released === true && session.hasHistory === true;
|
|
7516
|
+
}
|
|
7517
|
+
/**
|
|
6784
7518
|
* Everything the view can read that says "this conversation is set up on an
|
|
6785
7519
|
* agent": a live or parked ACP session, the host's binding (`session/chat-list`'s
|
|
6786
7520
|
* `boundAgentId`, which outlives a closed session), and the persisted pick.
|
|
@@ -6869,6 +7603,7 @@ window.__ModuleLoader__.load({
|
|
|
6869
7603
|
if (node === null || !pinned.current) return;
|
|
6870
7604
|
node.scrollTop = node.scrollHeight;
|
|
6871
7605
|
}, [blockCount, tail]);
|
|
7606
|
+
if (ui.acpMode[sessionId] !== true) return null;
|
|
6872
7607
|
if (transcript === void 0) {
|
|
6873
7608
|
const empty = acpEmptyCopy({
|
|
6874
7609
|
bound: boundAgent,
|
|
@@ -6980,7 +7715,7 @@ window.__ModuleLoader__.load({
|
|
|
6980
7715
|
},
|
|
6981
7716
|
children: transcript.blocks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6982
7717
|
className: "ntr-acp-empty-hint",
|
|
6983
|
-
children: t("还没有消息:在下面的输入框里发给它。", "Nothing yet — send it a message below.")
|
|
7718
|
+
children: historyAwaitsReplay(session) ? t("进程已释放;历史将在恢复后回放显示。", "The process is released — history replays once it is restored.") : t("还没有消息:在下面的输入框里发给它。", "Nothing yet — send it a message below.")
|
|
6984
7719
|
}) : transcript.blocks.map((block, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockView, {
|
|
6985
7720
|
block,
|
|
6986
7721
|
label: acp.label,
|
|
@@ -7184,6 +7919,234 @@ window.__ModuleLoader__.load({
|
|
|
7184
7919
|
});
|
|
7185
7920
|
}
|
|
7186
7921
|
//#endregion
|
|
7922
|
+
//#region src/client/released-banner.tsx
|
|
7923
|
+
/**
|
|
7924
|
+
* The conversation's released-session banner: one row above the composer
|
|
7925
|
+
* (`conversation.input.dock`) saying the agent process behind this
|
|
7926
|
+
* conversation has been released, with the explicit resume button.
|
|
7927
|
+
*
|
|
7928
|
+
* WHY A BANNER: releasing a process is deliberately silent (constraint 19 —
|
|
7929
|
+
* an idle teardown is the expected lifecycle, no `agent/notice`), and the
|
|
7930
|
+
* only other trace was the NTR view tab's status badge — a surface most
|
|
7931
|
+
* users never open. The "your next message resumes it" promise is real but
|
|
7932
|
+
* unverifiable from the chat itself, so the banner states it here and the
|
|
7933
|
+
* button turns it into a gesture (`session/chat-resume`), the explicit form
|
|
7934
|
+
* of the same recovery the next prompt performs lazily.
|
|
7935
|
+
*
|
|
7936
|
+
* The gate is a pure function ({@link releasedBannerVisible}) — the seam the
|
|
7937
|
+
* composer's other visibility decisions use, so the rules are testable with
|
|
7938
|
+
* no DOM. Like the agent tag and the context ring, an explicit
|
|
7939
|
+
* `acpMode === false` outranks a lingering `chatSessions` row: a conversation
|
|
7940
|
+
* that left NTR mode must not keep claiming a parked NTR process.
|
|
7941
|
+
*/
|
|
7942
|
+
/**
|
|
7943
|
+
* Whether the banner should render: this conversation's ACP session records
|
|
7944
|
+
* are all released — the process is parked (idle release or crash) and
|
|
7945
|
+
* nothing is live to serve the next message yet. An empty list (no agent has
|
|
7946
|
+
* run here) and a live row anywhere both mean "nothing to recover", so the
|
|
7947
|
+
* banner stays out of the way.
|
|
7948
|
+
*/
|
|
7949
|
+
function releasedBannerVisible(ui, dshSessionId) {
|
|
7950
|
+
if (ui.acpMode[dshSessionId] === false) return false;
|
|
7951
|
+
if (ui.chatSessionsFor !== dshSessionId) return false;
|
|
7952
|
+
return ui.chatSessions.length > 0 && ui.chatSessions.every((row) => row.released === true);
|
|
7953
|
+
}
|
|
7954
|
+
function ReleasedBanner({ acp, sessionId }) {
|
|
7955
|
+
ensureAcpStyle();
|
|
7956
|
+
const ui = useUi$2(acp);
|
|
7957
|
+
const dshSessionId = String(sessionId);
|
|
7958
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
7959
|
+
const [error, setError] = (0, react.useState)();
|
|
7960
|
+
(0, react.useEffect)(() => {
|
|
7961
|
+
acp.loadChatSessions(dshSessionId);
|
|
7962
|
+
}, [acp, dshSessionId]);
|
|
7963
|
+
if (!releasedBannerVisible(ui, dshSessionId)) return null;
|
|
7964
|
+
const resume = () => {
|
|
7965
|
+
if (busy) return;
|
|
7966
|
+
setBusy(true);
|
|
7967
|
+
setError(void 0);
|
|
7968
|
+
acp.resumeChatSession(dshSessionId).catch((failure) => {
|
|
7969
|
+
setError(failure instanceof Error ? failure.message : String(failure));
|
|
7970
|
+
}).finally(() => {
|
|
7971
|
+
setBusy(false);
|
|
7972
|
+
});
|
|
7973
|
+
};
|
|
7974
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7975
|
+
className: "ntr-released-banner",
|
|
7976
|
+
role: "region",
|
|
7977
|
+
"aria-label": acp.label("进程已释放", "Process released"),
|
|
7978
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7979
|
+
className: "ntr-released-banner-row",
|
|
7980
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7981
|
+
className: "ntr-released-banner-text",
|
|
7982
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7983
|
+
className: "ntr-released-banner-title",
|
|
7984
|
+
children: acp.label("进程已释放", "Process released")
|
|
7985
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7986
|
+
className: "ntr-released-banner-body",
|
|
7987
|
+
children: acp.label("上下文已保留;下一条消息将自动恢复,或点击【恢复进程】按钮。", "Context is kept — your next message resumes it, or click [Resume process].")
|
|
7988
|
+
})]
|
|
7989
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7990
|
+
type: "button",
|
|
7991
|
+
className: "ntr-acp-more is-primary",
|
|
7992
|
+
disabled: busy,
|
|
7993
|
+
title: acp.label("立即拉起 agent 进程并恢复这个会话", "Bring the agent process back and restore this session now"),
|
|
7994
|
+
onClick: resume,
|
|
7995
|
+
children: busy ? acp.label("正在恢复…", "Resuming…") : acp.label("恢复进程", "Resume process")
|
|
7996
|
+
})]
|
|
7997
|
+
}), error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7998
|
+
className: "ntr-permission-error",
|
|
7999
|
+
role: "alert",
|
|
8000
|
+
children: error
|
|
8001
|
+
})]
|
|
8002
|
+
});
|
|
8003
|
+
}
|
|
8004
|
+
//#endregion
|
|
8005
|
+
//#region src/client/branch-action.tsx
|
|
8006
|
+
/**
|
|
8007
|
+
* The NTR branch button (slot `conversation.chat.assistant-actions`): in an
|
|
8008
|
+
* ACP conversation this is the ONLY branch entry — the host's like / dislike /
|
|
8009
|
+
|
|
8010
|
+
* branch icons are hidden under `html[data-ntr-acp]` (see acp-ui.tsx), and
|
|
8011
|
+
* this button takes the host branch's place with the same icon, size and
|
|
8012
|
+
* row position (order 20, after the hidden feedback entry's order 10).
|
|
8013
|
+
*
|
|
8014
|
+
* Clicking runs the fixed two-stage branch: the HOST forks the DSH session
|
|
8015
|
+
* tree first (`sessions.fork`, structurally resolved), then the plugin adopts
|
|
8016
|
+
* the ACP side (`session/chat-fork-adopt` → the agent's own `session/fork`),
|
|
8017
|
+
* so the child conversation's agent actually remembers the parent context —
|
|
8018
|
+
* see the NTR message-actions plan, §D5/§D7.
|
|
8019
|
+
*
|
|
8020
|
+
* Availability is the store's `forkStateOf` six-state mapping over facts the
|
|
8021
|
+
* button never derives itself: the host's per-row `fork` verdict
|
|
8022
|
+
* (`session/chat-list`), the SSE running flag, the host-fork structural probe,
|
|
8023
|
+
* and the DOM tail verdict. The tail check reads the host's OWN
|
|
8024
|
+
* `[data-turn-tail]` marker (`data-actions-reveal === "always"` = the latest
|
|
8025
|
+
* completed turn) and fails CLOSED: a missing marker disables the button
|
|
8026
|
+
* rather than risking a fork cut at a point the user is not looking at.
|
|
8027
|
+
*/
|
|
8028
|
+
/**
|
|
8029
|
+
* Resolves the runtime's `sessions.fork` primitive, binding it to the service
|
|
8030
|
+
* instance so that internal `this` accesses (such as `this.list.getSnapshot()`)
|
|
8031
|
+
* resolve correctly when called through `face.fork(...)`.
|
|
8032
|
+
* Returns `undefined` if the service or `fork` method is absent or not a function.
|
|
8033
|
+
*/
|
|
8034
|
+
function resolveHostFork(sessionsService) {
|
|
8035
|
+
try {
|
|
8036
|
+
const sessions = sessionsService;
|
|
8037
|
+
const fork = sessions?.fork;
|
|
8038
|
+
return typeof fork === "function" ? { fork: fork.bind(sessions) } : void 0;
|
|
8039
|
+
} catch {
|
|
8040
|
+
return;
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
8043
|
+
/**
|
|
8044
|
+
* The host's own branch-button icon, picked per host generation. The icon
|
|
8045
|
+
* exports MOVED between shipped hosts — desktop 0.2.1-rc.2 has
|
|
8046
|
+
* `IconBranchOutlineRegular` / `…Medium` but NOT `…16`, newer packages export
|
|
8047
|
+
* `…16` — and a hard import of one name compiles to a plain property read
|
|
8048
|
+
* (`primitives.IconBranchOutline16`), so the missing export is `undefined`
|
|
8049
|
+
* and `<undefined />` throws "Element type is invalid". The slot's error
|
|
8050
|
+
* boundary then abdicates the whole entry: no button, no tooltip, nothing in
|
|
8051
|
+
* any log — the exact silent disappearance this picker exists to prevent.
|
|
8052
|
+
* Rendering the button never depends on the icon: with all three absent the
|
|
8053
|
+
* click still works, only the 28px glyph is missing.
|
|
8054
|
+
*/
|
|
8055
|
+
function branchIconOf(source) {
|
|
8056
|
+
return source.IconBranchOutline16 ?? source.IconBranchOutlineRegular ?? source.IconBranchOutlineMedium;
|
|
8057
|
+
}
|
|
8058
|
+
const BranchIcon = branchIconOf(_deepseek_ai_dsh_client_ui_primitives);
|
|
8059
|
+
/** What the label reads for each state (zh / en pairs, per the plan's §D4). */
|
|
8060
|
+
function tooltipFor(state, label) {
|
|
8061
|
+
switch (state) {
|
|
8062
|
+
case "ready": return label("在新对话中分支", "Branch into a new conversation");
|
|
8063
|
+
case "unsupported": return label("该 agent 不支持原生分叉", "This agent cannot fork natively");
|
|
8064
|
+
case "released": return label("会话已挂起,先发一条消息恢复再分叉", "Resume this session before branching");
|
|
8065
|
+
case "busy": return label("当前轮次结束后才能分叉", "Wait for the current turn to finish");
|
|
8066
|
+
case "notTail": return label("仅可从已完成轮次的最后一条消息分支", "Available only on the last message of a completed turn");
|
|
8067
|
+
case "hostUnavailable": return label("当前宿主不提供分叉", "This host does not support branching");
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
8070
|
+
function AcpBranchAction(props) {
|
|
8071
|
+
const ui = useUi$2(props.acp);
|
|
8072
|
+
const buttonRef = (0, react.useRef)(null);
|
|
8073
|
+
const [pending, setPending] = (0, react.useState)(false);
|
|
8074
|
+
/**
|
|
8075
|
+
* The host's own availability verdict for the turn tail this button sits
|
|
8076
|
+
* in, re-read after EVERY render (no dep array): the marker flips without
|
|
8077
|
+
* any store event of our own when a new turn lands, and before the first
|
|
8078
|
+
* layout pass the ref is not attached yet. Two facts, both the HOST's:
|
|
8079
|
+
* `data-actions-reveal === "always"` (this tail is the latest turn and
|
|
8080
|
+
* completed) and the host branch button's own `data-unavailable` (a stricter
|
|
8081
|
+
* gate — a tail whose final node is not the transcript's last, or a turn
|
|
8082
|
+
* with later nodes, is one the HOST refuses to fork). Reading the host's
|
|
8083
|
+
* button keeps the plugin's readiness a SUBSET of the host's: wherever the
|
|
8084
|
+
* two would disagree about the cut point, the plugin button disables, and
|
|
8085
|
+
* the remaining clicks fork at the same latest-completed prefix the host
|
|
8086
|
+
* would (the tail marked `always` is the latest completed turn, which is
|
|
8087
|
+
* exactly where the host's fallback `atSeq` lands).
|
|
8088
|
+
*/
|
|
8089
|
+
const [tailReady, setTailReady] = (0, react.useState)(false);
|
|
8090
|
+
(0, react.useLayoutEffect)(() => {
|
|
8091
|
+
const tail = buttonRef.current?.closest("[data-turn-tail]");
|
|
8092
|
+
const hostBranch = tail?.querySelector("button[aria-label=\"在新对话中分支\"]:not([data-ntr-branch]), button[aria-label=\"Branch into a new conversation\"]:not([data-ntr-branch])");
|
|
8093
|
+
setTailReady(tail?.getAttribute("data-actions-reveal") === "always" && hostBranch?.hasAttribute("data-unavailable") !== true);
|
|
8094
|
+
});
|
|
8095
|
+
/**
|
|
8096
|
+
* The click's local failure line. The store ALSO flashes the same reason
|
|
8097
|
+
* into the settings banner, but the user is HERE — without this row the
|
|
8098
|
+
* click would "do nothing" visibly, the exact invisibility the permission
|
|
8099
|
+
* dock's pinned-error pattern exists to prevent.
|
|
8100
|
+
*/
|
|
8101
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
8102
|
+
if (ui.acpMode[props.sessionId] !== true) return null;
|
|
8103
|
+
if (ui.chatSessionsFor !== props.sessionId) return null;
|
|
8104
|
+
const row = ui.chatSessionsFor === props.sessionId ? ui.chatSessions[0] : void 0;
|
|
8105
|
+
const forkFace = props.hostFork?.();
|
|
8106
|
+
const liveRunning = row !== void 0 && ui.sessions.some((s) => s.sessionId === row.sessionId && s.running);
|
|
8107
|
+
const state = forkStateOf(row, forkFace !== void 0, tailReady, liveRunning);
|
|
8108
|
+
const disabled = state !== "ready" || pending;
|
|
8109
|
+
const branchLabel = props.acp.label("在新对话中分支", "Branch into a new conversation");
|
|
8110
|
+
const onClick = () => {
|
|
8111
|
+
if (state !== "ready" || pending) return;
|
|
8112
|
+
setPending(true);
|
|
8113
|
+
setFailure(void 0);
|
|
8114
|
+
props.acp.forkChatSession(props.sessionId, (sessionId) => {
|
|
8115
|
+
const face = props.hostFork?.();
|
|
8116
|
+
if (face === void 0) throw new Error("acp: host fork service is gone");
|
|
8117
|
+
return face.fork({
|
|
8118
|
+
sessionId,
|
|
8119
|
+
increaseTitle: true
|
|
8120
|
+
});
|
|
8121
|
+
}).then((outcome) => {
|
|
8122
|
+
if ("childId" in outcome) props.openSession?.(outcome.childId);
|
|
8123
|
+
else setFailure(outcome.failure);
|
|
8124
|
+
}).finally(() => {
|
|
8125
|
+
setPending(false);
|
|
8126
|
+
});
|
|
8127
|
+
};
|
|
8128
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
8129
|
+
label: tooltipFor(state, props.acp.label),
|
|
8130
|
+
side: "bottom",
|
|
8131
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8132
|
+
ref: buttonRef,
|
|
8133
|
+
type: "button",
|
|
8134
|
+
className: "ntr-branch-action",
|
|
8135
|
+
"aria-label": branchLabel,
|
|
8136
|
+
"aria-disabled": disabled || void 0,
|
|
8137
|
+
"data-ntr-branch": "",
|
|
8138
|
+
"data-unavailable": disabled || void 0,
|
|
8139
|
+
onClick: disabled ? void 0 : onClick,
|
|
8140
|
+
children: BranchIcon === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BranchIcon, {})
|
|
8141
|
+
})
|
|
8142
|
+
}), failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8143
|
+
className: "ntr-branch-error",
|
|
8144
|
+
role: "alert",
|
|
8145
|
+
title: failure,
|
|
8146
|
+
children: failure
|
|
8147
|
+
})] });
|
|
8148
|
+
}
|
|
8149
|
+
//#endregion
|
|
7187
8150
|
//#region src/client/model-seat.tsx
|
|
7188
8151
|
/** Exact key of the composer's model seat (a `single`, session-scoped slot). */
|
|
7189
8152
|
const MODEL_SEAT_KEY = "conversation.input.model";
|
|
@@ -7206,7 +8169,7 @@ window.__ModuleLoader__.load({
|
|
|
7206
8169
|
* `entriesOfSlot` and performing bounded retakes if displaced by later registrations.
|
|
7207
8170
|
*/
|
|
7208
8171
|
function createSeatClaim(options) {
|
|
7209
|
-
const { slots, slotKey, basePriority, occupant, inject } = options;
|
|
8172
|
+
const { slots, slotKey, basePriority, occupant, inject, entryId } = options;
|
|
7210
8173
|
const label = options.label ?? slotKey;
|
|
7211
8174
|
const onDiag = options.onDiag;
|
|
7212
8175
|
const maxClaims = options.maxClaims ?? 4;
|
|
@@ -7236,10 +8199,11 @@ window.__ModuleLoader__.load({
|
|
|
7236
8199
|
};
|
|
7237
8200
|
try {
|
|
7238
8201
|
const heads = slots.entriesOfSlot(slotKey) ?? [];
|
|
8202
|
+
const entry = entryId === void 0 ? heads[0] : heads.find((head) => head.options?.id === entryId);
|
|
7239
8203
|
return {
|
|
7240
8204
|
declared: isDeclared,
|
|
7241
8205
|
verified: true,
|
|
7242
|
-
...
|
|
8206
|
+
...entry === void 0 ? {} : { entry }
|
|
7243
8207
|
};
|
|
7244
8208
|
} catch {
|
|
7245
8209
|
return {
|
|
@@ -7260,6 +8224,9 @@ window.__ModuleLoader__.load({
|
|
|
7260
8224
|
try {
|
|
7261
8225
|
installed.push(slots.inject(slotKey, () => slots.register({
|
|
7262
8226
|
name: slotKey,
|
|
8227
|
+
...entryId === void 0 ? {} : { id: entryId },
|
|
8228
|
+
...options.order === void 0 ? {} : { order: options.order },
|
|
8229
|
+
...options.locale === void 0 ? {} : { locale: options.locale },
|
|
7263
8230
|
priority: rank,
|
|
7264
8231
|
...inject ? { inject } : {}
|
|
7265
8232
|
}, component)));
|
|
@@ -7371,6 +8338,159 @@ window.__ModuleLoader__.load({
|
|
|
7371
8338
|
});
|
|
7372
8339
|
}
|
|
7373
8340
|
//#endregion
|
|
8341
|
+
//#region src/client/activity-pill.tsx
|
|
8342
|
+
/**
|
|
8343
|
+
* The composer dock's `x轮x步 · xx tok/s` pill on an ACP conversation.
|
|
8344
|
+
*
|
|
8345
|
+
* WHY SHADOW INSTEAD OF A SECOND PILL: the host's `ActivityPill` already states
|
|
8346
|
+
* the two things this conversation has — turn/step counts and a whole-session
|
|
8347
|
+
* decode speed — in the one place a speed belongs. Only its speed is wrong for
|
|
8348
|
+
* ACP: the host divides the tokens a step emitted by `firstTokenTime →
|
|
8349
|
+
* completedTime`, and that window contains every tool wait. Rendering a second
|
|
8350
|
+
* speed beside it would leave two numbers on screen and no way to tell which
|
|
8351
|
+
* is the honest one. So this module takes the host pill's own cell (`list` slot
|
|
8352
|
+
* `conversation.composer.dock`, id `activity`) at a lower priority, renders the
|
|
8353
|
+
* HOST's component, and hands it a patched `sessionStats` whose decode fields
|
|
8354
|
+
* are the plugin's burst aggregate. Style, counts, dialog, aria and locale stay
|
|
8355
|
+
* the host's; only the number changes.
|
|
8356
|
+
*
|
|
8357
|
+
* NON-ACP CONVERSATIONS PASS THROUGH. The shadow is installed only while the
|
|
8358
|
+
* gate confirms the staged conversation runs the ACP preset, but a list entry
|
|
8359
|
+
* is projected into EVERY session's composer. The wrapper therefore renders the
|
|
8360
|
+
* host component with the ORIGINAL projection whenever `acpMode` is not
|
|
8361
|
+
* affirmatively true, making the shadow visually indistinguishable from the
|
|
8362
|
+
* shipped entry there.
|
|
8363
|
+
*
|
|
8364
|
+
* FAIL-OPEN IS STRUCTURAL: if the host entry, its inject face, its locale or
|
|
8365
|
+
* its `performanceUsage` source is missing, no claim is created and the native
|
|
8366
|
+
* pill stays exactly as shipped. If the host component rejects the patched
|
|
8367
|
+
* projection, the slot's error boundary retires this entry and the native
|
|
8368
|
+
* `activity` entry wins again. No CSS hiding and no DOM patching are involved.
|
|
8369
|
+
*/
|
|
8370
|
+
const DOCK_SLOT = "conversation.composer.dock";
|
|
8371
|
+
const ACTIVITY_ENTRY_ID = "activity";
|
|
8372
|
+
/**
|
|
8373
|
+
* The aggregate the dock should show: the sum of every ACP session backing this
|
|
8374
|
+
* chat (the host pill's counts are whole-chat too), with a live `turn/metrics`
|
|
8375
|
+
* entry preferred per session over its persisted row. `undefined` means "no
|
|
8376
|
+
* measurable burst yet" — the wrapper then hides the speed instead of showing
|
|
8377
|
+
* the host's tool-inclusive number.
|
|
8378
|
+
*/
|
|
8379
|
+
function activityBurst(ui, dshSessionId) {
|
|
8380
|
+
if (ui.chatSessionsFor !== dshSessionId) return void 0;
|
|
8381
|
+
let outputTokens = 0;
|
|
8382
|
+
let burstMs = 0;
|
|
8383
|
+
for (const row of ui.chatSessions) {
|
|
8384
|
+
const measured = ui.burstStats[row.sessionId] ?? row.burst;
|
|
8385
|
+
if (measured === void 0) continue;
|
|
8386
|
+
outputTokens += measured.outputTokens;
|
|
8387
|
+
burstMs += measured.burstMs;
|
|
8388
|
+
}
|
|
8389
|
+
return outputTokens > 0 && burstMs > 0 ? {
|
|
8390
|
+
outputTokens,
|
|
8391
|
+
burstMs
|
|
8392
|
+
} : void 0;
|
|
8393
|
+
}
|
|
8394
|
+
/**
|
|
8395
|
+
* Replace ONLY the decode half of the host's session stats.
|
|
8396
|
+
*
|
|
8397
|
+
* `decodeTokens` and `decodeMs` are the two fields the host's `decodeSpeed`
|
|
8398
|
+
* reads (`decodeTokens / (decodeMs / 1000)`), so writing the plugin's aggregate
|
|
8399
|
+
* there makes the host's own formatter, dialog row and aria label display the
|
|
8400
|
+
* plugin's burst rate. Every other field — turns, steps, LLM/tool/TTFT — stays
|
|
8401
|
+
* host-native. With no measurable burst the two decode fields are zeroed: the
|
|
8402
|
+
* host then hides the speed (`decodeMs > 0` guard) while keeping the counts.
|
|
8403
|
+
*/
|
|
8404
|
+
function patchedActivityStats(native, burst, acp) {
|
|
8405
|
+
if (!acp || native === void 0) return native;
|
|
8406
|
+
if (burst !== void 0 && burst.outputTokens > 0 && burst.burstMs > 0) return {
|
|
8407
|
+
...native,
|
|
8408
|
+
decodeTokens: burst.outputTokens,
|
|
8409
|
+
decodeMs: burst.burstMs
|
|
8410
|
+
};
|
|
8411
|
+
return {
|
|
8412
|
+
...native,
|
|
8413
|
+
decodeTokens: 0,
|
|
8414
|
+
decodeMs: 0
|
|
8415
|
+
};
|
|
8416
|
+
}
|
|
8417
|
+
function ActivityPillOccupant({ host: Host, ...props }) {
|
|
8418
|
+
const ui = useUi$2(props.acp);
|
|
8419
|
+
const dshSessionId = String(props.sessionId);
|
|
8420
|
+
const acpMode = ui.acpMode[dshSessionId] === true;
|
|
8421
|
+
const native = props.useProjection("sessionStats");
|
|
8422
|
+
const burst = acpMode ? activityBurst(ui, dshSessionId) : void 0;
|
|
8423
|
+
const stats = (0, react.useMemo)(() => patchedActivityStats(native, burst, acpMode), [
|
|
8424
|
+
native,
|
|
8425
|
+
burst?.outputTokens,
|
|
8426
|
+
burst?.burstMs,
|
|
8427
|
+
acpMode
|
|
8428
|
+
]);
|
|
8429
|
+
const projection = (0, react.useMemo)(() => (key, selector) => {
|
|
8430
|
+
if (key !== "sessionStats") return void 0;
|
|
8431
|
+
return selector === void 0 ? stats : selector(stats);
|
|
8432
|
+
}, [stats]);
|
|
8433
|
+
return (0, react.createElement)(Host, {
|
|
8434
|
+
useChat: props.useChat,
|
|
8435
|
+
useProjection: acpMode ? projection : props.useProjection,
|
|
8436
|
+
usePerformanceUsage: props.usePerformanceUsage,
|
|
8437
|
+
t: props.t
|
|
8438
|
+
});
|
|
8439
|
+
}
|
|
8440
|
+
function findHostActivity(slots) {
|
|
8441
|
+
try {
|
|
8442
|
+
if (slots.spec?.("conversation.composer.dock")?.kind !== "list") return void 0;
|
|
8443
|
+
return (slots.entriesOfSlot?.("conversation.composer.dock") ?? []).find((entry) => entry.options?.id === ACTIVITY_ENTRY_ID);
|
|
8444
|
+
} catch {
|
|
8445
|
+
return;
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
/**
|
|
8449
|
+
* Take the dock's `activity` cell for one ACP-mode spell.
|
|
8450
|
+
*
|
|
8451
|
+
* Returns `undefined` on any host shape this bundle cannot serve — an older
|
|
8452
|
+
* host without the `activity` entry (0.1.5 ships `stats`), a slot kind change,
|
|
8453
|
+
* a missing inject/locale, or no `performanceUsage` source. The native pill is
|
|
8454
|
+
* then left untouched; that is the honest failure mode, not a hidden control.
|
|
8455
|
+
*/
|
|
8456
|
+
function createActivityPillClaim(options) {
|
|
8457
|
+
const host = findHostActivity(options.slots);
|
|
8458
|
+
if (host === void 0 || host.component === void 0 || typeof host.inject !== "function" || typeof host.locale !== "string" || host.locale === "") {
|
|
8459
|
+
options.onDiag?.("activity pill: host entry not available");
|
|
8460
|
+
return;
|
|
8461
|
+
}
|
|
8462
|
+
let performanceUsage;
|
|
8463
|
+
try {
|
|
8464
|
+
performanceUsage = host.inject()?.hooks?.performanceUsage;
|
|
8465
|
+
} catch {
|
|
8466
|
+
performanceUsage = void 0;
|
|
8467
|
+
}
|
|
8468
|
+
if (performanceUsage === void 0) {
|
|
8469
|
+
options.onDiag?.("activity pill: host performance hook missing");
|
|
8470
|
+
return;
|
|
8471
|
+
}
|
|
8472
|
+
const Host = host.component;
|
|
8473
|
+
return createSeatClaim({
|
|
8474
|
+
slots: options.slots,
|
|
8475
|
+
slotKey: DOCK_SLOT,
|
|
8476
|
+
entryId: ACTIVITY_ENTRY_ID,
|
|
8477
|
+
order: host.options?.order ?? 0,
|
|
8478
|
+
locale: host.locale,
|
|
8479
|
+
basePriority: -1,
|
|
8480
|
+
occupant: () => (props) => (0, react.createElement)(ActivityPillOccupant, {
|
|
8481
|
+
...props,
|
|
8482
|
+
host: Host
|
|
8483
|
+
}),
|
|
8484
|
+
inject: (sessionId, actions) => ({
|
|
8485
|
+
...options.inject(sessionId, actions),
|
|
8486
|
+
hooks: { performanceUsage }
|
|
8487
|
+
}),
|
|
8488
|
+
label: "activity pill",
|
|
8489
|
+
onDiag: options.onDiag,
|
|
8490
|
+
maxClaims: options.maxClaims ?? 1
|
|
8491
|
+
});
|
|
8492
|
+
}
|
|
8493
|
+
//#endregion
|
|
7374
8494
|
//#region src/client/slash-isolation.ts
|
|
7375
8495
|
/**
|
|
7376
8496
|
* Cross-instance marker. `apply()` can run again over the same live sources (a
|
|
@@ -7386,8 +8506,8 @@ window.__ModuleLoader__.load({
|
|
|
7386
8506
|
* caller reads as "not ACP" (fail-open: an ordinary conversation must never
|
|
7387
8507
|
* lose its native commands to a guess).
|
|
7388
8508
|
*/
|
|
7389
|
-
function slashAcpVerdict(snapshot, sessionId, verdict) {
|
|
7390
|
-
const staged = stagedSessionOf(snapshot);
|
|
8509
|
+
function slashAcpVerdict(snapshot, sessionId, verdict, currentSessionId) {
|
|
8510
|
+
const staged = stagedSessionOf(snapshot, currentSessionId);
|
|
7391
8511
|
if (staged !== void 0 && staged.sessionId === sessionId) {
|
|
7392
8512
|
const listed = rowAgentPreset(staged.row);
|
|
7393
8513
|
if (listed !== void 0) return listed === "acp";
|
|
@@ -7732,6 +8852,37 @@ window.__ModuleLoader__.load({
|
|
|
7732
8852
|
const inject = ["slots", "connection"];
|
|
7733
8853
|
const browserPrefersZh = typeof navigator !== "undefined" && /^zh\b/i.test(navigator.language);
|
|
7734
8854
|
/**
|
|
8855
|
+
* The NTR scope marker (`html[data-ntr-acp]`): the ONLY hook the message-action
|
|
8856
|
+
* hiding CSS keys on, so the host's like / dislike / branch icons disappear in
|
|
8857
|
+
* exactly the conversations this gate has confirmed to run the ACP preset —
|
|
8858
|
+
* and reappear the moment the evidence stops answering (every `standDown`
|
|
8859
|
+
* clears it; non-ACP sessions never see it). Idempotent per call.
|
|
8860
|
+
*/
|
|
8861
|
+
const setScopeMarker = (active) => {
|
|
8862
|
+
if (typeof document === "undefined") return;
|
|
8863
|
+
document.documentElement.toggleAttribute("data-ntr-acp", active);
|
|
8864
|
+
};
|
|
8865
|
+
/**
|
|
8866
|
+
* One-shot sanity probe for the aria-label hiding: the host's action buttons
|
|
8867
|
+
* carry LOCALIZED labels and no data-* hook, so a copy change (or a third
|
|
8868
|
+
* language) silently un-hides them while everything else keeps working. Once
|
|
8869
|
+
* the marker is on and a turn tail is actually on screen, check that at least
|
|
8870
|
+
* one label the rules key on still exists and leave one console line for
|
|
8871
|
+
* whoever debugs "the icons came back" — dev-visible, never user UI.
|
|
8872
|
+
*/
|
|
8873
|
+
let probedHostLabels = false;
|
|
8874
|
+
const probeHostActionLabels = () => {
|
|
8875
|
+
if (probedHostLabels || typeof document === "undefined") return;
|
|
8876
|
+
if (document.querySelector("[data-turn-tail]") === null) return;
|
|
8877
|
+
probedHostLabels = true;
|
|
8878
|
+
if (document.querySelector([
|
|
8879
|
+
"button:not([data-ntr-branch])[aria-label=\"在新对话中分支\"]",
|
|
8880
|
+
"button:not([data-ntr-branch])[aria-label=\"Branch into a new conversation\"]",
|
|
8881
|
+
"button[aria-label=\"好的回答\"]",
|
|
8882
|
+
"button[aria-label=\"Good response\"]"
|
|
8883
|
+
].join(", ")) === null) console.warn("[dsh-ntr] none of the host message-action labels matched; the like/dislike/branch icons may not be hidden in NTR conversations");
|
|
8884
|
+
};
|
|
8885
|
+
/**
|
|
7735
8886
|
* How long a host verdict may stand as the gate's ONLY fact source before the
|
|
7736
8887
|
* gate asks the host again.
|
|
7737
8888
|
*
|
|
@@ -7807,11 +8958,31 @@ window.__ModuleLoader__.load({
|
|
|
7807
8958
|
return;
|
|
7808
8959
|
}
|
|
7809
8960
|
};
|
|
8961
|
+
/**
|
|
8962
|
+
* The runtime's `sessions.fork`, resolved per call: `undefined` renders the
|
|
8963
|
+
* branch button disabled (`hostUnavailable`) instead of hiding it, so a
|
|
8964
|
+
* host that gains the primitive later is picked up on the next render.
|
|
8965
|
+
*/
|
|
8966
|
+
const hostFork = () => {
|
|
8967
|
+
try {
|
|
8968
|
+
return resolveHostFork(service("sessions"));
|
|
8969
|
+
} catch {
|
|
8970
|
+
return;
|
|
8971
|
+
}
|
|
8972
|
+
};
|
|
8973
|
+
/** Focus a session in the workspace (the branch click's last step). */
|
|
8974
|
+
const openSession = (sessionId) => {
|
|
8975
|
+
try {
|
|
8976
|
+
service("uiWorkspace")?.openSession?.(sessionId);
|
|
8977
|
+
} catch {}
|
|
8978
|
+
};
|
|
7810
8979
|
/** The injected face every ACP conversation entry receives. */
|
|
7811
8980
|
const chatFace = (sessionId) => ({
|
|
7812
8981
|
acp: store,
|
|
7813
8982
|
sessionId: String(sessionId),
|
|
7814
|
-
faceFor
|
|
8983
|
+
faceFor,
|
|
8984
|
+
hostFork,
|
|
8985
|
+
openSession
|
|
7815
8986
|
});
|
|
7816
8987
|
ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
7817
8988
|
name: "settings.section",
|
|
@@ -7850,6 +9021,18 @@ window.__ModuleLoader__.load({
|
|
|
7850
9021
|
order: 10,
|
|
7851
9022
|
inject: (sessionId) => chatFace(sessionId)
|
|
7852
9023
|
}, PermissionDock)), "dsh-ntr: conversation permission dock");
|
|
9024
|
+
ctx.effect(() => ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
9025
|
+
name: "conversation.input.dock",
|
|
9026
|
+
id: "acp-released-banner",
|
|
9027
|
+
order: 20,
|
|
9028
|
+
inject: (sessionId) => chatFace(sessionId)
|
|
9029
|
+
}, ReleasedBanner)), "dsh-ntr: released-session banner");
|
|
9030
|
+
ctx.effect(() => ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
|
|
9031
|
+
name: "conversation.chat.assistant-actions",
|
|
9032
|
+
id: "ntr-branch",
|
|
9033
|
+
order: 20,
|
|
9034
|
+
inject: (sessionId) => chatFace(sessionId)
|
|
9035
|
+
}, AcpBranchAction)), "dsh-ntr: message branch action");
|
|
7853
9036
|
/**
|
|
7854
9037
|
* The client runtime's session list store, read structurally. Used to find
|
|
7855
9038
|
* the session the composer is staged on and whether it runs the ACP preset —
|
|
@@ -7861,6 +9044,28 @@ window.__ModuleLoader__.load({
|
|
|
7861
9044
|
return list;
|
|
7862
9045
|
};
|
|
7863
9046
|
/**
|
|
9047
|
+
* The renderer's current Session scope observable, when this host exposes it.
|
|
9048
|
+
* This is the source the host's own main-view picker prefers; the list's
|
|
9049
|
+
* retained rows are only the fallback for hosts without the adapter.
|
|
9050
|
+
*/
|
|
9051
|
+
const currentScopeSource = () => {
|
|
9052
|
+
try {
|
|
9053
|
+
const current = service("uiSession")?.adapter?.current;
|
|
9054
|
+
return current !== void 0 && typeof current.getSnapshot === "function" ? current : void 0;
|
|
9055
|
+
} catch {
|
|
9056
|
+
return;
|
|
9057
|
+
}
|
|
9058
|
+
};
|
|
9059
|
+
/** The session the composer is actually staged on, if the runtime can say. */
|
|
9060
|
+
const currentScopeSessionId = () => {
|
|
9061
|
+
try {
|
|
9062
|
+
const key = currentScopeSource()?.getSnapshot?.()?.key;
|
|
9063
|
+
return typeof key === "string" && key !== "" ? key : void 0;
|
|
9064
|
+
} catch {
|
|
9065
|
+
return;
|
|
9066
|
+
}
|
|
9067
|
+
};
|
|
9068
|
+
/**
|
|
7864
9069
|
* Chat session ids the ACP-mode tracker has already asked the host about (the
|
|
7865
9070
|
* `session/chat-list` fallback), mapped to WHEN the last ask went out. The
|
|
7866
9071
|
* ask is once per id so a host whose answer carries no `acpMode` cannot turn
|
|
@@ -7885,6 +9090,8 @@ window.__ModuleLoader__.load({
|
|
|
7885
9090
|
const acpModeVerdict = (sessionId) => store.getSnapshot().acpMode[sessionId];
|
|
7886
9091
|
ctx.effect(() => {
|
|
7887
9092
|
let stopWatch;
|
|
9093
|
+
/** Subscription to the renderer's current Session binding (see `currentScopeSource`). */
|
|
9094
|
+
let stopCurrent;
|
|
7888
9095
|
/**
|
|
7889
9096
|
* The claim on the host's model seat, held while the staged conversation
|
|
7890
9097
|
* runs the ACP preset. That cell is where the host's own base sits (the
|
|
@@ -7921,6 +9128,66 @@ window.__ModuleLoader__.load({
|
|
|
7921
9128
|
*/
|
|
7922
9129
|
let permissionClaim;
|
|
7923
9130
|
/**
|
|
9131
|
+
* Claim on the dock's `activity` cell (`conversation.composer.dock`, id
|
|
9132
|
+
* `activity`), held for the same ACP-mode spell as the two seats. The
|
|
9133
|
+
* occupant renders the HOST's ActivityPill with the plugin's burst speed
|
|
9134
|
+
* patched into its `sessionStats`; it is a list cell, so the claim is
|
|
9135
|
+
* identified by id rather than by the slot's heads[0]. If the host shape is
|
|
9136
|
+
* not one this bundle knows, no claim is created and the native pill stays.
|
|
9137
|
+
*/
|
|
9138
|
+
let activityClaim;
|
|
9139
|
+
let activityDiag = "idle";
|
|
9140
|
+
const noteActivityDiag = (line) => {
|
|
9141
|
+
if (line === activityDiag) return;
|
|
9142
|
+
activityDiag = line;
|
|
9143
|
+
if (!/failed|missing|taken|gave up|still held/.test(line)) return;
|
|
9144
|
+
console.warn(`[dsh-ntr] ${line}`);
|
|
9145
|
+
};
|
|
9146
|
+
/**
|
|
9147
|
+
* ACP-only auxiliary entries: the NTR view tab and the composer's context
|
|
9148
|
+
* ring. Unlike the two seats these are `list` slots with no visibility
|
|
9149
|
+
* predicate of their own, so "self-hide in the component" cannot remove a
|
|
9150
|
+
* tab from the header or free the native bottom row. They are installed by
|
|
9151
|
+
* this gate for exactly the same lifetime as the claims and disposed on
|
|
9152
|
+
* every stand-down — a standard conversation then has no NTR tab at all,
|
|
9153
|
+
* and the host's native context meter sits alone in its dock row.
|
|
9154
|
+
*/
|
|
9155
|
+
let viewEntry;
|
|
9156
|
+
let meterEntry;
|
|
9157
|
+
const releaseAuxEntries = () => {
|
|
9158
|
+
try {
|
|
9159
|
+
viewEntry?.();
|
|
9160
|
+
} catch {}
|
|
9161
|
+
viewEntry = void 0;
|
|
9162
|
+
try {
|
|
9163
|
+
meterEntry?.();
|
|
9164
|
+
} catch {}
|
|
9165
|
+
meterEntry = void 0;
|
|
9166
|
+
};
|
|
9167
|
+
const holdAuxEntries = () => {
|
|
9168
|
+
if (viewEntry === void 0) try {
|
|
9169
|
+
viewEntry = ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
9170
|
+
name: "conversation.view",
|
|
9171
|
+
id: "acp",
|
|
9172
|
+
order: 20,
|
|
9173
|
+
label: () => label("NTR", "NTR"),
|
|
9174
|
+
inject: (sessionId) => chatFace(sessionId)
|
|
9175
|
+
}, AcpConversationView));
|
|
9176
|
+
} catch {
|
|
9177
|
+
viewEntry = void 0;
|
|
9178
|
+
}
|
|
9179
|
+
if (meterEntry === void 0) try {
|
|
9180
|
+
meterEntry = ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
9181
|
+
name: "conversation.composer.dock",
|
|
9182
|
+
id: "acp-context-meter",
|
|
9183
|
+
order: 10,
|
|
9184
|
+
inject: (sessionId) => chatFace(sessionId)
|
|
9185
|
+
}, AcpContextMeter));
|
|
9186
|
+
} catch {
|
|
9187
|
+
meterEntry = void 0;
|
|
9188
|
+
}
|
|
9189
|
+
};
|
|
9190
|
+
/**
|
|
7924
9191
|
* Last diagnostic line from {@link syncAcpMode}. Every way this gate can
|
|
7925
9192
|
* end with the native seat still on screen used to be silent — an inner
|
|
7926
9193
|
* `catch { return [] }` and an outer `catch { … }` between them swallowed
|
|
@@ -7932,7 +9199,8 @@ window.__ModuleLoader__.load({
|
|
|
7932
9199
|
let seatDiag = "idle";
|
|
7933
9200
|
let permissionSeatDiag = "idle";
|
|
7934
9201
|
/**
|
|
7935
|
-
* Hand
|
|
9202
|
+
* Hand every ACP-only surface back: both composer cells, the NTR view tab
|
|
9203
|
+
* and the context ring.
|
|
7936
9204
|
*
|
|
7937
9205
|
* Every path that is not "this staged conversation is CONFIRMED to run the
|
|
7938
9206
|
* ACP preset" ends here — including the paths that carry no verdict at all
|
|
@@ -7949,6 +9217,8 @@ window.__ModuleLoader__.load({
|
|
|
7949
9217
|
seatDiag = reason;
|
|
7950
9218
|
permissionSeatDiag = `${reason} (released)`;
|
|
7951
9219
|
disarmVerdictReask();
|
|
9220
|
+
setScopeMarker(false);
|
|
9221
|
+
releaseAuxEntries();
|
|
7952
9222
|
if (claim !== void 0) {
|
|
7953
9223
|
claim.release();
|
|
7954
9224
|
claim = void 0;
|
|
@@ -7957,6 +9227,10 @@ window.__ModuleLoader__.load({
|
|
|
7957
9227
|
permissionClaim.release();
|
|
7958
9228
|
permissionClaim = void 0;
|
|
7959
9229
|
}
|
|
9230
|
+
if (activityClaim !== void 0) {
|
|
9231
|
+
activityClaim.release();
|
|
9232
|
+
activityClaim = void 0;
|
|
9233
|
+
}
|
|
7960
9234
|
};
|
|
7961
9235
|
/**
|
|
7962
9236
|
* Re-ask timer for the staged session, armed while the row is silent and a
|
|
@@ -7988,7 +9262,8 @@ window.__ModuleLoader__.load({
|
|
|
7988
9262
|
standDown("no session list service");
|
|
7989
9263
|
return;
|
|
7990
9264
|
}
|
|
7991
|
-
const
|
|
9265
|
+
const current = currentScopeSessionId();
|
|
9266
|
+
const staged = stagedSessionOf(snapshot, current);
|
|
7992
9267
|
if (staged === void 0) {
|
|
7993
9268
|
standDown("no staged session (nothing retains the main view)");
|
|
7994
9269
|
return;
|
|
@@ -8004,12 +9279,18 @@ window.__ModuleLoader__.load({
|
|
|
8004
9279
|
acpModeAsked.set(staged.sessionId, Date.now());
|
|
8005
9280
|
store.loadChatSessions(staged.sessionId, verdictHeld ? { settleVerdict: true } : void 0);
|
|
8006
9281
|
} else if (!verdictHeld && askedAt !== void 0) acpModeAsked.delete(staged.sessionId);
|
|
8007
|
-
if (!stagedIsAcpMode(snapshot, acpModeVerdict)) {
|
|
9282
|
+
if (!stagedIsAcpMode(snapshot, acpModeVerdict, current)) {
|
|
9283
|
+
if (rowAgentPreset(staged.row) !== void 0) store.noteAcpMode(staged.sessionId, false);
|
|
8008
9284
|
standDown(`staged ${staged.sessionId} is not in ACP mode`);
|
|
8009
9285
|
return;
|
|
8010
9286
|
}
|
|
8011
|
-
if (rowAgentPreset(staged.row) !== void 0)
|
|
9287
|
+
if (rowAgentPreset(staged.row) !== void 0) {
|
|
9288
|
+
store.noteAcpMode(staged.sessionId, rowAgentPreset(staged.row) === "acp");
|
|
9289
|
+
disarmVerdictReask();
|
|
9290
|
+
}
|
|
8012
9291
|
seatDiag = `staged ${staged.sessionId} is ACP mode`;
|
|
9292
|
+
setScopeMarker(true);
|
|
9293
|
+
probeHostActionLabels();
|
|
8013
9294
|
claim ??= createModelSeatClaim({
|
|
8014
9295
|
slots: ctx.slots,
|
|
8015
9296
|
occupant: () => AcpSessionControls,
|
|
@@ -8027,6 +9308,13 @@ window.__ModuleLoader__.load({
|
|
|
8027
9308
|
}
|
|
8028
9309
|
});
|
|
8029
9310
|
permissionClaim.sync();
|
|
9311
|
+
activityClaim ??= createActivityPillClaim({
|
|
9312
|
+
slots: ctx.slots,
|
|
9313
|
+
inject: (sessionId) => chatFace(sessionId),
|
|
9314
|
+
onDiag: noteActivityDiag
|
|
9315
|
+
});
|
|
9316
|
+
activityClaim?.sync();
|
|
9317
|
+
holdAuxEntries();
|
|
8030
9318
|
if (rowAgentPreset(staged.row) === void 0) armVerdictReask();
|
|
8031
9319
|
};
|
|
8032
9320
|
let stopPreset;
|
|
@@ -8048,6 +9336,12 @@ window.__ModuleLoader__.load({
|
|
|
8048
9336
|
const attach = () => {
|
|
8049
9337
|
const list = sessionsList();
|
|
8050
9338
|
if (list !== void 0 && stopWatch === void 0) stopWatch = list.subscribe(runGate);
|
|
9339
|
+
const current = currentScopeSource();
|
|
9340
|
+
if (current?.subscribe !== void 0 && stopCurrent === void 0) try {
|
|
9341
|
+
stopCurrent = current.subscribe(runGate);
|
|
9342
|
+
} catch {
|
|
9343
|
+
stopCurrent = void 0;
|
|
9344
|
+
}
|
|
8051
9345
|
attachPresetFrames();
|
|
8052
9346
|
runGate();
|
|
8053
9347
|
};
|
|
@@ -8090,7 +9384,7 @@ window.__ModuleLoader__.load({
|
|
|
8090
9384
|
attach();
|
|
8091
9385
|
const stopStore = store.subscribe(runGate);
|
|
8092
9386
|
const stopCell = ctx.on("slots/changed", (...args) => {
|
|
8093
|
-
if (args[0] !== "conversation.input.model" && args[0] !== "conversation.input.permission") return;
|
|
9387
|
+
if (args[0] !== "conversation.input.model" && args[0] !== "conversation.input.permission" && args[0] !== "conversation.composer.dock") return;
|
|
8094
9388
|
runGate();
|
|
8095
9389
|
});
|
|
8096
9390
|
const stopService = ctx.on("internal/service", attach);
|
|
@@ -8100,7 +9394,10 @@ window.__ModuleLoader__.load({
|
|
|
8100
9394
|
stopCell();
|
|
8101
9395
|
stopPreset?.();
|
|
8102
9396
|
stopWatch?.();
|
|
9397
|
+
stopCurrent?.();
|
|
8103
9398
|
disarmVerdictReask();
|
|
9399
|
+
setScopeMarker(false);
|
|
9400
|
+
releaseAuxEntries();
|
|
8104
9401
|
if (claim !== void 0) {
|
|
8105
9402
|
claim.release();
|
|
8106
9403
|
claim = void 0;
|
|
@@ -8109,6 +9406,10 @@ window.__ModuleLoader__.load({
|
|
|
8109
9406
|
permissionClaim.release();
|
|
8110
9407
|
permissionClaim = void 0;
|
|
8111
9408
|
}
|
|
9409
|
+
if (activityClaim !== void 0) {
|
|
9410
|
+
activityClaim.release();
|
|
9411
|
+
activityClaim = void 0;
|
|
9412
|
+
}
|
|
8112
9413
|
store.noteSeatDiag("gate disposed");
|
|
8113
9414
|
store.notePermissionSeatDiag("gate disposed");
|
|
8114
9415
|
};
|
|
@@ -8139,9 +9440,9 @@ window.__ModuleLoader__.load({
|
|
|
8139
9440
|
const sources = triggers.live?.sources;
|
|
8140
9441
|
return Array.isArray(sources) ? sources : void 0;
|
|
8141
9442
|
},
|
|
8142
|
-
verdict: (sessionId) => slashAcpVerdict(sessionsList()?.getSnapshot(), sessionId, (id) => store.getSnapshot().acpMode[id]),
|
|
9443
|
+
verdict: (sessionId) => slashAcpVerdict(sessionsList()?.getSnapshot(), sessionId, (id) => store.getSnapshot().acpMode[id], currentScopeSessionId()),
|
|
8143
9444
|
ensureVerdict: async (sessionId) => {
|
|
8144
|
-
const held = () => slashAcpVerdict(sessionsList()?.getSnapshot(), sessionId, (id) => store.getSnapshot().acpMode[id]);
|
|
9445
|
+
const held = () => slashAcpVerdict(sessionsList()?.getSnapshot(), sessionId, (id) => store.getSnapshot().acpMode[id], currentScopeSessionId());
|
|
8145
9446
|
const known = held();
|
|
8146
9447
|
if (known !== void 0) return known;
|
|
8147
9448
|
await store.loadChatSessions(sessionId);
|
|
@@ -8204,13 +9505,6 @@ window.__ModuleLoader__.load({
|
|
|
8204
9505
|
store.noteSlashDiag("idle");
|
|
8205
9506
|
};
|
|
8206
9507
|
}, "dsh-ntr: slash isolation");
|
|
8207
|
-
ctx.effect(() => ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
8208
|
-
name: "conversation.view",
|
|
8209
|
-
id: "acp",
|
|
8210
|
-
order: 20,
|
|
8211
|
-
label: () => "NTR",
|
|
8212
|
-
inject: (sessionId) => chatFace(sessionId)
|
|
8213
|
-
}, AcpConversationView)), "dsh-ntr: ACP view tab");
|
|
8214
9508
|
}
|
|
8215
9509
|
//#endregion
|
|
8216
9510
|
exports.apply = apply;
|