dsh-ntr 0.3.0 → 0.3.2
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 +1 -1
- package/README.md +1 -1
- package/cordis.patch.yml +17 -1
- package/lib/bridge.js +614 -25
- package/lib/client/acp-ui.js +103 -3
- package/lib/client/acp-view.js +24 -4
- package/lib/client/branch-action.js +133 -0
- package/lib/client/composer-mode.js +48 -13
- package/lib/client/context-meter.js +86 -0
- package/lib/client/index.js +238 -15
- package/lib/client/panel.js +127 -30
- 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 +403 -16
- package/lib/client.js +1267 -70
- package/lib/client.js.map +1 -1
- package/lib/impl.js +54 -1
- package/lib/index.js +7 -0
- package/lib/protocol.js +19 -0
- package/lib/types/bridge.d.ts +230 -7
- package/lib/types/client/acp-ui.d.ts +34 -2
- package/lib/types/client/acp-view.d.ts +13 -2
- package/lib/types/client/branch-action.d.ts +40 -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/released-banner.d.ts +34 -0
- package/lib/types/client/slash-isolation.d.ts +1 -1
- package/lib/types/client/store.d.ts +131 -1
- package/lib/types/impl.d.ts +41 -1
- package/lib/types/protocol.d.ts +130 -0
- package/lib/types/update.d.ts +7 -4
- package/lib/update.js +25 -10
- package/package.json +2 -1
- package/src/bridge.ts +645 -27
- package/src/client/acp-ui.tsx +123 -2
- package/src/client/acp-view.tsx +30 -5
- package/src/client/branch-action.tsx +160 -0
- package/src/client/composer-mode.tsx +59 -13
- package/src/client/context-meter.tsx +128 -0
- package/src/client/index.tsx +265 -14
- package/src/client/panel.tsx +229 -41
- 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 +463 -20
- package/src/impl.ts +84 -2
- package/src/index.ts +8 -0
- package/src/protocol.ts +129 -0
- package/src/update.ts +21 -8
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.
|
|
@@ -322,6 +392,8 @@ window.__ModuleLoader__.load({
|
|
|
322
392
|
blocks: [],
|
|
323
393
|
permissions: [],
|
|
324
394
|
busy: false,
|
|
395
|
+
logs: [],
|
|
396
|
+
logUnread: 0,
|
|
325
397
|
detectedAgents: [...DEFAULT_KNOWN_PRESETS],
|
|
326
398
|
detecting: false,
|
|
327
399
|
modelCatalogs: {},
|
|
@@ -331,6 +403,7 @@ window.__ModuleLoader__.load({
|
|
|
331
403
|
chatSessions: [],
|
|
332
404
|
tagHalo: { ...DEFAULT_TAG_HALO },
|
|
333
405
|
approvalDeadlines: void 0,
|
|
406
|
+
idleRelease: void 0,
|
|
334
407
|
chatTranscripts: {},
|
|
335
408
|
acpMode: {},
|
|
336
409
|
chatBoundAgent: {},
|
|
@@ -343,12 +416,21 @@ window.__ModuleLoader__.load({
|
|
|
343
416
|
};
|
|
344
417
|
listeners = /* @__PURE__ */ new Set();
|
|
345
418
|
source;
|
|
419
|
+
/** Monotonic id source for {@link AcpLogEntry}s (never reset by `clearLogs`). */
|
|
420
|
+
logSeq = 0;
|
|
346
421
|
lastSeq = /* @__PURE__ */ new Map();
|
|
347
422
|
/** Sessions with a finalized turn (dedups the cancel-fallback + late real turn/end pair). */
|
|
348
423
|
openTurns = /* @__PURE__ */ new Set();
|
|
349
424
|
/** In-flight `session/chat-list` reads, keyed by chat session id. */
|
|
350
425
|
chatSessionReads = /* @__PURE__ */ new Map();
|
|
351
426
|
/**
|
|
427
|
+
* ACP session ids whose chat-list row was requested mid-turn (one read each).
|
|
428
|
+
* A conversation's first ACP session does not exist when the composer's
|
|
429
|
+
* mount/running read fires, so its first `usage_update` would find no row to
|
|
430
|
+
* patch; `turn/start` asks again once the host has materialized the session.
|
|
431
|
+
*/
|
|
432
|
+
chatSessionRowReads = /* @__PURE__ */ new Set();
|
|
433
|
+
/**
|
|
352
434
|
* Chat sessions whose list has answered at least once in this page. The
|
|
353
435
|
* FIRST answer only seeds the lock; a lock appearing on a later answer is a
|
|
354
436
|
* transition this page actually witnessed, and only that one is announced via
|
|
@@ -429,6 +511,23 @@ window.__ModuleLoader__.load({
|
|
|
429
511
|
for (const listener of [...this.listeners]) listener();
|
|
430
512
|
}
|
|
431
513
|
/**
|
|
514
|
+
* Build the log patch for one line. Does NOT mutate: the caller merges it
|
|
515
|
+
* into the single `mutate` call it was already making for the same event.
|
|
516
|
+
*/
|
|
517
|
+
logPatch(level, text, agentId) {
|
|
518
|
+
const entry = {
|
|
519
|
+
id: ++this.logSeq,
|
|
520
|
+
at: Date.now(),
|
|
521
|
+
level,
|
|
522
|
+
text,
|
|
523
|
+
...agentId === void 0 ? {} : { agentId }
|
|
524
|
+
};
|
|
525
|
+
return {
|
|
526
|
+
logs: [...this.state.logs, entry].slice(-200),
|
|
527
|
+
logUnread: this.state.logUnread + 1
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
432
531
|
* Live `session/update` frames waiting to be folded. Agents stream at token
|
|
433
532
|
* granularity — hundreds of frames a second with usage ticks interleaved —
|
|
434
533
|
* and folding plus notifying listeners per frame re-rendered every store
|
|
@@ -450,17 +549,25 @@ window.__ModuleLoader__.load({
|
|
|
450
549
|
else list.push(item);
|
|
451
550
|
}
|
|
452
551
|
let chatTranscripts = this.state.chatTranscripts;
|
|
552
|
+
let chatSessions = this.state.chatSessions;
|
|
453
553
|
let blocksChanged = false;
|
|
454
554
|
for (const [sessionId, items] of bySession) {
|
|
455
555
|
const last = items[items.length - 1];
|
|
556
|
+
const usage = usageOfBatch(items);
|
|
557
|
+
if (usage !== void 0) chatSessions = withSessionUsage(chatSessions, sessionId, usage);
|
|
456
558
|
const record = chatTranscripts[sessionId];
|
|
457
559
|
if (record !== void 0) {
|
|
458
560
|
let changed = false;
|
|
459
561
|
for (const item of items) if (foldUpdate(record.blocks, item.update)) changed = true;
|
|
562
|
+
const session = usage !== void 0 && !sameUsage(record.session.usage, usage) ? {
|
|
563
|
+
...record.session,
|
|
564
|
+
usage
|
|
565
|
+
} : record.session;
|
|
460
566
|
chatTranscripts = {
|
|
461
567
|
...chatTranscripts,
|
|
462
568
|
[sessionId]: {
|
|
463
569
|
...record,
|
|
570
|
+
session,
|
|
464
571
|
seq: last.seq,
|
|
465
572
|
...changed ? { blocks: [...record.blocks] } : {}
|
|
466
573
|
}
|
|
@@ -481,6 +588,7 @@ window.__ModuleLoader__.load({
|
|
|
481
588
|
}
|
|
482
589
|
const patch = {};
|
|
483
590
|
if (chatTranscripts !== this.state.chatTranscripts) patch.chatTranscripts = chatTranscripts;
|
|
591
|
+
if (chatSessions !== this.state.chatSessions) patch.chatSessions = chatSessions;
|
|
484
592
|
if (blocksChanged) patch.blocks = [...this.state.blocks];
|
|
485
593
|
if (Object.keys(patch).length > 0) this.mutate(patch);
|
|
486
594
|
}
|
|
@@ -529,7 +637,7 @@ window.__ModuleLoader__.load({
|
|
|
529
637
|
/** Snapshot refresh used on panel open and after a stream outage. */
|
|
530
638
|
async resync() {
|
|
531
639
|
try {
|
|
532
|
-
const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult, versionResult] = await Promise.all([
|
|
640
|
+
const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult, idleReleaseResult, versionResult] = await Promise.all([
|
|
533
641
|
this.call("agents/list"),
|
|
534
642
|
this.call("sessions/list"),
|
|
535
643
|
this.call("default/agent").catch(() => ({})),
|
|
@@ -537,6 +645,7 @@ window.__ModuleLoader__.load({
|
|
|
537
645
|
this.call("preset/status").catch(() => null),
|
|
538
646
|
this.call("ui/tag-halo").catch(() => ({ halo: { ...DEFAULT_TAG_HALO } })),
|
|
539
647
|
this.call("ui/approval-deadlines").catch(() => ({ deadlines: void 0 })),
|
|
648
|
+
this.call("ui/idle-release").catch(() => ({ idleRelease: void 0 })),
|
|
540
649
|
this.call("plugin/version").catch(() => null)
|
|
541
650
|
]);
|
|
542
651
|
const detectedAgents = detectResult && detectResult.agents.length > 0 ? detectResult.agents : this.state.detectedAgents.length > 0 ? this.state.detectedAgents : [...DEFAULT_KNOWN_PRESETS];
|
|
@@ -549,8 +658,10 @@ window.__ModuleLoader__.load({
|
|
|
549
658
|
preset: presetResult?.preset ?? void 0,
|
|
550
659
|
tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
|
|
551
660
|
approvalDeadlines: deadlinesResult.deadlines,
|
|
661
|
+
idleRelease: idleReleaseResult.idleRelease,
|
|
552
662
|
pluginVersion: versionResult?.version !== void 0 && versionResult.version !== "" ? versionResult.version : void 0
|
|
553
663
|
});
|
|
664
|
+
if (this.state.chatSessionsFor !== void 0) this.loadChatSessions(this.state.chatSessionsFor);
|
|
554
665
|
const active = this.state.activeSessionId ?? sessions[0]?.sessionId;
|
|
555
666
|
if (active !== void 0) await this.loadTranscript(active);
|
|
556
667
|
} catch (error) {
|
|
@@ -974,7 +1085,7 @@ window.__ModuleLoader__.load({
|
|
|
974
1085
|
async readChatSessionList(dshSessionId, options, read) {
|
|
975
1086
|
const settle = options?.settleVerdict === true || !this.acpModeLive.has(dshSessionId);
|
|
976
1087
|
try {
|
|
977
|
-
const { sessions, acpMode, boundAgentId, lockedAgentId, choice } = await this.call("session/chat-list", {
|
|
1088
|
+
const { sessions = [], acpMode, boundAgentId, lockedAgentId, choice } = await this.call("session/chat-list", {
|
|
978
1089
|
dshSessionId,
|
|
979
1090
|
refresh: true
|
|
980
1091
|
});
|
|
@@ -1082,6 +1193,13 @@ window.__ModuleLoader__.load({
|
|
|
1082
1193
|
* being the one authorized exception. Same no-op-when-unchanged discipline as
|
|
1083
1194
|
* the other accessors, so a frame restating the held verdict costs no
|
|
1084
1195
|
* listener churn.
|
|
1196
|
+
*
|
|
1197
|
+
* The seat gate (`syncAcpMode`) is the SECOND caller: it folds the runtime
|
|
1198
|
+
* row's own `agentPreset` — the same session-log source the frames announce —
|
|
1199
|
+
* through here, in both directions. That hands the row the frame's authority
|
|
1200
|
+
* on purpose (one provenance, one rank): a stale projection is corrected by
|
|
1201
|
+
* the gate's next evaluation with fresher row data, and only HOST READS are
|
|
1202
|
+
* barred from regressing what the runtime itself already said.
|
|
1085
1203
|
*/
|
|
1086
1204
|
noteAcpMode(dshSessionId, acpMode) {
|
|
1087
1205
|
this.acpModeLive.add(dshSessionId);
|
|
@@ -1165,12 +1283,99 @@ window.__ModuleLoader__.load({
|
|
|
1165
1283
|
modeId,
|
|
1166
1284
|
...agentId === void 0 ? {} : { agentId }
|
|
1167
1285
|
});
|
|
1286
|
+
const safeAgent = agentId && agentId.trim() || void 0;
|
|
1287
|
+
this.writeChoice(dshSessionId, (choice) => ({
|
|
1288
|
+
...choice,
|
|
1289
|
+
mode: modeId
|
|
1290
|
+
}), safeAgent);
|
|
1291
|
+
if (this.state.chatSessionsFor === dshSessionId) {
|
|
1292
|
+
const owner = safeAgent ?? this.state.chatChoice[dshSessionId]?.agentId ?? this.state.chatBoundAgent[dshSessionId] ?? this.state.defaultAgent;
|
|
1293
|
+
let changed = false;
|
|
1294
|
+
const chatSessions = this.state.chatSessions.map((s) => {
|
|
1295
|
+
if (s.agentId !== owner || !s.modes || s.modes.currentModeId === modeId) return s;
|
|
1296
|
+
changed = true;
|
|
1297
|
+
return {
|
|
1298
|
+
...s,
|
|
1299
|
+
modes: {
|
|
1300
|
+
...s.modes,
|
|
1301
|
+
currentModeId: modeId
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
});
|
|
1305
|
+
if (changed) this.mutate({ chatSessions });
|
|
1306
|
+
}
|
|
1168
1307
|
} catch (error) {
|
|
1169
1308
|
this.flashError(error);
|
|
1170
1309
|
}
|
|
1171
1310
|
await this.loadChatSessions(dshSessionId);
|
|
1172
1311
|
}
|
|
1173
1312
|
/**
|
|
1313
|
+
* Bring the conversation's released ACP session back on a fresh process —
|
|
1314
|
+
* the released banner's explicit form of the "next message resumes it"
|
|
1315
|
+
* promise (`session/chat-resume`). A failure is reported through the notice
|
|
1316
|
+
* AND rethrown (the `respondPermission` contract), so the banner can pin
|
|
1317
|
+
* the message beside the button the user just pressed; the binding is
|
|
1318
|
+
* re-read either way so the row follows the host's real answer.
|
|
1319
|
+
*/
|
|
1320
|
+
async resumeChatSession(dshSessionId) {
|
|
1321
|
+
let failure;
|
|
1322
|
+
try {
|
|
1323
|
+
await this.call("session/chat-resume", { dshSessionId });
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
this.flashError(error);
|
|
1326
|
+
failure = error;
|
|
1327
|
+
}
|
|
1328
|
+
await this.loadChatSessions(dshSessionId);
|
|
1329
|
+
if (failure !== void 0) throw failure;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Branch one conversation (the NTR branch button's click): the fixed
|
|
1333
|
+
* two-stage order — the HOST forks the DSH session tree first (copied
|
|
1334
|
+
* history, `(1)` title, workspace), then the plugin adopts the ACP side via
|
|
1335
|
+
* `session/chat-fork-adopt`, which runs the agent's own `session/fork` so
|
|
1336
|
+
* the child actually remembers the parent context. Host first is structural:
|
|
1337
|
+
* ACP fork has no cut-point parameter and nothing to return to if the host
|
|
1338
|
+
* half failed second (see the plan's §D7).
|
|
1339
|
+
*
|
|
1340
|
+
* Resolves `{ childId }` on success, or `{ failure }` with one actionable
|
|
1341
|
+
* line — the reason is ALSO flashed into the notice banner (a user-action
|
|
1342
|
+
* failure, so the banner is its surface), and the local line is what the
|
|
1343
|
+
* conversation renders beside the button (the user is not on the settings
|
|
1344
|
+
* page). A failed adopt leaves the child an ordinary copied-history
|
|
1345
|
+
* conversation (its first message starts a fresh `session/new` — today's
|
|
1346
|
+
* behavior), never a half-bound one, and NOTHING is opened.
|
|
1347
|
+
*
|
|
1348
|
+
* `forkHost` is the runtime's `sessions.fork` face, resolved structurally by
|
|
1349
|
+
* the entry and injected here: the store stays transport-free.
|
|
1350
|
+
*/
|
|
1351
|
+
async forkChatSession(dshSessionId, forkHost) {
|
|
1352
|
+
let childId;
|
|
1353
|
+
try {
|
|
1354
|
+
childId = await forkHost(dshSessionId);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
const failure = error instanceof Error ? error.message : String(error);
|
|
1357
|
+
this.flashError(failure);
|
|
1358
|
+
return { failure };
|
|
1359
|
+
}
|
|
1360
|
+
if (typeof childId !== "string" || childId === "") {
|
|
1361
|
+
const failure = this.locale("宿主没有返回分支会话,分支未完成。", "The host returned no branched session; the branch did not complete.");
|
|
1362
|
+
this.flashError(new Error(failure));
|
|
1363
|
+
return { failure };
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
await this.call("session/chat-fork-adopt", {
|
|
1367
|
+
dshSessionId: childId,
|
|
1368
|
+
parentDshSessionId: dshSessionId
|
|
1369
|
+
});
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1372
|
+
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.`);
|
|
1373
|
+
this.flashError(new Error(failure));
|
|
1374
|
+
return { failure };
|
|
1375
|
+
}
|
|
1376
|
+
return { childId };
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1174
1379
|
* Start (or refresh) observation of every ACP transcript behind one DSH chat
|
|
1175
1380
|
* session. Blocks are rebuilt from scratch — the host buffer is the record,
|
|
1176
1381
|
* and this view is the only consumer that needs the raw agent stream.
|
|
@@ -1337,6 +1542,33 @@ window.__ModuleLoader__.load({
|
|
|
1337
1542
|
async resetApprovalDeadlines() {
|
|
1338
1543
|
await this.writeApprovalDeadlines({ reset: true });
|
|
1339
1544
|
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Write the idle-release window (in ms).
|
|
1547
|
+
*
|
|
1548
|
+
* The host answer — the same route returns the window now in force — is
|
|
1549
|
+
* the authority, so an optimistic write that the host trims self-corrects
|
|
1550
|
+
* instead of drifting from what runs.
|
|
1551
|
+
*/
|
|
1552
|
+
async setIdleRelease(idleTimeoutMs) {
|
|
1553
|
+
await this.writeIdleRelease({ idleTimeoutMs });
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Drop the persisted override: the config file (or the built-in default)
|
|
1557
|
+
* decides again.
|
|
1558
|
+
*/
|
|
1559
|
+
async resetIdleRelease() {
|
|
1560
|
+
await this.writeIdleRelease({ reset: true });
|
|
1561
|
+
}
|
|
1562
|
+
async writeIdleRelease(payload) {
|
|
1563
|
+
const previous = this.state.idleRelease;
|
|
1564
|
+
try {
|
|
1565
|
+
const result = await this.call("ui/idle-release-set", payload);
|
|
1566
|
+
this.mutate({ idleRelease: result.idleRelease });
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
this.mutate({ idleRelease: previous });
|
|
1569
|
+
this.flashError(error);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1340
1572
|
async writeApprovalDeadlines(payload) {
|
|
1341
1573
|
const previous = this.state.approvalDeadlines;
|
|
1342
1574
|
if (previous !== void 0) this.mutate({ approvalDeadlines: {
|
|
@@ -1383,10 +1615,24 @@ window.__ModuleLoader__.load({
|
|
|
1383
1615
|
dshSessionId,
|
|
1384
1616
|
modelId
|
|
1385
1617
|
});
|
|
1618
|
+
const targetAgent = (agentId && agentId.trim() || void 0) ?? (modelId ? splitModelAgentId(modelId) : void 0);
|
|
1386
1619
|
this.writeChoice(dshSessionId, (choice) => ({
|
|
1387
1620
|
...choice,
|
|
1388
1621
|
modelId
|
|
1389
|
-
}),
|
|
1622
|
+
}), targetAgent);
|
|
1623
|
+
if (this.state.chatSessionsFor === dshSessionId) {
|
|
1624
|
+
const owner = targetAgent ?? this.state.chatChoice[dshSessionId]?.agentId ?? this.state.chatBoundAgent[dshSessionId] ?? this.state.defaultAgent;
|
|
1625
|
+
let changed = false;
|
|
1626
|
+
const chatSessions = this.state.chatSessions.map((s) => {
|
|
1627
|
+
if (s.agentId !== owner || s.modelId === modelId) return s;
|
|
1628
|
+
changed = true;
|
|
1629
|
+
return {
|
|
1630
|
+
...s,
|
|
1631
|
+
modelId
|
|
1632
|
+
};
|
|
1633
|
+
});
|
|
1634
|
+
if (changed) this.mutate({ chatSessions });
|
|
1635
|
+
}
|
|
1390
1636
|
} catch (error) {
|
|
1391
1637
|
this.flashError(error);
|
|
1392
1638
|
throw error;
|
|
@@ -1400,13 +1646,27 @@ window.__ModuleLoader__.load({
|
|
|
1400
1646
|
...effort === void 0 ? {} : { effort },
|
|
1401
1647
|
...agentId === void 0 ? {} : { agentId }
|
|
1402
1648
|
});
|
|
1649
|
+
const targetAgent = agentId && agentId.trim() || void 0;
|
|
1403
1650
|
this.writeChoice(dshSessionId, (choice) => {
|
|
1404
1651
|
const { effort: _dropped, ...rest } = choice;
|
|
1405
1652
|
return effort === void 0 ? rest : {
|
|
1406
1653
|
...rest,
|
|
1407
1654
|
effort
|
|
1408
1655
|
};
|
|
1409
|
-
},
|
|
1656
|
+
}, targetAgent);
|
|
1657
|
+
if (this.state.chatSessionsFor === dshSessionId) {
|
|
1658
|
+
const owner = targetAgent ?? this.state.chatChoice[dshSessionId]?.agentId ?? this.state.chatBoundAgent[dshSessionId] ?? this.state.defaultAgent;
|
|
1659
|
+
let changed = false;
|
|
1660
|
+
const chatSessions = this.state.chatSessions.map((s) => {
|
|
1661
|
+
if (s.agentId !== owner || s.reasoningEffort === effort) return s;
|
|
1662
|
+
changed = true;
|
|
1663
|
+
return {
|
|
1664
|
+
...s,
|
|
1665
|
+
reasoningEffort: effort
|
|
1666
|
+
};
|
|
1667
|
+
});
|
|
1668
|
+
if (changed) this.mutate({ chatSessions });
|
|
1669
|
+
}
|
|
1410
1670
|
} catch (error) {
|
|
1411
1671
|
this.flashError(error);
|
|
1412
1672
|
throw error;
|
|
@@ -1479,13 +1739,25 @@ window.__ModuleLoader__.load({
|
|
|
1479
1739
|
clearNotice() {
|
|
1480
1740
|
this.mutate({ notice: void 0 });
|
|
1481
1741
|
}
|
|
1742
|
+
/** Drop every retained runtime-log line (the unread counter restarts too). */
|
|
1743
|
+
clearLogs() {
|
|
1744
|
+
this.mutate({
|
|
1745
|
+
logs: [],
|
|
1746
|
+
logUnread: 0
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
/** The user looked at the log: clear the unread badge. */
|
|
1750
|
+
markLogsRead() {
|
|
1751
|
+
if (this.state.logUnread === 0) return;
|
|
1752
|
+
this.mutate({ logUnread: 0 });
|
|
1753
|
+
}
|
|
1482
1754
|
flashError(error) {
|
|
1483
1755
|
this.mutate({ notice: error instanceof Error ? error.message : String(error) });
|
|
1484
1756
|
}
|
|
1485
1757
|
applyEvent(event) {
|
|
1486
1758
|
switch (event.type) {
|
|
1487
1759
|
case "agent/notice":
|
|
1488
|
-
this.mutate(
|
|
1760
|
+
this.mutate(this.logPatch(event.level, event.text, event.agentId));
|
|
1489
1761
|
return Promise.resolve();
|
|
1490
1762
|
case "agent/status":
|
|
1491
1763
|
if (event.agent !== void 0) {
|
|
@@ -1503,13 +1775,17 @@ window.__ModuleLoader__.load({
|
|
|
1503
1775
|
}
|
|
1504
1776
|
case "session/meta": {
|
|
1505
1777
|
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;
|
|
1778
|
+
let chatSessions = withSessionUsage(this.state.chatSessions, event.sessionId, event.session.usage);
|
|
1779
|
+
chatSessions = withSessionModes(chatSessions, event.sessionId, event.session.modes);
|
|
1506
1780
|
this.mutate({
|
|
1507
1781
|
sessions,
|
|
1782
|
+
...chatSessions === this.state.chatSessions ? {} : { chatSessions },
|
|
1508
1783
|
...this.watchedSessionPatch(event.sessionId, event.session)
|
|
1509
1784
|
});
|
|
1510
1785
|
return Promise.resolve();
|
|
1511
1786
|
}
|
|
1512
1787
|
case "session/closed": {
|
|
1788
|
+
const closingSession = this.state.sessions.find((s) => s.sessionId === event.sessionId);
|
|
1513
1789
|
const sessions = this.state.sessions.filter((s) => s.sessionId !== event.sessionId);
|
|
1514
1790
|
const active = this.state.activeSessionId === event.sessionId ? sessions[0]?.sessionId : this.state.activeSessionId;
|
|
1515
1791
|
this.openTurns.delete(event.sessionId);
|
|
@@ -1522,23 +1798,26 @@ window.__ModuleLoader__.load({
|
|
|
1522
1798
|
chatTranscripts: closedTranscripts,
|
|
1523
1799
|
...chatSessions.length !== this.state.chatSessions.length ? { chatSessions } : {},
|
|
1524
1800
|
permissions: this.state.permissions.filter((p) => p.sessionId !== event.sessionId),
|
|
1525
|
-
...event.reason === "agent-died" ?
|
|
1801
|
+
...event.reason === "agent-died" ? this.logPatch("error", this.locale("会话因 agent 进程退出而终止", "session ended: agent process died"), closingSession?.agentId) : {}
|
|
1526
1802
|
});
|
|
1527
1803
|
return Promise.resolve();
|
|
1528
1804
|
}
|
|
1529
1805
|
case "session/released": {
|
|
1530
1806
|
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];
|
|
1531
1807
|
this.openTurns.delete(event.sessionId);
|
|
1532
|
-
const chatSessions = this.state.chatSessions
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1808
|
+
const chatSessions = withSessionReleased(this.state.chatSessions, event.sessionId, true);
|
|
1809
|
+
const cause = event.cause ?? "died";
|
|
1810
|
+
let log = {};
|
|
1811
|
+
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);
|
|
1812
|
+
else if (cause === "resume-failed") log = this.logPatch("error", this.locale("会话恢复失败;再次发送消息将重试恢复。", "session resume failed; your next message will retry it."), event.session.agentId);
|
|
1813
|
+
else if (cause === "stalled") log = this.logPatch("error", this.locale("agent 未响应卡死的回合,会话已挂起;再次发送消息将自动恢复。", "agent did not acknowledge the stalled turn; the session is paused and will resume on your next message."), event.session.agentId);
|
|
1814
|
+
else if (cause === "died") log = this.logPatch("error", this.locale("agent 进程已退出,会话已挂起;再次发送消息将自动恢复。", "agent process died; the session is paused and will resume on your next message."), event.session.agentId);
|
|
1536
1815
|
this.mutate({
|
|
1537
1816
|
sessions,
|
|
1538
1817
|
...chatSessions !== this.state.chatSessions ? { chatSessions } : {},
|
|
1539
1818
|
permissions: this.state.permissions.filter((p) => p.sessionId !== event.sessionId),
|
|
1540
1819
|
...this.watchedSessionPatch(event.sessionId, event.session),
|
|
1541
|
-
|
|
1820
|
+
...log
|
|
1542
1821
|
});
|
|
1543
1822
|
return Promise.resolve();
|
|
1544
1823
|
}
|
|
@@ -1548,8 +1827,10 @@ window.__ModuleLoader__.load({
|
|
|
1548
1827
|
const isActive = event.sessionId === this.state.activeSessionId;
|
|
1549
1828
|
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];
|
|
1550
1829
|
const watched = this.state.chatTranscripts[event.sessionId];
|
|
1830
|
+
const chatSessions = withSessionReleased(this.state.chatSessions, event.sessionId, false);
|
|
1551
1831
|
this.mutate({
|
|
1552
1832
|
sessions,
|
|
1833
|
+
...chatSessions === this.state.chatSessions ? {} : { chatSessions },
|
|
1553
1834
|
...isActive ? { blocks: [] } : {},
|
|
1554
1835
|
...watched === void 0 ? {} : { chatTranscripts: {
|
|
1555
1836
|
...this.state.chatTranscripts,
|
|
@@ -1637,9 +1918,14 @@ window.__ModuleLoader__.load({
|
|
|
1637
1918
|
});
|
|
1638
1919
|
return Promise.resolve();
|
|
1639
1920
|
}
|
|
1640
|
-
case "turn/start":
|
|
1921
|
+
case "turn/start": {
|
|
1641
1922
|
this.openTurns.add(event.sessionId);
|
|
1642
1923
|
this.markWatchedRunning(event.sessionId, true);
|
|
1924
|
+
const chatSessionId = this.state.chatSessionsFor;
|
|
1925
|
+
if (chatSessionId !== void 0 && !this.state.chatSessions.some((row) => row.sessionId === event.sessionId) && !this.chatSessionRowReads.has(event.sessionId)) {
|
|
1926
|
+
this.chatSessionRowReads.add(event.sessionId);
|
|
1927
|
+
this.loadChatSessions(chatSessionId);
|
|
1928
|
+
}
|
|
1643
1929
|
if (event.sessionId === this.state.activeSessionId) {
|
|
1644
1930
|
this.state.blocks.push({
|
|
1645
1931
|
kind: "user",
|
|
@@ -1652,6 +1938,7 @@ window.__ModuleLoader__.load({
|
|
|
1652
1938
|
} : s) });
|
|
1653
1939
|
}
|
|
1654
1940
|
return Promise.resolve();
|
|
1941
|
+
}
|
|
1655
1942
|
case "turn/end":
|
|
1656
1943
|
if (!this.openTurns.has(event.sessionId)) return Promise.resolve();
|
|
1657
1944
|
this.openTurns.delete(event.sessionId);
|
|
@@ -1833,6 +2120,113 @@ window.__ModuleLoader__.load({
|
|
|
1833
2120
|
"usage_update",
|
|
1834
2121
|
"session_info_update"
|
|
1835
2122
|
]);
|
|
2123
|
+
/**
|
|
2124
|
+
* Context occupancy carried by one raw `usage_update` frame, in the shape the
|
|
2125
|
+
* session snapshots already use. A malformed frame is ignored (the last good
|
|
2126
|
+
* sample stays on screen) instead of zeroing the meter.
|
|
2127
|
+
*/
|
|
2128
|
+
function usageFromUpdate(update) {
|
|
2129
|
+
const used = update.used;
|
|
2130
|
+
const size = update.size;
|
|
2131
|
+
if (typeof used !== "number" || !Number.isFinite(used)) return void 0;
|
|
2132
|
+
if (typeof size !== "number" || !Number.isFinite(size)) return void 0;
|
|
2133
|
+
const cost = update.cost;
|
|
2134
|
+
if (cost !== null && typeof cost === "object" && !Array.isArray(cost)) {
|
|
2135
|
+
const row = cost;
|
|
2136
|
+
if (typeof row.amount === "number" && Number.isFinite(row.amount) && typeof row.currency === "string" && row.currency !== "") return {
|
|
2137
|
+
used,
|
|
2138
|
+
size,
|
|
2139
|
+
cost: {
|
|
2140
|
+
amount: row.amount,
|
|
2141
|
+
currency: row.currency
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
return {
|
|
2146
|
+
used,
|
|
2147
|
+
size
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function sameUsage(left, right) {
|
|
2151
|
+
if (left === right) return true;
|
|
2152
|
+
if (left === void 0 || right === void 0) return false;
|
|
2153
|
+
if (left.used !== right.used || left.size !== right.size) return false;
|
|
2154
|
+
const leftCost = left.cost;
|
|
2155
|
+
const rightCost = right.cost;
|
|
2156
|
+
if (leftCost === void 0 || rightCost === void 0) return leftCost === rightCost;
|
|
2157
|
+
return leftCost.amount === rightCost.amount && leftCost.currency === rightCost.currency;
|
|
2158
|
+
}
|
|
2159
|
+
/** Patch one session row's usage; other rows and an unchanged value keep their identities. */
|
|
2160
|
+
function withSessionUsage(rows, sessionId, usage) {
|
|
2161
|
+
let changed = false;
|
|
2162
|
+
const next = rows.map((row) => {
|
|
2163
|
+
if (row.sessionId !== sessionId || sameUsage(row.usage, usage)) return row;
|
|
2164
|
+
changed = true;
|
|
2165
|
+
return {
|
|
2166
|
+
...row,
|
|
2167
|
+
usage
|
|
2168
|
+
};
|
|
2169
|
+
});
|
|
2170
|
+
return changed ? next : rows;
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Flip one chat row's `released` flag (release/resume bookkeeping). The
|
|
2174
|
+
* composer's own list (`session/chat-list` rows) has no other live writer —
|
|
2175
|
+
* `session/released` must mark it, `session/reset` must un-mark it, or the
|
|
2176
|
+
* agent tag's halo and the released banner read yesterday's process state.
|
|
2177
|
+
*/
|
|
2178
|
+
function withSessionReleased(rows, sessionId, released) {
|
|
2179
|
+
let changed = false;
|
|
2180
|
+
const next = rows.map((row) => {
|
|
2181
|
+
if (row.sessionId !== sessionId || row.released === true === released) return row;
|
|
2182
|
+
changed = true;
|
|
2183
|
+
return {
|
|
2184
|
+
...row,
|
|
2185
|
+
released
|
|
2186
|
+
};
|
|
2187
|
+
});
|
|
2188
|
+
return changed ? next : rows;
|
|
2189
|
+
}
|
|
2190
|
+
function sameModes(a, b) {
|
|
2191
|
+
if (a === b) return true;
|
|
2192
|
+
if (!a || !b) return false;
|
|
2193
|
+
if (a.currentModeId !== b.currentModeId) return false;
|
|
2194
|
+
const aModes = a.availableModes ?? [];
|
|
2195
|
+
const bModes = b.availableModes ?? [];
|
|
2196
|
+
if (aModes.length !== bModes.length) return false;
|
|
2197
|
+
return aModes.every((m, i) => {
|
|
2198
|
+
const o = bModes[i];
|
|
2199
|
+
return m != null && o != null && m.id === o.id && m.name === o.name && m.description === o.description;
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
function withSessionModes(rows, sessionId, modes) {
|
|
2203
|
+
if (!modes) return rows;
|
|
2204
|
+
let changed = false;
|
|
2205
|
+
const next = rows.map((row) => {
|
|
2206
|
+
if (row.sessionId !== sessionId || sameModes(row.modes, modes)) return row;
|
|
2207
|
+
changed = true;
|
|
2208
|
+
return {
|
|
2209
|
+
...row,
|
|
2210
|
+
modes
|
|
2211
|
+
};
|
|
2212
|
+
});
|
|
2213
|
+
return changed ? next : rows;
|
|
2214
|
+
}
|
|
2215
|
+
function splitModelAgentId(modelId) {
|
|
2216
|
+
const at = modelId.indexOf("@");
|
|
2217
|
+
return (at === -1 ? modelId.trim() : at === 0 ? "" : modelId.slice(0, at).trim()) || void 0;
|
|
2218
|
+
}
|
|
2219
|
+
/** Last well-formed `usage_update` in one coalesced batch (later ticks win). */
|
|
2220
|
+
function usageOfBatch(items) {
|
|
2221
|
+
let usage;
|
|
2222
|
+
for (const item of items) {
|
|
2223
|
+
const update = item.update;
|
|
2224
|
+
if (update.sessionUpdate !== "usage_update") continue;
|
|
2225
|
+
const parsed = usageFromUpdate(update);
|
|
2226
|
+
if (parsed !== void 0) usage = parsed;
|
|
2227
|
+
}
|
|
2228
|
+
return usage;
|
|
2229
|
+
}
|
|
1836
2230
|
function trimBlocks(blocks) {
|
|
1837
2231
|
if (blocks.length > MAX_BLOCKS) blocks.splice(0, blocks.length - MAX_BLOCKS);
|
|
1838
2232
|
}
|
|
@@ -1952,6 +2346,13 @@ window.__ModuleLoader__.load({
|
|
|
1952
2346
|
/**
|
|
1953
2347
|
* Which session the composer is currently staged on.
|
|
1954
2348
|
*
|
|
2349
|
+
* The renderer's own current scope binding is the first answer when the caller
|
|
2350
|
+
* passes it (`currentSessionId`, read from `uiSession.adapter.current` in the
|
|
2351
|
+
* browser entry): that is the exact session the host has selected, while the
|
|
2352
|
+
* list can retain more than one row in a multi-view layout. Falling back to
|
|
2353
|
+
* the list is still required — a host without that scope adapter, and the old
|
|
2354
|
+
* snapshot shape below, have no better source.
|
|
2355
|
+
*
|
|
1955
2356
|
* Old runtimes answer from `snapshot.current` (with `currentAddress` as the
|
|
1956
2357
|
* subagent marker). rc.2 has no `current`: the staged session is the row the
|
|
1957
2358
|
* main view retains (`retainedBy.mainView > 0`) — the exact rule
|
|
@@ -1959,8 +2360,13 @@ window.__ModuleLoader__.load({
|
|
|
1959
2360
|
* row with `origin === "subagent"` is an addressed subagent route. `undefined`
|
|
1960
2361
|
* when nothing is staged yet (the list has not loaded, or no row holds the main
|
|
1961
2362
|
* view): callers must stand down rather than guess.
|
|
2363
|
+
*
|
|
2364
|
+
* A current id the list has no row for is ALSO `undefined`: the selection may
|
|
2365
|
+
* be a subagent address, and guessing "not a subagent" there would let the
|
|
2366
|
+
* gate install ACP chrome on a route the plugin must not drive. The next list
|
|
2367
|
+
* notification retries.
|
|
1962
2368
|
*/
|
|
1963
|
-
function stagedSessionOf(snapshot) {
|
|
2369
|
+
function stagedSessionOf(snapshot, currentSessionId) {
|
|
1964
2370
|
if (snapshot === void 0) return void 0;
|
|
1965
2371
|
if (snapshot.current !== void 0) return {
|
|
1966
2372
|
sessionId: snapshot.current,
|
|
@@ -1968,6 +2374,15 @@ window.__ModuleLoader__.load({
|
|
|
1968
2374
|
subagent: snapshot.currentAddress !== void 0
|
|
1969
2375
|
};
|
|
1970
2376
|
const byId = snapshot.byId ?? {};
|
|
2377
|
+
if (currentSessionId !== void 0) {
|
|
2378
|
+
const row = byId[currentSessionId];
|
|
2379
|
+
if (row === void 0) return void 0;
|
|
2380
|
+
return {
|
|
2381
|
+
sessionId: currentSessionId,
|
|
2382
|
+
row,
|
|
2383
|
+
subagent: row.origin === "subagent"
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
1971
2386
|
const order = /* @__PURE__ */ new Map();
|
|
1972
2387
|
for (const [index, id] of (snapshot.ids ?? []).entries()) order.set(id, index);
|
|
1973
2388
|
const rank = (id) => order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
@@ -1981,8 +2396,8 @@ window.__ModuleLoader__.load({
|
|
|
1981
2396
|
};
|
|
1982
2397
|
}
|
|
1983
2398
|
/** Whether the staged session runs the ACP agent preset. */
|
|
1984
|
-
function stagedIsAcpMode(snapshot, acpMode) {
|
|
1985
|
-
const staged = stagedSessionOf(snapshot);
|
|
2399
|
+
function stagedIsAcpMode(snapshot, acpMode, currentSessionId) {
|
|
2400
|
+
const staged = stagedSessionOf(snapshot, currentSessionId);
|
|
1986
2401
|
if (staged === void 0 || staged.subagent) return false;
|
|
1987
2402
|
const listed = rowAgentPreset(staged.row);
|
|
1988
2403
|
if (listed !== void 0) return listed === "acp";
|
|
@@ -2474,6 +2889,15 @@ window.__ModuleLoader__.load({
|
|
|
2474
2889
|
.ntr-acp-chip.is-static:hover { background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
|
|
2475
2890
|
.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); }
|
|
2476
2891
|
.ntr-acp-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2892
|
+
/* Composer context-occupancy ring (conversation.composer.dock,
|
|
2893
|
+
context-meter.tsx): the agent's own prompt-side used/size in the host
|
|
2894
|
+
ContextMeter's ring geometry, pill and tokens, so it reads as that meter's
|
|
2895
|
+
sibling in the bottom info row. Gated inside the component; absent until the
|
|
2896
|
+
agent reports a sample. */
|
|
2897
|
+
.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; }
|
|
2898
|
+
.ntr-acp-meter-track { fill: none; stroke: var(--dsw-alias-border-l3, #e5e7eb); stroke-width: 2px; }
|
|
2899
|
+
.ntr-acp-meter-fill { fill: none; stroke: var(--dsw-alias-label-tertiary, #9ca3af); stroke-width: 2px; stroke-linecap: round; }
|
|
2900
|
+
.ntr-acp-meter-percent { color: inherit; }
|
|
2477
2901
|
/* The lock hand-off (src/client/agent-lock-anim.ts): the outgoing agent base
|
|
2478
2902
|
breaks into debris for 460ms, then the edge tag condenses from the same
|
|
2479
2903
|
particles. Both halves read one shared clock in that module; these durations
|
|
@@ -2649,6 +3073,79 @@ window.__ModuleLoader__.load({
|
|
|
2649
3073
|
.ntr-permission-option:focus-visible, .ntr-permission-cancel:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #2563eb); outline-offset: 2px; }
|
|
2650
3074
|
.ntr-permission-cancel { margin-left: auto; color: var(--dsw-alias-label-secondary, #6b7280); }
|
|
2651
3075
|
.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; }
|
|
3076
|
+
/* ---- Released-session banner (conversation.input.dock, released-banner.tsx) ---- */
|
|
3077
|
+
/* Same composer-width formula as the permission dock above: the slot spans
|
|
3078
|
+
the column, the card lines up with the input box. One row of state + one
|
|
3079
|
+
action; the error pins under it, beside the button that failed. */
|
|
3080
|
+
.ntr-released-banner {
|
|
3081
|
+
box-sizing: border-box;
|
|
3082
|
+
width: calc(100% - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-side-clearance, 16px));
|
|
3083
|
+
max-width: var(--dsh-composer-card-max-width, 920px);
|
|
3084
|
+
margin: 0 auto;
|
|
3085
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
3086
|
+
padding: 8px 12px;
|
|
3087
|
+
border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
|
|
3088
|
+
border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706);
|
|
3089
|
+
border-radius: 10px;
|
|
3090
|
+
background: var(--dsw-alias-bg-layer-1, #fff);
|
|
3091
|
+
font-size: 12px; line-height: 1.5;
|
|
3092
|
+
}
|
|
3093
|
+
.ntr-released-banner-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3094
|
+
.ntr-released-banner-text { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
|
|
3095
|
+
.ntr-released-banner-title { font-weight: 600; }
|
|
3096
|
+
.ntr-released-banner-body { color: var(--dsw-alias-label-secondary, #6b7280); }
|
|
3097
|
+
.ntr-released-banner .ntr-acp-more { flex: none; white-space: nowrap; }
|
|
3098
|
+
/* ---- NTR message actions (conversation.chat.assistant-actions) ---- */
|
|
3099
|
+
/* In an NTR conversation the host's like / dislike / branch icons are the
|
|
3100
|
+
plugin's business (the agent decides its own feedback surface, and the
|
|
3101
|
+
host's branch would fork a conversation the agent does not remember). The
|
|
3102
|
+
three icons hide under the html[data-ntr-acp] scope marker, by their
|
|
3103
|
+
LOCALIZED aria-labels — the only stable handles the host leaves (its button
|
|
3104
|
+
classes are CSS-module hashes and there is no data-* hook). Hiding the
|
|
3105
|
+
button is the WHOLE job: the host's Tooltip clones its child without a
|
|
3106
|
+
wrapper element, so there is no container left behind to plug the row gap
|
|
3107
|
+
— and any :has(> button…) rule would match the actions row itself (and the
|
|
3108
|
+
list slot's shared display:contents anchor), hiding the entire row. The
|
|
3109
|
+
:not([data-ntr-branch]) exemption is the pass our OWN button carries —
|
|
3110
|
+
same label, same row. Known fragility: a host copy change or a third
|
|
3111
|
+
language un-hides the icons; the entry probes the labels once per page and
|
|
3112
|
+
warns in the console. */
|
|
3113
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="在新对话中分支"]:not([data-ntr-branch]),
|
|
3114
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="Branch into a new conversation"]:not([data-ntr-branch]),
|
|
3115
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="好的回答"],
|
|
3116
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="有问题的回答"],
|
|
3117
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="取消标记"],
|
|
3118
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="Good response"],
|
|
3119
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="Bad response"],
|
|
3120
|
+
html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
3121
|
+
display: none !important;
|
|
3122
|
+
}
|
|
3123
|
+
/* The plugin's own branch button, dressed exactly like the host's .action
|
|
3124
|
+
class (the values are copied from the host's compiled CSS, token colors
|
|
3125
|
+
only). It rides the same actions row, so the host's row gap, font-delta
|
|
3126
|
+
sizing and the [data-actions-reveal] hover rules already govern it. */
|
|
3127
|
+
.ntr-branch-action {
|
|
3128
|
+
width: calc(28px + var(--dsh-content-font-delta, 0px));
|
|
3129
|
+
height: calc(28px + var(--dsh-content-font-delta, 0px));
|
|
3130
|
+
color: var(--dsw-alias-label-tertiary, #9ca3af);
|
|
3131
|
+
cursor: pointer;
|
|
3132
|
+
background: none;
|
|
3133
|
+
border: none;
|
|
3134
|
+
border-radius: 28px;
|
|
3135
|
+
justify-content: center;
|
|
3136
|
+
align-items: center;
|
|
3137
|
+
padding: 6px;
|
|
3138
|
+
display: inline-flex;
|
|
3139
|
+
}
|
|
3140
|
+
.ntr-branch-action svg { width: calc(15px + var(--dsh-content-font-delta, 0px)); height: calc(15px + var(--dsh-content-font-delta, 0px)); }
|
|
3141
|
+
.ntr-branch-action:hover { background: var(--dsw-alias-interactive-bg-hover, #0000000d); color: var(--dsw-alias-label-secondary, #6b7280); }
|
|
3142
|
+
.ntr-branch-action[data-unavailable] { cursor: default; opacity: 0.4; }
|
|
3143
|
+
.ntr-branch-action[data-unavailable]:hover { color: var(--dsw-alias-label-tertiary, #9ca3af); background: none; }
|
|
3144
|
+
.ntr-branch-action:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #2563eb); outline-offset: 2px; }
|
|
3145
|
+
/* A failed branch click, pinned in the row beside the button (the conversation
|
|
3146
|
+
surface of the settings banner the store also writes). One line, truncated;
|
|
3147
|
+
the full text is the title tooltip and the banner. */
|
|
3148
|
+
.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); }
|
|
2652
3149
|
/* ---- NTR settings dialog (shell.overlay) ---- */
|
|
2653
3150
|
/* Frame-wide surface opened from the agent base's empty-roster menu. The
|
|
2654
3151
|
overlay layer itself is click-through, so the layer opts back in and blocks
|
|
@@ -2921,6 +3418,17 @@ window.__ModuleLoader__.load({
|
|
|
2921
3418
|
.dwa-deadline-flag { margin-top: 4px; font-size: var(--ntr-font-xs); font-weight: 550; color: var(--ntr-warn); }
|
|
2922
3419
|
.dwa-deadline-warn { margin-top: 10px; padding: 9px 12px; border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-secondary, #fffbeb); border-radius: var(--ntr-radius-sm); font-size: var(--ntr-font-xs); line-height: 1.55; }
|
|
2923
3420
|
.dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
|
|
3421
|
+
/* Runtime log: one bounded list, newest first. The time column never wraps;
|
|
3422
|
+
the message is the only cell that may break anywhere. */
|
|
3423
|
+
.dwa-log-list { list-style: none; margin: 0; padding: 0; border: var(--ntr-line); border-radius: var(--ntr-radius); overflow: hidden; }
|
|
3424
|
+
.dwa-log-row { display: flex; align-items: baseline; gap: 8px; padding: 6px 10px; font-size: var(--ntr-font-sm); border-top: var(--ntr-line); min-width: 0; }
|
|
3425
|
+
.dwa-log-row:first-child { border-top: 0; }
|
|
3426
|
+
.dwa-log-row.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); }
|
|
3427
|
+
.dwa-log-time { color: var(--ntr-muted); white-space: nowrap; }
|
|
3428
|
+
.dwa-log-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
3429
|
+
.dwa-log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ntr-danger); display: inline-block; margin-left: 6px; }
|
|
3430
|
+
.dwa-log-level.is-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
3431
|
+
.dwa-log-level.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); color: var(--ntr-danger); }
|
|
2924
3432
|
@media (max-width: 640px) {
|
|
2925
3433
|
.dwa-root { padding: 20px 16px; }
|
|
2926
3434
|
}
|
|
@@ -3095,22 +3603,14 @@ window.__ModuleLoader__.load({
|
|
|
3095
3603
|
* def (env / npmPackage / exposedModels included); the synthesized fallback
|
|
3096
3604
|
* covers only the fields an `AgentView` carries, so anything else compares as
|
|
3097
3605
|
* "changed" — the honest answer when the host never reported it.
|
|
3606
|
+
*
|
|
3607
|
+
* The idle-release field is compared AS STORED: an absent per-agent field
|
|
3608
|
+
* means "follow the global window", and the host's resolved value must not be
|
|
3609
|
+
* written back into it — that would freeze the global window into this agent's
|
|
3610
|
+
* definition and the global slider would then never reach it.
|
|
3098
3611
|
*/
|
|
3099
|
-
/**
|
|
3100
|
-
* The idle-release window in effect for an agent: an unset per-agent field is
|
|
3101
|
-
* resolved by the HOST and reported back on `AgentView`, so filling it from
|
|
3102
|
-
* there keeps "unset" and "explicitly the host default" the same state —
|
|
3103
|
-
* merely opening the form must never read as an unsaved change.
|
|
3104
|
-
*/
|
|
3105
|
-
function resolvedIdleTimeoutMs(def, activeAgent) {
|
|
3106
|
-
if (def.idleTimeoutMs !== void 0 || activeAgent?.idleTimeoutMs === void 0) return def;
|
|
3107
|
-
return {
|
|
3108
|
-
...def,
|
|
3109
|
-
idleTimeoutMs: activeAgent.idleTimeoutMs
|
|
3110
|
-
};
|
|
3111
|
-
}
|
|
3112
3612
|
function persistedDef(detected, activeAgent) {
|
|
3113
|
-
if (detected.currentConfig !== void 0) return
|
|
3613
|
+
if (detected.currentConfig !== void 0) return detected.currentConfig;
|
|
3114
3614
|
if (activeAgent === void 0) return void 0;
|
|
3115
3615
|
return {
|
|
3116
3616
|
id: activeAgent.id,
|
|
@@ -3119,8 +3619,7 @@ window.__ModuleLoader__.load({
|
|
|
3119
3619
|
args: [...activeAgent.args],
|
|
3120
3620
|
...activeAgent.cwd === void 0 ? {} : { cwd: activeAgent.cwd },
|
|
3121
3621
|
allowedCwdRoots: [...activeAgent.allowedCwdRoots],
|
|
3122
|
-
env: {}
|
|
3123
|
-
...activeAgent.idleTimeoutMs === void 0 ? {} : { idleTimeoutMs: activeAgent.idleTimeoutMs }
|
|
3622
|
+
env: {}
|
|
3124
3623
|
};
|
|
3125
3624
|
}
|
|
3126
3625
|
/**
|
|
@@ -3141,7 +3640,7 @@ window.__ModuleLoader__.load({
|
|
|
3141
3640
|
if (exposureKey(payload.exposedModels) !== exposureKey(current.exposedModels)) return false;
|
|
3142
3641
|
if (mcpKey(payload.mcpServers) !== mcpKey(current.mcpServers)) return false;
|
|
3143
3642
|
if (payload.autoDiscoverModels !== false !== (current.autoDiscoverModels !== false)) return false;
|
|
3144
|
-
if (payload.idleTimeoutMs !==
|
|
3643
|
+
if (payload.idleTimeoutMs !== current.idleTimeoutMs) return false;
|
|
3145
3644
|
return true;
|
|
3146
3645
|
}
|
|
3147
3646
|
/**
|
|
@@ -3449,6 +3948,131 @@ window.__ModuleLoader__.load({
|
|
|
3449
3948
|
})]
|
|
3450
3949
|
});
|
|
3451
3950
|
}
|
|
3951
|
+
/**
|
|
3952
|
+
* The idle-release window, as the settings page edits it.
|
|
3953
|
+
*
|
|
3954
|
+
* ONE control, because there is ONE window: how long a chat process may sit
|
|
3955
|
+
* idle before it is parked. The slider snaps to the shared ladder
|
|
3956
|
+
* ({@link IDLE_RELEASE_MS}), so every rung it offers is a window the host can
|
|
3957
|
+
* run.
|
|
3958
|
+
*
|
|
3959
|
+
* The card reports the deployment baseline as the fact being overridden, and
|
|
3960
|
+
* offers a reset ONLY when something is really overridden — the same rule the
|
|
3961
|
+
* approval deadline card follows.
|
|
3962
|
+
*/
|
|
3963
|
+
function IdleReleaseCard(props) {
|
|
3964
|
+
const { acp, ui } = props;
|
|
3965
|
+
const release = ui.idleRelease;
|
|
3966
|
+
if (release === void 0) return null;
|
|
3967
|
+
const baseline = release.defaultIdleTimeoutMs;
|
|
3968
|
+
const overridden = release.idleTimeoutMs !== baseline;
|
|
3969
|
+
const index = IDLE_RELEASE_MS.indexOf(release.idleTimeoutMs);
|
|
3970
|
+
const exact = index >= 0;
|
|
3971
|
+
const shown = exact ? release.idleTimeoutMs : nearestIdleRung(release.idleTimeoutMs);
|
|
3972
|
+
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.`);
|
|
3973
|
+
const resetLabel = release.configuredIdleTimeoutMs === void 0 ? acp.label("恢复默认值", "Restore default") : acp.label("恢复配置文件的值", "Use config file value");
|
|
3974
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3975
|
+
className: "dwa-section",
|
|
3976
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3977
|
+
className: "dwa-section-heading",
|
|
3978
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
3979
|
+
className: "dwa-section-title",
|
|
3980
|
+
children: acp.label("空闲释放", "Idle release")
|
|
3981
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3982
|
+
className: "dwa-section-copy",
|
|
3983
|
+
children: acp.label("空闲多久后挂起聊天进程。", "How long an idle chat process is kept before it is parked.")
|
|
3984
|
+
})] })
|
|
3985
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3986
|
+
className: "dwa-preferences",
|
|
3987
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3988
|
+
className: "dwa-preference-block",
|
|
3989
|
+
children: [
|
|
3990
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3991
|
+
className: "dwa-preference-label",
|
|
3992
|
+
children: acp.label("释放窗口", "Release window")
|
|
3993
|
+
}),
|
|
3994
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3995
|
+
className: "dwa-deadline-value",
|
|
3996
|
+
children: idleWindowText(release.idleTimeoutMs, acp)
|
|
3997
|
+
}),
|
|
3998
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3999
|
+
className: "dwa-deadline-range",
|
|
4000
|
+
type: "range",
|
|
4001
|
+
min: 0,
|
|
4002
|
+
max: IDLE_RELEASE_MS.length - 1,
|
|
4003
|
+
step: 1,
|
|
4004
|
+
value: exact ? index : IDLE_RELEASE_MS.indexOf(shown),
|
|
4005
|
+
"aria-label": acp.label("释放窗口", "Release window"),
|
|
4006
|
+
onChange: (event) => {
|
|
4007
|
+
const next = IDLE_RELEASE_MS[Number(event.target.value)];
|
|
4008
|
+
if (next !== void 0 && next !== release.idleTimeoutMs) acp.setIdleRelease(next);
|
|
4009
|
+
}
|
|
4010
|
+
}),
|
|
4011
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4012
|
+
className: "dwa-deadline-ticks",
|
|
4013
|
+
children: IDLE_RELEASE_MS.filter((_, i) => i % 2 === 0).map((ms) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: idleWindowText(ms, acp) }, ms))
|
|
4014
|
+
}),
|
|
4015
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4016
|
+
className: "dwa-preference-copy",
|
|
4017
|
+
children: acp.label("挂起后会话仍保留,下一条消息自动恢复。", "A parked session is kept; the next message resumes it.")
|
|
4018
|
+
}),
|
|
4019
|
+
note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4020
|
+
className: "dwa-deadline-flag",
|
|
4021
|
+
children: ["⚠ ", note]
|
|
4022
|
+
})
|
|
4023
|
+
]
|
|
4024
|
+
}), overridden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4025
|
+
className: "dwa-deadline-reset",
|
|
4026
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4027
|
+
type: "button",
|
|
4028
|
+
className: "dwa-btn dwa-btn-secondary",
|
|
4029
|
+
onClick: () => void acp.resetIdleRelease(),
|
|
4030
|
+
children: resetLabel
|
|
4031
|
+
})
|
|
4032
|
+
})]
|
|
4033
|
+
})]
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
/**
|
|
4037
|
+
* The rung nearest a window that is not on the ladder — used only to park the
|
|
4038
|
+
* thumb; the readout always prints the exact value.
|
|
4039
|
+
*/
|
|
4040
|
+
function nearestIdleRung(ms) {
|
|
4041
|
+
let best = IDLE_RELEASE_MS[0];
|
|
4042
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
4043
|
+
for (const rung of IDLE_RELEASE_MS) {
|
|
4044
|
+
const distance = Math.abs(rung - ms);
|
|
4045
|
+
if (distance < bestDistance) {
|
|
4046
|
+
bestDistance = distance;
|
|
4047
|
+
best = rung;
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
return best;
|
|
4051
|
+
}
|
|
4052
|
+
/**
|
|
4053
|
+
* `30 分钟` — the readout and the tick label, one formatter.
|
|
4054
|
+
*
|
|
4055
|
+
* ALWAYS minutes, even at the top of the ladder: a scale that runs "1 分钟"
|
|
4056
|
+
* to "50 分钟" and then "1 小时" reads as two different scales.
|
|
4057
|
+
*/
|
|
4058
|
+
function idleWindowText(ms, acp) {
|
|
4059
|
+
if (ms <= 0) return acp.label("永不释放", "Never");
|
|
4060
|
+
const minutes = Math.round(ms / 6e4);
|
|
4061
|
+
return acp.label(`${String(minutes)} 分钟`, `${String(minutes)}m`);
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* Local wall-clock `HH:MM:SS` for one runtime-log line. Hand-built on purpose:
|
|
4065
|
+
* `toLocaleTimeString` may drop seconds or pick a 12-hour clock depending on
|
|
4066
|
+
* the browser locale, which makes the column ragged and ambiguous.
|
|
4067
|
+
*/
|
|
4068
|
+
function logClock(at) {
|
|
4069
|
+
const date = new Date(at);
|
|
4070
|
+
return [
|
|
4071
|
+
date.getHours(),
|
|
4072
|
+
date.getMinutes(),
|
|
4073
|
+
date.getSeconds()
|
|
4074
|
+
].map((value) => String(value).padStart(2, "0")).join(":");
|
|
4075
|
+
}
|
|
3452
4076
|
/** The states that carry an update badge next to the version badge. */
|
|
3453
4077
|
function updateBadgeText(update, acp) {
|
|
3454
4078
|
if (update.status === "available") return acp.label(`可更新 v${update.latest ?? update.current}`, `Update v${update.latest ?? update.current}`);
|
|
@@ -3594,6 +4218,13 @@ window.__ModuleLoader__.load({
|
|
|
3594
4218
|
(0, react.useEffect)(() => {
|
|
3595
4219
|
acp.checkPluginUpdate();
|
|
3596
4220
|
}, [acp]);
|
|
4221
|
+
(0, react.useEffect)(() => {
|
|
4222
|
+
if (page === "logs" && ui.logUnread > 0) acp.markLogsRead();
|
|
4223
|
+
}, [
|
|
4224
|
+
acp,
|
|
4225
|
+
page,
|
|
4226
|
+
ui.logUnread
|
|
4227
|
+
]);
|
|
3597
4228
|
const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents);
|
|
3598
4229
|
const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED;
|
|
3599
4230
|
const openAdd = () => {
|
|
@@ -3755,11 +4386,26 @@ window.__ModuleLoader__.load({
|
|
|
3755
4386
|
onClick: () => setPage("approvals"),
|
|
3756
4387
|
children: acp.label("审批时限", "Approval deadlines")
|
|
3757
4388
|
}),
|
|
4389
|
+
ui.idleRelease !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4390
|
+
type: "button",
|
|
4391
|
+
"aria-pressed": page === "processes",
|
|
4392
|
+
onClick: () => setPage("processes"),
|
|
4393
|
+
children: acp.label("进程管理", "Processes")
|
|
4394
|
+
}),
|
|
3758
4395
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3759
4396
|
type: "button",
|
|
3760
4397
|
"aria-pressed": page === "appearance",
|
|
3761
4398
|
onClick: () => setPage("appearance"),
|
|
3762
4399
|
children: acp.label("对话外观", "Appearance")
|
|
4400
|
+
}),
|
|
4401
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4402
|
+
type: "button",
|
|
4403
|
+
"aria-pressed": page === "logs",
|
|
4404
|
+
onClick: () => setPage("logs"),
|
|
4405
|
+
children: [acp.label("日志", "Logs"), ui.logUnread > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4406
|
+
className: "dwa-log-dot",
|
|
4407
|
+
"aria-hidden": "true"
|
|
4408
|
+
})]
|
|
3763
4409
|
})
|
|
3764
4410
|
]
|
|
3765
4411
|
}),
|
|
@@ -3949,6 +4595,13 @@ window.__ModuleLoader__.load({
|
|
|
3949
4595
|
ui
|
|
3950
4596
|
})
|
|
3951
4597
|
}),
|
|
4598
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4599
|
+
hidden: page !== "processes",
|
|
4600
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IdleReleaseCard, {
|
|
4601
|
+
acp,
|
|
4602
|
+
ui
|
|
4603
|
+
})
|
|
4604
|
+
}),
|
|
3952
4605
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3953
4606
|
className: "dwa-section",
|
|
3954
4607
|
hidden: page !== "appearance",
|
|
@@ -4011,6 +4664,52 @@ window.__ModuleLoader__.load({
|
|
|
4011
4664
|
})]
|
|
4012
4665
|
})]
|
|
4013
4666
|
}),
|
|
4667
|
+
page === "logs" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4668
|
+
className: "dwa-section",
|
|
4669
|
+
"aria-label": acp.label("运行日志", "Runtime log"),
|
|
4670
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4671
|
+
className: "dwa-section-heading",
|
|
4672
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
4673
|
+
className: "dwa-section-title",
|
|
4674
|
+
children: acp.label("运行日志", "Runtime log")
|
|
4675
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4676
|
+
className: "dwa-section-copy",
|
|
4677
|
+
children: acp.label(`agent 运行消息,最多 ${String(200)} 条。`, `Agent runtime messages, up to ${String(200)}.`)
|
|
4678
|
+
})] }), ui.logs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4679
|
+
type: "button",
|
|
4680
|
+
className: "dwa-btn dwa-btn-quiet",
|
|
4681
|
+
onClick: () => acp.clearLogs(),
|
|
4682
|
+
children: acp.label("清空", "Clear")
|
|
4683
|
+
})]
|
|
4684
|
+
}), ui.logs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4685
|
+
className: "dwa-empty",
|
|
4686
|
+
role: "status",
|
|
4687
|
+
children: acp.label("暂无运行日志。", "No runtime messages yet.")
|
|
4688
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
4689
|
+
className: "dwa-log-list",
|
|
4690
|
+
children: [...ui.logs].reverse().map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4691
|
+
className: entry.level === "error" ? "dwa-log-row is-error" : "dwa-log-row",
|
|
4692
|
+
children: [
|
|
4693
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4694
|
+
className: "dwa-log-time dwa-mono",
|
|
4695
|
+
children: logClock(entry.at)
|
|
4696
|
+
}),
|
|
4697
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4698
|
+
className: `dwa-badge dwa-log-level ${entry.level === "error" ? "is-error" : "is-info"}`,
|
|
4699
|
+
children: entry.level === "error" ? acp.label("错误", "Error") : acp.label("信息", "Info")
|
|
4700
|
+
}),
|
|
4701
|
+
entry.agentId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4702
|
+
className: "dwa-badge",
|
|
4703
|
+
children: entry.agentId
|
|
4704
|
+
}),
|
|
4705
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4706
|
+
className: "dwa-log-text",
|
|
4707
|
+
children: entry.text
|
|
4708
|
+
})
|
|
4709
|
+
]
|
|
4710
|
+
}, entry.id))
|
|
4711
|
+
})]
|
|
4712
|
+
}),
|
|
4014
4713
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
|
|
4015
4714
|
className: "dwa-health",
|
|
4016
4715
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4295,13 +4994,6 @@ window.__ModuleLoader__.load({
|
|
|
4295
4994
|
});
|
|
4296
4995
|
}
|
|
4297
4996
|
/**
|
|
4298
|
-
* Hand copy of `DEFAULT_TUNING.idleTimeoutMs` (src/bridge.ts): the browser
|
|
4299
|
-
* cannot import the host plane, and this number is only ever placeholder text
|
|
4300
|
-
* for a NOT-yet-configured agent (a configured one renders the host's resolved
|
|
4301
|
-
* value from `AgentView.idleTimeoutMs`). Keep the two in sync.
|
|
4302
|
-
*/
|
|
4303
|
-
const DEFAULT_IDLE_RELEASE_MS = 18e5;
|
|
4304
|
-
/**
|
|
4305
4997
|
* Hand copy of `DEFAULT_TUNING.probeIdleTimeoutMs` (src/bridge.ts). A process
|
|
4306
4998
|
* that has never hosted a session — the model probe / post-save probe — lives
|
|
4307
4999
|
* by THIS window whatever the agent's own field says, so the placeholder has to
|
|
@@ -4325,7 +5017,7 @@ window.__ModuleLoader__.load({
|
|
|
4325
5017
|
})),
|
|
4326
5018
|
exposed: config?.exposedModels === void 0 ? void 0 : [...config.exposedModels],
|
|
4327
5019
|
autoDiscoverModels: config?.autoDiscoverModels !== false,
|
|
4328
|
-
idleStr: idleText(
|
|
5020
|
+
idleStr: idleText(config?.idleTimeoutMs),
|
|
4329
5021
|
mcpText: config?.mcpServers === void 0 ? "" : JSON.stringify(config.mcpServers, null, 2)
|
|
4330
5022
|
};
|
|
4331
5023
|
}
|
|
@@ -4526,18 +5218,18 @@ window.__ModuleLoader__.load({
|
|
|
4526
5218
|
children: [
|
|
4527
5219
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
4528
5220
|
className: "dwa-form-label",
|
|
4529
|
-
children: acp.label("
|
|
5221
|
+
children: acp.label("空闲释放覆盖(毫秒)", "Idle Release Override (ms)")
|
|
4530
5222
|
}),
|
|
4531
5223
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4532
5224
|
className: "dwa-input dwa-input-mono",
|
|
4533
|
-
"aria-label": acp.label("
|
|
5225
|
+
"aria-label": acp.label("空闲释放覆盖(毫秒)", "Idle Release Override (ms)"),
|
|
4534
5226
|
value: form.idleStr,
|
|
4535
5227
|
onChange: (e) => patch({ idleStr: e.target.value }),
|
|
4536
|
-
placeholder: acp.label(`留空 =
|
|
5228
|
+
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)`)
|
|
4537
5229
|
}),
|
|
4538
5230
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4539
5231
|
className: idleInvalid ? "dwa-error" : "dwa-hint",
|
|
4540
|
-
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("
|
|
5232
|
+
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”.") })
|
|
4541
5233
|
})
|
|
4542
5234
|
]
|
|
4543
5235
|
})
|
|
@@ -5352,6 +6044,34 @@ window.__ModuleLoader__.load({
|
|
|
5352
6044
|
function agentModeTitle(agentId, acp) {
|
|
5353
6045
|
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.`);
|
|
5354
6046
|
}
|
|
6047
|
+
/**
|
|
6048
|
+
* Whether this conversation is one the agent-mode control may serve.
|
|
6049
|
+
*
|
|
6050
|
+
* An EXPLICIT non-ACP verdict outranks a lingering session row: switching a
|
|
6051
|
+
* conversation off the NTR preset does not close its ACP session, so "a row
|
|
6052
|
+
* exists" alone would keep this control on an ordinary conversation — the same
|
|
6053
|
+
* precedence the agent tag, the context ring and the released banner already
|
|
6054
|
+
* apply. Only while the host has not answered (`undefined`) does a bound
|
|
6055
|
+
* session count as proof, which is what keeps the control available before the
|
|
6056
|
+
* host's first `session/chat-list` answer lands.
|
|
6057
|
+
*/
|
|
6058
|
+
function agentModeVisible(acpMode, hasBoundSession) {
|
|
6059
|
+
if (acpMode === false) return false;
|
|
6060
|
+
return acpMode === true || hasBoundSession;
|
|
6061
|
+
}
|
|
6062
|
+
/**
|
|
6063
|
+
* The mode the select shows as in force.
|
|
6064
|
+
*
|
|
6065
|
+
* A LIVE session is the authority (it is what the agent runs right now). A
|
|
6066
|
+
* RELEASED row only describes the mode the agent was parked ON, so a newer
|
|
6067
|
+
* pick — which `applyChatChoice` will apply on the next resume — outranks it
|
|
6068
|
+
* (and the parked row still beats the catalog default). Same precedence as
|
|
6069
|
+
* the model and thought level in `describeRoute`: a pre-switch on a parked
|
|
6070
|
+
* session must be visible or the control reads as dead.
|
|
6071
|
+
*/
|
|
6072
|
+
function modeInForce(target, persisted, fallback) {
|
|
6073
|
+
return (target !== void 0 && target.released !== true ? target.modes?.currentModeId : void 0) ?? persisted ?? target?.modes?.currentModeId ?? fallback ?? "";
|
|
6074
|
+
}
|
|
5355
6075
|
function AcpComposerMode({ acp, sessionId, useSession }) {
|
|
5356
6076
|
const ui = useUi(acp);
|
|
5357
6077
|
const dshSessionId = String(sessionId);
|
|
@@ -5366,7 +6086,7 @@ window.__ModuleLoader__.load({
|
|
|
5366
6086
|
]);
|
|
5367
6087
|
const bound = ui.chatSessionsFor === dshSessionId ? ui.chatSessions : [];
|
|
5368
6088
|
const target = bound.find((session) => !session.released) ?? bound[0];
|
|
5369
|
-
const isAcp =
|
|
6089
|
+
const isAcp = agentModeVisible(ui.acpMode[dshSessionId], target !== void 0);
|
|
5370
6090
|
const agentId = target?.agentId ?? ui.chatLockedAgent[dshSessionId] ?? ui.chatBoundAgent[dshSessionId] ?? ui.chatChoice[dshSessionId]?.agentId ?? (isAcp ? ui.defaultAgent : void 0);
|
|
5371
6091
|
const catalog = agentId === void 0 ? void 0 : ui.seatCatalogs[agentId];
|
|
5372
6092
|
(0, react.useEffect)(() => {
|
|
@@ -5378,15 +6098,15 @@ window.__ModuleLoader__.load({
|
|
|
5378
6098
|
isAcp,
|
|
5379
6099
|
target
|
|
5380
6100
|
]);
|
|
6101
|
+
if (!isAcp) return null;
|
|
5381
6102
|
const live = target?.modes?.availableModes ?? [];
|
|
5382
|
-
const modes = live.length > 0 ? live : (
|
|
6103
|
+
const modes = live.length > 0 ? live : (catalog?.mode?.choices ?? []).map((choice) => ({
|
|
5383
6104
|
id: choice.id,
|
|
5384
6105
|
name: choice.name,
|
|
5385
6106
|
description: choice.description
|
|
5386
6107
|
}));
|
|
5387
6108
|
if (modes.length === 0) return null;
|
|
5388
|
-
const
|
|
5389
|
-
const current = target?.modes?.currentModeId ?? persisted ?? catalog?.mode?.currentValue ?? modes[0]?.id ?? "";
|
|
6109
|
+
const current = modeInForce(target, ui.chatChoice[dshSessionId]?.agentId === agentId ? ui.chatChoice[dshSessionId]?.mode : void 0, catalog?.mode?.currentValue ?? modes[0]?.id);
|
|
5390
6110
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
5391
6111
|
style: {
|
|
5392
6112
|
display: "inline-flex",
|
|
@@ -5422,6 +6142,109 @@ window.__ModuleLoader__.load({
|
|
|
5422
6142
|
})
|
|
5423
6143
|
});
|
|
5424
6144
|
}
|
|
6145
|
+
//#endregion
|
|
6146
|
+
//#region src/client/context-meter.tsx
|
|
6147
|
+
/**
|
|
6148
|
+
* Composer context-occupancy ring at `conversation.composer.dock` — the bottom
|
|
6149
|
+
* info row below the input card, where the host also renders its ContextMeter.
|
|
6150
|
+
*
|
|
6151
|
+
* WHY A PLUGIN-OWNED RING: the host's meter reads its `contextPressure`
|
|
6152
|
+
* projection, folded from DSH provider-usage samples plus a route capacity. An
|
|
6153
|
+
* ACP conversation produces neither — the agent owns the request and the host
|
|
6154
|
+
* never sees its prompt — so the native meter renders nothing there. This ring
|
|
6155
|
+
* reports the agent's OWN `usage_update` used/size instead: occupancy only. ACP
|
|
6156
|
+
* declares no system/tools/messages split, so no breakdown is invented.
|
|
6157
|
+
*
|
|
6158
|
+
* Registered for every conversation and gating itself: it renders only for an
|
|
6159
|
+
* ACP conversation whose session row carries usage. The row comes from
|
|
6160
|
+
* `session/chat-list`; the store live-patches it from the coalesced
|
|
6161
|
+
* `usage_update` frames (see `AcpStore.flushUpdates`).
|
|
6162
|
+
*/
|
|
6163
|
+
/**
|
|
6164
|
+
* The sample this conversation's ring should show, or `undefined` when the
|
|
6165
|
+
* entry must render nothing. An explicit `acpMode === false` outranks a stale
|
|
6166
|
+
* `chatSessions` row: the conversation's preset left NTR while its ACP session
|
|
6167
|
+
* was still alive, and the ring must leave with it (same precedence as the
|
|
6168
|
+
* agent tag's `agentTagVisible`). While the verdict is unknown, a LIVE row is
|
|
6169
|
+
* proof; a PARKED one is not — it can outlive the preset switch, and the ring
|
|
6170
|
+
* must not appear on an ordinary conversation during the answer window.
|
|
6171
|
+
*/
|
|
6172
|
+
function acpMeterUsage(ui, dshSessionId) {
|
|
6173
|
+
if (ui.acpMode[dshSessionId] === false) return void 0;
|
|
6174
|
+
if (ui.chatSessionsFor !== dshSessionId) return void 0;
|
|
6175
|
+
const live = ui.chatSessions.find((row) => !row.released);
|
|
6176
|
+
if (live === void 0 && ui.acpMode[dshSessionId] !== true) return void 0;
|
|
6177
|
+
return (live ?? ui.chatSessions[0])?.usage;
|
|
6178
|
+
}
|
|
6179
|
+
/**
|
|
6180
|
+
* Bounded display occupancy from one agent sample. `undefined` (no meter) for
|
|
6181
|
+
* an absent / malformed / zero-capacity sample rather than a made-up 0%.
|
|
6182
|
+
*/
|
|
6183
|
+
function contextOccupancy(usage) {
|
|
6184
|
+
if (usage === void 0) return void 0;
|
|
6185
|
+
const { used, size } = usage;
|
|
6186
|
+
if (!Number.isFinite(used) || !Number.isFinite(size) || used < 0 || size <= 0) return void 0;
|
|
6187
|
+
return {
|
|
6188
|
+
percent: Math.min(100, Math.round(used / size * 100)),
|
|
6189
|
+
used,
|
|
6190
|
+
size
|
|
6191
|
+
};
|
|
6192
|
+
}
|
|
6193
|
+
/** Compact token count for the tooltip, in the host meter's K/M shape. */
|
|
6194
|
+
function compactTokenCount(value) {
|
|
6195
|
+
if (value < 1e3) return String(Math.round(value));
|
|
6196
|
+
const thousands = value / 1e3;
|
|
6197
|
+
if (value < 1e6) return `${thousands >= 100 ? String(Math.round(thousands)) : String(Math.round(thousands * 10) / 10)}K`;
|
|
6198
|
+
const millions = value / 1e6;
|
|
6199
|
+
return `${millions >= 100 ? String(Math.round(millions)) : String(Math.round(millions * 10) / 10)}M`;
|
|
6200
|
+
}
|
|
6201
|
+
/** Ring geometry matches the host ContextMeter: 14px viewBox, 2px stroke. */
|
|
6202
|
+
const RADIUS = 5.5;
|
|
6203
|
+
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
|
6204
|
+
function AcpContextMeter({ acp, sessionId, useSession }) {
|
|
6205
|
+
ensureAcpStyle();
|
|
6206
|
+
const ui = useUi$2(acp);
|
|
6207
|
+
const dshSessionId = String(sessionId);
|
|
6208
|
+
const running = useSession((snapshot) => snapshot.running);
|
|
6209
|
+
(0, react.useEffect)(() => {
|
|
6210
|
+
acp.loadChatSessions(dshSessionId);
|
|
6211
|
+
}, [
|
|
6212
|
+
acp,
|
|
6213
|
+
dshSessionId,
|
|
6214
|
+
running
|
|
6215
|
+
]);
|
|
6216
|
+
const context = contextOccupancy(acpMeterUsage(ui, dshSessionId));
|
|
6217
|
+
if (context === void 0) return null;
|
|
6218
|
+
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)}`);
|
|
6219
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6220
|
+
className: "ntr-acp-meter",
|
|
6221
|
+
role: "img",
|
|
6222
|
+
"aria-label": title,
|
|
6223
|
+
title,
|
|
6224
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
6225
|
+
viewBox: "0 0 14 14",
|
|
6226
|
+
width: "14",
|
|
6227
|
+
height: "14",
|
|
6228
|
+
"aria-hidden": "true",
|
|
6229
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
6230
|
+
className: "ntr-acp-meter-track",
|
|
6231
|
+
cx: "7",
|
|
6232
|
+
cy: "7",
|
|
6233
|
+
r: RADIUS
|
|
6234
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
6235
|
+
className: "ntr-acp-meter-fill",
|
|
6236
|
+
cx: "7",
|
|
6237
|
+
cy: "7",
|
|
6238
|
+
r: RADIUS,
|
|
6239
|
+
strokeDasharray: `${String(CIRCUMFERENCE * context.percent / 100)} ${String(CIRCUMFERENCE)}`,
|
|
6240
|
+
transform: "rotate(-90 7 7)"
|
|
6241
|
+
})]
|
|
6242
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6243
|
+
className: "ntr-acp-meter-percent",
|
|
6244
|
+
children: [context.percent, "%"]
|
|
6245
|
+
})]
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
5425
6248
|
const phases = /* @__PURE__ */ new Map();
|
|
5426
6249
|
/** The reveal serial whose ceremony has already played (or was skipped). */
|
|
5427
6250
|
const consumed = /* @__PURE__ */ new Map();
|
|
@@ -5714,8 +6537,8 @@ window.__ModuleLoader__.load({
|
|
|
5714
6537
|
if (agentId === void 0) return { efforts: [] };
|
|
5715
6538
|
const agentTitle = agents.find((agent) => agent.id === agentId)?.title ?? bound?.agentTitle ?? catalog?.title ?? agentId;
|
|
5716
6539
|
const pick = choice !== void 0 && choice.agentId === agentId ? choice : void 0;
|
|
5717
|
-
const modelId = bound?.modelId ?? pick?.modelId;
|
|
5718
|
-
const effort = bound?.reasoningEffort ?? pick?.effort;
|
|
6540
|
+
const modelId = bound?.released === true ? pick?.modelId ?? bound?.modelId : bound?.modelId ?? pick?.modelId;
|
|
6541
|
+
const effort = bound?.released === true ? pick?.effort ?? bound?.reasoningEffort : bound?.reasoningEffort ?? pick?.effort;
|
|
5719
6542
|
const reasoning = catalog?.reasoning;
|
|
5720
6543
|
const agentModel = catalog?.currentModel;
|
|
5721
6544
|
if (bound === void 0 && pick === void 0 && agentModel === void 0) return {
|
|
@@ -5894,9 +6717,6 @@ window.__ModuleLoader__.load({
|
|
|
5894
6717
|
agentListed,
|
|
5895
6718
|
agentId
|
|
5896
6719
|
]);
|
|
5897
|
-
const route = describeRoute(agents, bound, lockedTo === void 0 ? ui.defaultAgent : void 0, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
|
|
5898
|
-
if (!agentBaseHasData(bound, agents, ui.agentsLoaded)) return null;
|
|
5899
|
-
const routed = route.agentId !== void 0;
|
|
5900
6720
|
(0, react.useEffect)(() => {
|
|
5901
6721
|
if (lockedTo === void 0 || menu === null) return;
|
|
5902
6722
|
if (menu.sessionId === sessionId && menu.kind === "agent") closeAcpMenu();
|
|
@@ -5906,6 +6726,10 @@ window.__ModuleLoader__.load({
|
|
|
5906
6726
|
menu?.kind,
|
|
5907
6727
|
sessionId
|
|
5908
6728
|
]);
|
|
6729
|
+
const route = describeRoute(agents, bound, lockedTo === void 0 ? ui.defaultAgent : void 0, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
|
|
6730
|
+
if (!agentBaseHasData(bound, agents, ui.agentsLoaded)) return null;
|
|
6731
|
+
const routed = route.agentId !== void 0;
|
|
6732
|
+
if (ui.acpMode[sessionId] !== true) return null;
|
|
5909
6733
|
const shattering = lockPhase === "shatter" && lockedTo !== void 0;
|
|
5910
6734
|
const showAgentBase = !agentBaseHidden(lockedTo) || shattering;
|
|
5911
6735
|
const modelLabel = routed ? route.modelLabel : void 0;
|
|
@@ -6211,6 +7035,7 @@ window.__ModuleLoader__.load({
|
|
|
6211
7035
|
agentId
|
|
6212
7036
|
]);
|
|
6213
7037
|
if (menu === null || menu.sessionId !== sessionId) return null;
|
|
7038
|
+
if (ui.acpMode[sessionId] !== true) return null;
|
|
6214
7039
|
const route = describeRoute(agents, bound, lockedTo === void 0 ? ui.defaultAgent : void 0, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
|
|
6215
7040
|
let model;
|
|
6216
7041
|
try {
|
|
@@ -6655,6 +7480,19 @@ window.__ModuleLoader__.load({
|
|
|
6655
7480
|
return list.find((session) => !session.released) ?? list[0];
|
|
6656
7481
|
}
|
|
6657
7482
|
/**
|
|
7483
|
+
* Whether an empty stream means "history exists but is not shown yet": the
|
|
7484
|
+
* record was restored from disk after a host restart (the agent's replay has
|
|
7485
|
+
* not landed), or a resume cleared the buffer ahead of the replay. The view
|
|
7486
|
+
* then promises the replay instead of claiming the conversation never
|
|
7487
|
+
* happened — "还没有消息" to someone whose chat plane still shows yesterday's
|
|
7488
|
+
* turns is exactly the kind of contradiction this file's copy rules exist to
|
|
7489
|
+
* prevent. In-memory released sessions keep their buffer, so this only fires
|
|
7490
|
+
* when the buffer really is absent.
|
|
7491
|
+
*/
|
|
7492
|
+
function historyAwaitsReplay(session) {
|
|
7493
|
+
return session.released === true && session.hasHistory === true;
|
|
7494
|
+
}
|
|
7495
|
+
/**
|
|
6658
7496
|
* Everything the view can read that says "this conversation is set up on an
|
|
6659
7497
|
* agent": a live or parked ACP session, the host's binding (`session/chat-list`'s
|
|
6660
7498
|
* `boundAgentId`, which outlives a closed session), and the persisted pick.
|
|
@@ -6743,6 +7581,7 @@ window.__ModuleLoader__.load({
|
|
|
6743
7581
|
if (node === null || !pinned.current) return;
|
|
6744
7582
|
node.scrollTop = node.scrollHeight;
|
|
6745
7583
|
}, [blockCount, tail]);
|
|
7584
|
+
if (ui.acpMode[sessionId] !== true) return null;
|
|
6746
7585
|
if (transcript === void 0) {
|
|
6747
7586
|
const empty = acpEmptyCopy({
|
|
6748
7587
|
bound: boundAgent,
|
|
@@ -6854,7 +7693,7 @@ window.__ModuleLoader__.load({
|
|
|
6854
7693
|
},
|
|
6855
7694
|
children: transcript.blocks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6856
7695
|
className: "ntr-acp-empty-hint",
|
|
6857
|
-
children: t("还没有消息:在下面的输入框里发给它。", "Nothing yet — send it a message below.")
|
|
7696
|
+
children: historyAwaitsReplay(session) ? t("进程已释放;历史将在恢复后回放显示。", "The process is released — history replays once it is restored.") : t("还没有消息:在下面的输入框里发给它。", "Nothing yet — send it a message below.")
|
|
6858
7697
|
}) : transcript.blocks.map((block, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockView, {
|
|
6859
7698
|
block,
|
|
6860
7699
|
label: acp.label,
|
|
@@ -7058,6 +7897,219 @@ window.__ModuleLoader__.load({
|
|
|
7058
7897
|
});
|
|
7059
7898
|
}
|
|
7060
7899
|
//#endregion
|
|
7900
|
+
//#region src/client/released-banner.tsx
|
|
7901
|
+
/**
|
|
7902
|
+
* The conversation's released-session banner: one row above the composer
|
|
7903
|
+
* (`conversation.input.dock`) saying the agent process behind this
|
|
7904
|
+
* conversation has been released, with the explicit resume button.
|
|
7905
|
+
*
|
|
7906
|
+
* WHY A BANNER: releasing a process is deliberately silent (constraint 19 —
|
|
7907
|
+
* an idle teardown is the expected lifecycle, no `agent/notice`), and the
|
|
7908
|
+
* only other trace was the NTR view tab's status badge — a surface most
|
|
7909
|
+
* users never open. The "your next message resumes it" promise is real but
|
|
7910
|
+
* unverifiable from the chat itself, so the banner states it here and the
|
|
7911
|
+
* button turns it into a gesture (`session/chat-resume`), the explicit form
|
|
7912
|
+
* of the same recovery the next prompt performs lazily.
|
|
7913
|
+
*
|
|
7914
|
+
* The gate is a pure function ({@link releasedBannerVisible}) — the seam the
|
|
7915
|
+
* composer's other visibility decisions use, so the rules are testable with
|
|
7916
|
+
* no DOM. Like the agent tag and the context ring, an explicit
|
|
7917
|
+
* `acpMode === false` outranks a lingering `chatSessions` row: a conversation
|
|
7918
|
+
* that left NTR mode must not keep claiming a parked NTR process.
|
|
7919
|
+
*/
|
|
7920
|
+
/**
|
|
7921
|
+
* Whether the banner should render: this conversation's ACP session records
|
|
7922
|
+
* are all released — the process is parked (idle release or crash) and
|
|
7923
|
+
* nothing is live to serve the next message yet. An empty list (no agent has
|
|
7924
|
+
* run here) and a live row anywhere both mean "nothing to recover", so the
|
|
7925
|
+
* banner stays out of the way.
|
|
7926
|
+
*/
|
|
7927
|
+
function releasedBannerVisible(ui, dshSessionId) {
|
|
7928
|
+
if (ui.acpMode[dshSessionId] === false) return false;
|
|
7929
|
+
if (ui.chatSessionsFor !== dshSessionId) return false;
|
|
7930
|
+
return ui.chatSessions.length > 0 && ui.chatSessions.every((row) => row.released === true);
|
|
7931
|
+
}
|
|
7932
|
+
function ReleasedBanner({ acp, sessionId }) {
|
|
7933
|
+
ensureAcpStyle();
|
|
7934
|
+
const ui = useUi$2(acp);
|
|
7935
|
+
const dshSessionId = String(sessionId);
|
|
7936
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
7937
|
+
const [error, setError] = (0, react.useState)();
|
|
7938
|
+
(0, react.useEffect)(() => {
|
|
7939
|
+
acp.loadChatSessions(dshSessionId);
|
|
7940
|
+
}, [acp, dshSessionId]);
|
|
7941
|
+
if (!releasedBannerVisible(ui, dshSessionId)) return null;
|
|
7942
|
+
const resume = () => {
|
|
7943
|
+
if (busy) return;
|
|
7944
|
+
setBusy(true);
|
|
7945
|
+
setError(void 0);
|
|
7946
|
+
acp.resumeChatSession(dshSessionId).catch((failure) => {
|
|
7947
|
+
setError(failure instanceof Error ? failure.message : String(failure));
|
|
7948
|
+
}).finally(() => {
|
|
7949
|
+
setBusy(false);
|
|
7950
|
+
});
|
|
7951
|
+
};
|
|
7952
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7953
|
+
className: "ntr-released-banner",
|
|
7954
|
+
role: "region",
|
|
7955
|
+
"aria-label": acp.label("进程已释放", "Process released"),
|
|
7956
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7957
|
+
className: "ntr-released-banner-row",
|
|
7958
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7959
|
+
className: "ntr-released-banner-text",
|
|
7960
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7961
|
+
className: "ntr-released-banner-title",
|
|
7962
|
+
children: acp.label("进程已释放", "Process released")
|
|
7963
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7964
|
+
className: "ntr-released-banner-body",
|
|
7965
|
+
children: acp.label("上下文已保留;下一条消息将自动恢复,或点击【恢复进程】按钮。", "Context is kept — your next message resumes it, or click [Resume process].")
|
|
7966
|
+
})]
|
|
7967
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7968
|
+
type: "button",
|
|
7969
|
+
className: "ntr-acp-more is-primary",
|
|
7970
|
+
disabled: busy,
|
|
7971
|
+
title: acp.label("立即拉起 agent 进程并恢复这个会话", "Bring the agent process back and restore this session now"),
|
|
7972
|
+
onClick: resume,
|
|
7973
|
+
children: busy ? acp.label("正在恢复…", "Resuming…") : acp.label("恢复进程", "Resume process")
|
|
7974
|
+
})]
|
|
7975
|
+
}), error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7976
|
+
className: "ntr-permission-error",
|
|
7977
|
+
role: "alert",
|
|
7978
|
+
children: error
|
|
7979
|
+
})]
|
|
7980
|
+
});
|
|
7981
|
+
}
|
|
7982
|
+
//#endregion
|
|
7983
|
+
//#region src/client/branch-action.tsx
|
|
7984
|
+
/**
|
|
7985
|
+
* The NTR branch button (slot `conversation.chat.assistant-actions`): in an
|
|
7986
|
+
* ACP conversation this is the ONLY branch entry — the host's like / dislike /
|
|
7987
|
+
|
|
7988
|
+
* branch icons are hidden under `html[data-ntr-acp]` (see acp-ui.tsx), and
|
|
7989
|
+
* this button takes the host branch's place with the same icon, size and
|
|
7990
|
+
* row position (order 20, after the hidden feedback entry's order 10).
|
|
7991
|
+
*
|
|
7992
|
+
* Clicking runs the fixed two-stage branch: the HOST forks the DSH session
|
|
7993
|
+
* tree first (`sessions.fork`, structurally resolved), then the plugin adopts
|
|
7994
|
+
* the ACP side (`session/chat-fork-adopt` → the agent's own `session/fork`),
|
|
7995
|
+
* so the child conversation's agent actually remembers the parent context —
|
|
7996
|
+
* see the NTR message-actions plan, §D5/§D7.
|
|
7997
|
+
*
|
|
7998
|
+
* Availability is the store's `forkStateOf` six-state mapping over facts the
|
|
7999
|
+
* button never derives itself: the host's per-row `fork` verdict
|
|
8000
|
+
* (`session/chat-list`), the SSE running flag, the host-fork structural probe,
|
|
8001
|
+
* and the DOM tail verdict. The tail check reads the host's OWN
|
|
8002
|
+
* `[data-turn-tail]` marker (`data-actions-reveal === "always"` = the latest
|
|
8003
|
+
* completed turn) and fails CLOSED: a missing marker disables the button
|
|
8004
|
+
* rather than risking a fork cut at a point the user is not looking at.
|
|
8005
|
+
*/
|
|
8006
|
+
/**
|
|
8007
|
+
* The host's own branch-button icon, picked per host generation. The icon
|
|
8008
|
+
* exports MOVED between shipped hosts — desktop 0.2.1-rc.2 has
|
|
8009
|
+
* `IconBranchOutlineRegular` / `…Medium` but NOT `…16`, newer packages export
|
|
8010
|
+
* `…16` — and a hard import of one name compiles to a plain property read
|
|
8011
|
+
* (`primitives.IconBranchOutline16`), so the missing export is `undefined`
|
|
8012
|
+
* and `<undefined />` throws "Element type is invalid". The slot's error
|
|
8013
|
+
* boundary then abdicates the whole entry: no button, no tooltip, nothing in
|
|
8014
|
+
* any log — the exact silent disappearance this picker exists to prevent.
|
|
8015
|
+
* Rendering the button never depends on the icon: with all three absent the
|
|
8016
|
+
* click still works, only the 28px glyph is missing.
|
|
8017
|
+
*/
|
|
8018
|
+
function branchIconOf(source) {
|
|
8019
|
+
return source.IconBranchOutline16 ?? source.IconBranchOutlineRegular ?? source.IconBranchOutlineMedium;
|
|
8020
|
+
}
|
|
8021
|
+
const BranchIcon = branchIconOf(_deepseek_ai_dsh_client_ui_primitives);
|
|
8022
|
+
/** What the label reads for each state (zh / en pairs, per the plan's §D4). */
|
|
8023
|
+
function tooltipFor(state, label) {
|
|
8024
|
+
switch (state) {
|
|
8025
|
+
case "ready": return label("在新对话中分支", "Branch into a new conversation");
|
|
8026
|
+
case "unsupported": return label("该 agent 不支持原生分叉", "This agent cannot fork natively");
|
|
8027
|
+
case "released": return label("会话已挂起,先发一条消息恢复再分叉", "Resume this session before branching");
|
|
8028
|
+
case "busy": return label("当前轮次结束后才能分叉", "Wait for the current turn to finish");
|
|
8029
|
+
case "notTail": return label("仅可从已完成轮次的最后一条消息分支", "Available only on the last message of a completed turn");
|
|
8030
|
+
case "hostUnavailable": return label("当前宿主不提供分叉", "This host does not support branching");
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8033
|
+
function AcpBranchAction(props) {
|
|
8034
|
+
const ui = useUi$2(props.acp);
|
|
8035
|
+
const buttonRef = (0, react.useRef)(null);
|
|
8036
|
+
const [pending, setPending] = (0, react.useState)(false);
|
|
8037
|
+
/**
|
|
8038
|
+
* The host's own availability verdict for the turn tail this button sits
|
|
8039
|
+
* in, re-read after EVERY render (no dep array): the marker flips without
|
|
8040
|
+
* any store event of our own when a new turn lands, and before the first
|
|
8041
|
+
* layout pass the ref is not attached yet. Two facts, both the HOST's:
|
|
8042
|
+
* `data-actions-reveal === "always"` (this tail is the latest turn and
|
|
8043
|
+
* completed) and the host branch button's own `data-unavailable` (a stricter
|
|
8044
|
+
* gate — a tail whose final node is not the transcript's last, or a turn
|
|
8045
|
+
* with later nodes, is one the HOST refuses to fork). Reading the host's
|
|
8046
|
+
* button keeps the plugin's readiness a SUBSET of the host's: wherever the
|
|
8047
|
+
* two would disagree about the cut point, the plugin button disables, and
|
|
8048
|
+
* the remaining clicks fork at the same latest-completed prefix the host
|
|
8049
|
+
* would (the tail marked `always` is the latest completed turn, which is
|
|
8050
|
+
* exactly where the host's fallback `atSeq` lands).
|
|
8051
|
+
*/
|
|
8052
|
+
const [tailReady, setTailReady] = (0, react.useState)(false);
|
|
8053
|
+
(0, react.useLayoutEffect)(() => {
|
|
8054
|
+
const tail = buttonRef.current?.closest("[data-turn-tail]");
|
|
8055
|
+
const hostBranch = tail?.querySelector("button[aria-label=\"在新对话中分支\"]:not([data-ntr-branch]), button[aria-label=\"Branch into a new conversation\"]:not([data-ntr-branch])");
|
|
8056
|
+
setTailReady(tail?.getAttribute("data-actions-reveal") === "always" && hostBranch?.hasAttribute("data-unavailable") !== true);
|
|
8057
|
+
});
|
|
8058
|
+
/**
|
|
8059
|
+
* The click's local failure line. The store ALSO flashes the same reason
|
|
8060
|
+
* into the settings banner, but the user is HERE — without this row the
|
|
8061
|
+
* click would "do nothing" visibly, the exact invisibility the permission
|
|
8062
|
+
* dock's pinned-error pattern exists to prevent.
|
|
8063
|
+
*/
|
|
8064
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
8065
|
+
if (ui.acpMode[props.sessionId] !== true) return null;
|
|
8066
|
+
if (ui.chatSessionsFor !== props.sessionId) return null;
|
|
8067
|
+
const row = ui.chatSessionsFor === props.sessionId ? ui.chatSessions[0] : void 0;
|
|
8068
|
+
const forkFace = props.hostFork?.();
|
|
8069
|
+
const liveRunning = row !== void 0 && ui.sessions.some((s) => s.sessionId === row.sessionId && s.running);
|
|
8070
|
+
const state = forkStateOf(row, forkFace !== void 0, tailReady, liveRunning);
|
|
8071
|
+
const disabled = state !== "ready" || pending;
|
|
8072
|
+
const branchLabel = props.acp.label("在新对话中分支", "Branch into a new conversation");
|
|
8073
|
+
const onClick = () => {
|
|
8074
|
+
if (state !== "ready" || pending) return;
|
|
8075
|
+
setPending(true);
|
|
8076
|
+
setFailure(void 0);
|
|
8077
|
+
props.acp.forkChatSession(props.sessionId, (sessionId) => {
|
|
8078
|
+
const face = props.hostFork?.();
|
|
8079
|
+
if (face === void 0) throw new Error("acp: host fork service is gone");
|
|
8080
|
+
return face.fork({
|
|
8081
|
+
sessionId,
|
|
8082
|
+
increaseTitle: true
|
|
8083
|
+
});
|
|
8084
|
+
}).then((outcome) => {
|
|
8085
|
+
if ("childId" in outcome) props.openSession?.(outcome.childId);
|
|
8086
|
+
else setFailure(outcome.failure);
|
|
8087
|
+
}).finally(() => {
|
|
8088
|
+
setPending(false);
|
|
8089
|
+
});
|
|
8090
|
+
};
|
|
8091
|
+
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, {
|
|
8092
|
+
label: tooltipFor(state, props.acp.label),
|
|
8093
|
+
side: "bottom",
|
|
8094
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8095
|
+
ref: buttonRef,
|
|
8096
|
+
type: "button",
|
|
8097
|
+
className: "ntr-branch-action",
|
|
8098
|
+
"aria-label": branchLabel,
|
|
8099
|
+
"aria-disabled": disabled || void 0,
|
|
8100
|
+
"data-ntr-branch": "",
|
|
8101
|
+
"data-unavailable": disabled || void 0,
|
|
8102
|
+
onClick: disabled ? void 0 : onClick,
|
|
8103
|
+
children: BranchIcon === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BranchIcon, {})
|
|
8104
|
+
})
|
|
8105
|
+
}), failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8106
|
+
className: "ntr-branch-error",
|
|
8107
|
+
role: "alert",
|
|
8108
|
+
title: failure,
|
|
8109
|
+
children: failure
|
|
8110
|
+
})] });
|
|
8111
|
+
}
|
|
8112
|
+
//#endregion
|
|
7061
8113
|
//#region src/client/model-seat.tsx
|
|
7062
8114
|
/** Exact key of the composer's model seat (a `single`, session-scoped slot). */
|
|
7063
8115
|
const MODEL_SEAT_KEY = "conversation.input.model";
|
|
@@ -7260,8 +8312,8 @@ window.__ModuleLoader__.load({
|
|
|
7260
8312
|
* caller reads as "not ACP" (fail-open: an ordinary conversation must never
|
|
7261
8313
|
* lose its native commands to a guess).
|
|
7262
8314
|
*/
|
|
7263
|
-
function slashAcpVerdict(snapshot, sessionId, verdict) {
|
|
7264
|
-
const staged = stagedSessionOf(snapshot);
|
|
8315
|
+
function slashAcpVerdict(snapshot, sessionId, verdict, currentSessionId) {
|
|
8316
|
+
const staged = stagedSessionOf(snapshot, currentSessionId);
|
|
7265
8317
|
if (staged !== void 0 && staged.sessionId === sessionId) {
|
|
7266
8318
|
const listed = rowAgentPreset(staged.row);
|
|
7267
8319
|
if (listed !== void 0) return listed === "acp";
|
|
@@ -7606,6 +8658,37 @@ window.__ModuleLoader__.load({
|
|
|
7606
8658
|
const inject = ["slots", "connection"];
|
|
7607
8659
|
const browserPrefersZh = typeof navigator !== "undefined" && /^zh\b/i.test(navigator.language);
|
|
7608
8660
|
/**
|
|
8661
|
+
* The NTR scope marker (`html[data-ntr-acp]`): the ONLY hook the message-action
|
|
8662
|
+
* hiding CSS keys on, so the host's like / dislike / branch icons disappear in
|
|
8663
|
+
* exactly the conversations this gate has confirmed to run the ACP preset —
|
|
8664
|
+
* and reappear the moment the evidence stops answering (every `standDown`
|
|
8665
|
+
* clears it; non-ACP sessions never see it). Idempotent per call.
|
|
8666
|
+
*/
|
|
8667
|
+
const setScopeMarker = (active) => {
|
|
8668
|
+
if (typeof document === "undefined") return;
|
|
8669
|
+
document.documentElement.toggleAttribute("data-ntr-acp", active);
|
|
8670
|
+
};
|
|
8671
|
+
/**
|
|
8672
|
+
* One-shot sanity probe for the aria-label hiding: the host's action buttons
|
|
8673
|
+
* carry LOCALIZED labels and no data-* hook, so a copy change (or a third
|
|
8674
|
+
* language) silently un-hides them while everything else keeps working. Once
|
|
8675
|
+
* the marker is on and a turn tail is actually on screen, check that at least
|
|
8676
|
+
* one label the rules key on still exists and leave one console line for
|
|
8677
|
+
* whoever debugs "the icons came back" — dev-visible, never user UI.
|
|
8678
|
+
*/
|
|
8679
|
+
let probedHostLabels = false;
|
|
8680
|
+
const probeHostActionLabels = () => {
|
|
8681
|
+
if (probedHostLabels || typeof document === "undefined") return;
|
|
8682
|
+
if (document.querySelector("[data-turn-tail]") === null) return;
|
|
8683
|
+
probedHostLabels = true;
|
|
8684
|
+
if (document.querySelector([
|
|
8685
|
+
"button:not([data-ntr-branch])[aria-label=\"在新对话中分支\"]",
|
|
8686
|
+
"button:not([data-ntr-branch])[aria-label=\"Branch into a new conversation\"]",
|
|
8687
|
+
"button[aria-label=\"好的回答\"]",
|
|
8688
|
+
"button[aria-label=\"Good response\"]"
|
|
8689
|
+
].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");
|
|
8690
|
+
};
|
|
8691
|
+
/**
|
|
7609
8692
|
* How long a host verdict may stand as the gate's ONLY fact source before the
|
|
7610
8693
|
* gate asks the host again.
|
|
7611
8694
|
*
|
|
@@ -7681,11 +8764,32 @@ window.__ModuleLoader__.load({
|
|
|
7681
8764
|
return;
|
|
7682
8765
|
}
|
|
7683
8766
|
};
|
|
8767
|
+
/**
|
|
8768
|
+
* The runtime's `sessions.fork`, resolved per call: `undefined` renders the
|
|
8769
|
+
* branch button disabled (`hostUnavailable`) instead of hiding it, so a
|
|
8770
|
+
* host that gains the primitive later is picked up on the next render.
|
|
8771
|
+
*/
|
|
8772
|
+
const hostFork = () => {
|
|
8773
|
+
try {
|
|
8774
|
+
const fork = service("sessions")?.fork;
|
|
8775
|
+
return typeof fork === "function" ? { fork } : void 0;
|
|
8776
|
+
} catch {
|
|
8777
|
+
return;
|
|
8778
|
+
}
|
|
8779
|
+
};
|
|
8780
|
+
/** Focus a session in the workspace (the branch click's last step). */
|
|
8781
|
+
const openSession = (sessionId) => {
|
|
8782
|
+
try {
|
|
8783
|
+
service("uiWorkspace")?.openSession?.(sessionId);
|
|
8784
|
+
} catch {}
|
|
8785
|
+
};
|
|
7684
8786
|
/** The injected face every ACP conversation entry receives. */
|
|
7685
8787
|
const chatFace = (sessionId) => ({
|
|
7686
8788
|
acp: store,
|
|
7687
8789
|
sessionId: String(sessionId),
|
|
7688
|
-
faceFor
|
|
8790
|
+
faceFor,
|
|
8791
|
+
hostFork,
|
|
8792
|
+
openSession
|
|
7689
8793
|
});
|
|
7690
8794
|
ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
7691
8795
|
name: "settings.section",
|
|
@@ -7724,6 +8828,18 @@ window.__ModuleLoader__.load({
|
|
|
7724
8828
|
order: 10,
|
|
7725
8829
|
inject: (sessionId) => chatFace(sessionId)
|
|
7726
8830
|
}, PermissionDock)), "dsh-ntr: conversation permission dock");
|
|
8831
|
+
ctx.effect(() => ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
8832
|
+
name: "conversation.input.dock",
|
|
8833
|
+
id: "acp-released-banner",
|
|
8834
|
+
order: 20,
|
|
8835
|
+
inject: (sessionId) => chatFace(sessionId)
|
|
8836
|
+
}, ReleasedBanner)), "dsh-ntr: released-session banner");
|
|
8837
|
+
ctx.effect(() => ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
|
|
8838
|
+
name: "conversation.chat.assistant-actions",
|
|
8839
|
+
id: "ntr-branch",
|
|
8840
|
+
order: 20,
|
|
8841
|
+
inject: (sessionId) => chatFace(sessionId)
|
|
8842
|
+
}, AcpBranchAction)), "dsh-ntr: message branch action");
|
|
7727
8843
|
/**
|
|
7728
8844
|
* The client runtime's session list store, read structurally. Used to find
|
|
7729
8845
|
* the session the composer is staged on and whether it runs the ACP preset —
|
|
@@ -7735,6 +8851,28 @@ window.__ModuleLoader__.load({
|
|
|
7735
8851
|
return list;
|
|
7736
8852
|
};
|
|
7737
8853
|
/**
|
|
8854
|
+
* The renderer's current Session scope observable, when this host exposes it.
|
|
8855
|
+
* This is the source the host's own main-view picker prefers; the list's
|
|
8856
|
+
* retained rows are only the fallback for hosts without the adapter.
|
|
8857
|
+
*/
|
|
8858
|
+
const currentScopeSource = () => {
|
|
8859
|
+
try {
|
|
8860
|
+
const current = service("uiSession")?.adapter?.current;
|
|
8861
|
+
return current !== void 0 && typeof current.getSnapshot === "function" ? current : void 0;
|
|
8862
|
+
} catch {
|
|
8863
|
+
return;
|
|
8864
|
+
}
|
|
8865
|
+
};
|
|
8866
|
+
/** The session the composer is actually staged on, if the runtime can say. */
|
|
8867
|
+
const currentScopeSessionId = () => {
|
|
8868
|
+
try {
|
|
8869
|
+
const key = currentScopeSource()?.getSnapshot?.()?.key;
|
|
8870
|
+
return typeof key === "string" && key !== "" ? key : void 0;
|
|
8871
|
+
} catch {
|
|
8872
|
+
return;
|
|
8873
|
+
}
|
|
8874
|
+
};
|
|
8875
|
+
/**
|
|
7738
8876
|
* Chat session ids the ACP-mode tracker has already asked the host about (the
|
|
7739
8877
|
* `session/chat-list` fallback), mapped to WHEN the last ask went out. The
|
|
7740
8878
|
* ask is once per id so a host whose answer carries no `acpMode` cannot turn
|
|
@@ -7759,6 +8897,8 @@ window.__ModuleLoader__.load({
|
|
|
7759
8897
|
const acpModeVerdict = (sessionId) => store.getSnapshot().acpMode[sessionId];
|
|
7760
8898
|
ctx.effect(() => {
|
|
7761
8899
|
let stopWatch;
|
|
8900
|
+
/** Subscription to the renderer's current Session binding (see `currentScopeSource`). */
|
|
8901
|
+
let stopCurrent;
|
|
7762
8902
|
/**
|
|
7763
8903
|
* The claim on the host's model seat, held while the staged conversation
|
|
7764
8904
|
* runs the ACP preset. That cell is where the host's own base sits (the
|
|
@@ -7795,6 +8935,50 @@ window.__ModuleLoader__.load({
|
|
|
7795
8935
|
*/
|
|
7796
8936
|
let permissionClaim;
|
|
7797
8937
|
/**
|
|
8938
|
+
* ACP-only auxiliary entries: the NTR view tab and the composer's context
|
|
8939
|
+
* ring. Unlike the two seats these are `list` slots with no visibility
|
|
8940
|
+
* predicate of their own, so "self-hide in the component" cannot remove a
|
|
8941
|
+
* tab from the header or free the native bottom row. They are installed by
|
|
8942
|
+
* this gate for exactly the same lifetime as the claims and disposed on
|
|
8943
|
+
* every stand-down — a standard conversation then has no NTR tab at all,
|
|
8944
|
+
* and the host's native context meter sits alone in its dock row.
|
|
8945
|
+
*/
|
|
8946
|
+
let viewEntry;
|
|
8947
|
+
let meterEntry;
|
|
8948
|
+
const releaseAuxEntries = () => {
|
|
8949
|
+
try {
|
|
8950
|
+
viewEntry?.();
|
|
8951
|
+
} catch {}
|
|
8952
|
+
viewEntry = void 0;
|
|
8953
|
+
try {
|
|
8954
|
+
meterEntry?.();
|
|
8955
|
+
} catch {}
|
|
8956
|
+
meterEntry = void 0;
|
|
8957
|
+
};
|
|
8958
|
+
const holdAuxEntries = () => {
|
|
8959
|
+
if (viewEntry === void 0) try {
|
|
8960
|
+
viewEntry = ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
8961
|
+
name: "conversation.view",
|
|
8962
|
+
id: "acp",
|
|
8963
|
+
order: 20,
|
|
8964
|
+
label: () => label("NTR", "NTR"),
|
|
8965
|
+
inject: (sessionId) => chatFace(sessionId)
|
|
8966
|
+
}, AcpConversationView));
|
|
8967
|
+
} catch {
|
|
8968
|
+
viewEntry = void 0;
|
|
8969
|
+
}
|
|
8970
|
+
if (meterEntry === void 0) try {
|
|
8971
|
+
meterEntry = ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
8972
|
+
name: "conversation.composer.dock",
|
|
8973
|
+
id: "acp-context-meter",
|
|
8974
|
+
order: 10,
|
|
8975
|
+
inject: (sessionId) => chatFace(sessionId)
|
|
8976
|
+
}, AcpContextMeter));
|
|
8977
|
+
} catch {
|
|
8978
|
+
meterEntry = void 0;
|
|
8979
|
+
}
|
|
8980
|
+
};
|
|
8981
|
+
/**
|
|
7798
8982
|
* Last diagnostic line from {@link syncAcpMode}. Every way this gate can
|
|
7799
8983
|
* end with the native seat still on screen used to be silent — an inner
|
|
7800
8984
|
* `catch { return [] }` and an outer `catch { … }` between them swallowed
|
|
@@ -7806,7 +8990,8 @@ window.__ModuleLoader__.load({
|
|
|
7806
8990
|
let seatDiag = "idle";
|
|
7807
8991
|
let permissionSeatDiag = "idle";
|
|
7808
8992
|
/**
|
|
7809
|
-
* Hand
|
|
8993
|
+
* Hand every ACP-only surface back: both composer cells, the NTR view tab
|
|
8994
|
+
* and the context ring.
|
|
7810
8995
|
*
|
|
7811
8996
|
* Every path that is not "this staged conversation is CONFIRMED to run the
|
|
7812
8997
|
* ACP preset" ends here — including the paths that carry no verdict at all
|
|
@@ -7823,6 +9008,8 @@ window.__ModuleLoader__.load({
|
|
|
7823
9008
|
seatDiag = reason;
|
|
7824
9009
|
permissionSeatDiag = `${reason} (released)`;
|
|
7825
9010
|
disarmVerdictReask();
|
|
9011
|
+
setScopeMarker(false);
|
|
9012
|
+
releaseAuxEntries();
|
|
7826
9013
|
if (claim !== void 0) {
|
|
7827
9014
|
claim.release();
|
|
7828
9015
|
claim = void 0;
|
|
@@ -7862,7 +9049,8 @@ window.__ModuleLoader__.load({
|
|
|
7862
9049
|
standDown("no session list service");
|
|
7863
9050
|
return;
|
|
7864
9051
|
}
|
|
7865
|
-
const
|
|
9052
|
+
const current = currentScopeSessionId();
|
|
9053
|
+
const staged = stagedSessionOf(snapshot, current);
|
|
7866
9054
|
if (staged === void 0) {
|
|
7867
9055
|
standDown("no staged session (nothing retains the main view)");
|
|
7868
9056
|
return;
|
|
@@ -7878,12 +9066,18 @@ window.__ModuleLoader__.load({
|
|
|
7878
9066
|
acpModeAsked.set(staged.sessionId, Date.now());
|
|
7879
9067
|
store.loadChatSessions(staged.sessionId, verdictHeld ? { settleVerdict: true } : void 0);
|
|
7880
9068
|
} else if (!verdictHeld && askedAt !== void 0) acpModeAsked.delete(staged.sessionId);
|
|
7881
|
-
if (!stagedIsAcpMode(snapshot, acpModeVerdict)) {
|
|
9069
|
+
if (!stagedIsAcpMode(snapshot, acpModeVerdict, current)) {
|
|
9070
|
+
if (rowAgentPreset(staged.row) !== void 0) store.noteAcpMode(staged.sessionId, false);
|
|
7882
9071
|
standDown(`staged ${staged.sessionId} is not in ACP mode`);
|
|
7883
9072
|
return;
|
|
7884
9073
|
}
|
|
7885
|
-
if (rowAgentPreset(staged.row) !== void 0)
|
|
9074
|
+
if (rowAgentPreset(staged.row) !== void 0) {
|
|
9075
|
+
store.noteAcpMode(staged.sessionId, rowAgentPreset(staged.row) === "acp");
|
|
9076
|
+
disarmVerdictReask();
|
|
9077
|
+
}
|
|
7886
9078
|
seatDiag = `staged ${staged.sessionId} is ACP mode`;
|
|
9079
|
+
setScopeMarker(true);
|
|
9080
|
+
probeHostActionLabels();
|
|
7887
9081
|
claim ??= createModelSeatClaim({
|
|
7888
9082
|
slots: ctx.slots,
|
|
7889
9083
|
occupant: () => AcpSessionControls,
|
|
@@ -7901,6 +9095,7 @@ window.__ModuleLoader__.load({
|
|
|
7901
9095
|
}
|
|
7902
9096
|
});
|
|
7903
9097
|
permissionClaim.sync();
|
|
9098
|
+
holdAuxEntries();
|
|
7904
9099
|
if (rowAgentPreset(staged.row) === void 0) armVerdictReask();
|
|
7905
9100
|
};
|
|
7906
9101
|
let stopPreset;
|
|
@@ -7922,6 +9117,12 @@ window.__ModuleLoader__.load({
|
|
|
7922
9117
|
const attach = () => {
|
|
7923
9118
|
const list = sessionsList();
|
|
7924
9119
|
if (list !== void 0 && stopWatch === void 0) stopWatch = list.subscribe(runGate);
|
|
9120
|
+
const current = currentScopeSource();
|
|
9121
|
+
if (current?.subscribe !== void 0 && stopCurrent === void 0) try {
|
|
9122
|
+
stopCurrent = current.subscribe(runGate);
|
|
9123
|
+
} catch {
|
|
9124
|
+
stopCurrent = void 0;
|
|
9125
|
+
}
|
|
7925
9126
|
attachPresetFrames();
|
|
7926
9127
|
runGate();
|
|
7927
9128
|
};
|
|
@@ -7974,7 +9175,10 @@ window.__ModuleLoader__.load({
|
|
|
7974
9175
|
stopCell();
|
|
7975
9176
|
stopPreset?.();
|
|
7976
9177
|
stopWatch?.();
|
|
9178
|
+
stopCurrent?.();
|
|
7977
9179
|
disarmVerdictReask();
|
|
9180
|
+
setScopeMarker(false);
|
|
9181
|
+
releaseAuxEntries();
|
|
7978
9182
|
if (claim !== void 0) {
|
|
7979
9183
|
claim.release();
|
|
7980
9184
|
claim = void 0;
|
|
@@ -8013,9 +9217,9 @@ window.__ModuleLoader__.load({
|
|
|
8013
9217
|
const sources = triggers.live?.sources;
|
|
8014
9218
|
return Array.isArray(sources) ? sources : void 0;
|
|
8015
9219
|
},
|
|
8016
|
-
verdict: (sessionId) => slashAcpVerdict(sessionsList()?.getSnapshot(), sessionId, (id) => store.getSnapshot().acpMode[id]),
|
|
9220
|
+
verdict: (sessionId) => slashAcpVerdict(sessionsList()?.getSnapshot(), sessionId, (id) => store.getSnapshot().acpMode[id], currentScopeSessionId()),
|
|
8017
9221
|
ensureVerdict: async (sessionId) => {
|
|
8018
|
-
const held = () => slashAcpVerdict(sessionsList()?.getSnapshot(), sessionId, (id) => store.getSnapshot().acpMode[id]);
|
|
9222
|
+
const held = () => slashAcpVerdict(sessionsList()?.getSnapshot(), sessionId, (id) => store.getSnapshot().acpMode[id], currentScopeSessionId());
|
|
8019
9223
|
const known = held();
|
|
8020
9224
|
if (known !== void 0) return known;
|
|
8021
9225
|
await store.loadChatSessions(sessionId);
|
|
@@ -8078,13 +9282,6 @@ window.__ModuleLoader__.load({
|
|
|
8078
9282
|
store.noteSlashDiag("idle");
|
|
8079
9283
|
};
|
|
8080
9284
|
}, "dsh-ntr: slash isolation");
|
|
8081
|
-
ctx.effect(() => ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
8082
|
-
name: "conversation.view",
|
|
8083
|
-
id: "acp",
|
|
8084
|
-
order: 20,
|
|
8085
|
-
label: () => "NTR",
|
|
8086
|
-
inject: (sessionId) => chatFace(sessionId)
|
|
8087
|
-
}, AcpConversationView)), "dsh-ntr: ACP view tab");
|
|
8088
9285
|
}
|
|
8089
9286
|
//#endregion
|
|
8090
9287
|
exports.apply = apply;
|