dsh-ntr 0.3.2 → 0.3.4
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 +2 -0
- package/README.md +2 -0
- package/lib/bridge.js +1930 -60
- package/lib/client/activity-pill.js +156 -0
- package/lib/client/branch-action.js +16 -0
- package/lib/client/index.js +44 -4
- package/lib/client/model-seat.js +8 -2
- package/lib/client/store.js +32 -0
- package/lib/client.js +228 -5
- package/lib/client.js.map +1 -1
- package/lib/impl.js +1281 -23
- package/lib/index.js +27 -0
- package/lib/tps.js +32 -0
- package/lib/types/bridge.d.ts +636 -12
- package/lib/types/client/activity-pill.d.ts +81 -0
- package/lib/types/client/branch-action.d.ts +8 -1
- package/lib/types/client/model-seat.d.ts +21 -0
- package/lib/types/client/store.d.ts +10 -0
- package/lib/types/impl.d.ts +61 -6
- package/lib/types/protocol.d.ts +22 -0
- package/lib/types/tps.d.ts +17 -0
- package/package.json +1 -1
- package/src/bridge.ts +2216 -60
- package/src/client/activity-pill.tsx +201 -0
- package/src/client/branch-action.tsx +19 -1
- package/src/client/index.tsx +36 -4
- package/src/client/model-seat.tsx +28 -3
- package/src/client/store.ts +37 -0
- package/src/impl.ts +1268 -27
- package/src/index.ts +25 -0
- package/src/protocol.ts +24 -0
- package/src/tps.ts +40 -0
package/lib/client.js
CHANGED
|
@@ -401,6 +401,7 @@ window.__ModuleLoader__.load({
|
|
|
401
401
|
probingModels: {},
|
|
402
402
|
probeErrors: {},
|
|
403
403
|
chatSessions: [],
|
|
404
|
+
burstStats: {},
|
|
404
405
|
tagHalo: { ...DEFAULT_TAG_HALO },
|
|
405
406
|
approvalDeadlines: void 0,
|
|
406
407
|
idleRelease: void 0,
|
|
@@ -1792,11 +1793,18 @@ window.__ModuleLoader__.load({
|
|
|
1792
1793
|
const closedTranscripts = { ...this.state.chatTranscripts };
|
|
1793
1794
|
delete closedTranscripts[event.sessionId];
|
|
1794
1795
|
const chatSessions = this.state.chatSessions.filter((s) => s.sessionId !== event.sessionId);
|
|
1796
|
+
const hadBurst = this.state.burstStats[event.sessionId] !== void 0;
|
|
1797
|
+
let burstStats = this.state.burstStats;
|
|
1798
|
+
if (hadBurst) {
|
|
1799
|
+
burstStats = { ...burstStats };
|
|
1800
|
+
delete burstStats[event.sessionId];
|
|
1801
|
+
}
|
|
1795
1802
|
this.mutate({
|
|
1796
1803
|
sessions,
|
|
1797
1804
|
activeSessionId: active,
|
|
1798
1805
|
chatTranscripts: closedTranscripts,
|
|
1799
1806
|
...chatSessions.length !== this.state.chatSessions.length ? { chatSessions } : {},
|
|
1807
|
+
...hadBurst ? { burstStats } : {},
|
|
1800
1808
|
permissions: this.state.permissions.filter((p) => p.sessionId !== event.sessionId),
|
|
1801
1809
|
...event.reason === "agent-died" ? this.logPatch("error", this.locale("会话因 agent 进程退出而终止", "session ended: agent process died"), closingSession?.agentId) : {}
|
|
1802
1810
|
});
|
|
@@ -1918,6 +1926,20 @@ window.__ModuleLoader__.load({
|
|
|
1918
1926
|
});
|
|
1919
1927
|
return Promise.resolve();
|
|
1920
1928
|
}
|
|
1929
|
+
case "turn/metrics": {
|
|
1930
|
+
const prior = this.state.burstStats[event.sessionId];
|
|
1931
|
+
if (event.final === false && prior !== void 0 && prior.final === true && prior.turnId === event.turnId) return Promise.resolve();
|
|
1932
|
+
this.mutate({ burstStats: {
|
|
1933
|
+
...this.state.burstStats,
|
|
1934
|
+
[event.sessionId]: {
|
|
1935
|
+
outputTokens: event.outputTokens,
|
|
1936
|
+
burstMs: event.burstMs,
|
|
1937
|
+
turnId: event.turnId,
|
|
1938
|
+
final: event.final
|
|
1939
|
+
}
|
|
1940
|
+
} });
|
|
1941
|
+
return Promise.resolve();
|
|
1942
|
+
}
|
|
1921
1943
|
case "turn/start": {
|
|
1922
1944
|
this.openTurns.add(event.sessionId);
|
|
1923
1945
|
this.markWatchedRunning(event.sessionId, true);
|
|
@@ -8004,6 +8026,21 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
8004
8026
|
* rather than risking a fork cut at a point the user is not looking at.
|
|
8005
8027
|
*/
|
|
8006
8028
|
/**
|
|
8029
|
+
* Resolves the runtime's `sessions.fork` primitive, binding it to the service
|
|
8030
|
+
* instance so that internal `this` accesses (such as `this.list.getSnapshot()`)
|
|
8031
|
+
* resolve correctly when called through `face.fork(...)`.
|
|
8032
|
+
* Returns `undefined` if the service or `fork` method is absent or not a function.
|
|
8033
|
+
*/
|
|
8034
|
+
function resolveHostFork(sessionsService) {
|
|
8035
|
+
try {
|
|
8036
|
+
const sessions = sessionsService;
|
|
8037
|
+
const fork = sessions?.fork;
|
|
8038
|
+
return typeof fork === "function" ? { fork: fork.bind(sessions) } : void 0;
|
|
8039
|
+
} catch {
|
|
8040
|
+
return;
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
8043
|
+
/**
|
|
8007
8044
|
* The host's own branch-button icon, picked per host generation. The icon
|
|
8008
8045
|
* exports MOVED between shipped hosts — desktop 0.2.1-rc.2 has
|
|
8009
8046
|
* `IconBranchOutlineRegular` / `…Medium` but NOT `…16`, newer packages export
|
|
@@ -8132,7 +8169,7 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
8132
8169
|
* `entriesOfSlot` and performing bounded retakes if displaced by later registrations.
|
|
8133
8170
|
*/
|
|
8134
8171
|
function createSeatClaim(options) {
|
|
8135
|
-
const { slots, slotKey, basePriority, occupant, inject } = options;
|
|
8172
|
+
const { slots, slotKey, basePriority, occupant, inject, entryId } = options;
|
|
8136
8173
|
const label = options.label ?? slotKey;
|
|
8137
8174
|
const onDiag = options.onDiag;
|
|
8138
8175
|
const maxClaims = options.maxClaims ?? 4;
|
|
@@ -8162,10 +8199,11 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
8162
8199
|
};
|
|
8163
8200
|
try {
|
|
8164
8201
|
const heads = slots.entriesOfSlot(slotKey) ?? [];
|
|
8202
|
+
const entry = entryId === void 0 ? heads[0] : heads.find((head) => head.options?.id === entryId);
|
|
8165
8203
|
return {
|
|
8166
8204
|
declared: isDeclared,
|
|
8167
8205
|
verified: true,
|
|
8168
|
-
...
|
|
8206
|
+
...entry === void 0 ? {} : { entry }
|
|
8169
8207
|
};
|
|
8170
8208
|
} catch {
|
|
8171
8209
|
return {
|
|
@@ -8186,6 +8224,9 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
8186
8224
|
try {
|
|
8187
8225
|
installed.push(slots.inject(slotKey, () => slots.register({
|
|
8188
8226
|
name: slotKey,
|
|
8227
|
+
...entryId === void 0 ? {} : { id: entryId },
|
|
8228
|
+
...options.order === void 0 ? {} : { order: options.order },
|
|
8229
|
+
...options.locale === void 0 ? {} : { locale: options.locale },
|
|
8189
8230
|
priority: rank,
|
|
8190
8231
|
...inject ? { inject } : {}
|
|
8191
8232
|
}, component)));
|
|
@@ -8297,6 +8338,159 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
8297
8338
|
});
|
|
8298
8339
|
}
|
|
8299
8340
|
//#endregion
|
|
8341
|
+
//#region src/client/activity-pill.tsx
|
|
8342
|
+
/**
|
|
8343
|
+
* The composer dock's `x轮x步 · xx tok/s` pill on an ACP conversation.
|
|
8344
|
+
*
|
|
8345
|
+
* WHY SHADOW INSTEAD OF A SECOND PILL: the host's `ActivityPill` already states
|
|
8346
|
+
* the two things this conversation has — turn/step counts and a whole-session
|
|
8347
|
+
* decode speed — in the one place a speed belongs. Only its speed is wrong for
|
|
8348
|
+
* ACP: the host divides the tokens a step emitted by `firstTokenTime →
|
|
8349
|
+
* completedTime`, and that window contains every tool wait. Rendering a second
|
|
8350
|
+
* speed beside it would leave two numbers on screen and no way to tell which
|
|
8351
|
+
* is the honest one. So this module takes the host pill's own cell (`list` slot
|
|
8352
|
+
* `conversation.composer.dock`, id `activity`) at a lower priority, renders the
|
|
8353
|
+
* HOST's component, and hands it a patched `sessionStats` whose decode fields
|
|
8354
|
+
* are the plugin's burst aggregate. Style, counts, dialog, aria and locale stay
|
|
8355
|
+
* the host's; only the number changes.
|
|
8356
|
+
*
|
|
8357
|
+
* NON-ACP CONVERSATIONS PASS THROUGH. The shadow is installed only while the
|
|
8358
|
+
* gate confirms the staged conversation runs the ACP preset, but a list entry
|
|
8359
|
+
* is projected into EVERY session's composer. The wrapper therefore renders the
|
|
8360
|
+
* host component with the ORIGINAL projection whenever `acpMode` is not
|
|
8361
|
+
* affirmatively true, making the shadow visually indistinguishable from the
|
|
8362
|
+
* shipped entry there.
|
|
8363
|
+
*
|
|
8364
|
+
* FAIL-OPEN IS STRUCTURAL: if the host entry, its inject face, its locale or
|
|
8365
|
+
* its `performanceUsage` source is missing, no claim is created and the native
|
|
8366
|
+
* pill stays exactly as shipped. If the host component rejects the patched
|
|
8367
|
+
* projection, the slot's error boundary retires this entry and the native
|
|
8368
|
+
* `activity` entry wins again. No CSS hiding and no DOM patching are involved.
|
|
8369
|
+
*/
|
|
8370
|
+
const DOCK_SLOT = "conversation.composer.dock";
|
|
8371
|
+
const ACTIVITY_ENTRY_ID = "activity";
|
|
8372
|
+
/**
|
|
8373
|
+
* The aggregate the dock should show: the sum of every ACP session backing this
|
|
8374
|
+
* chat (the host pill's counts are whole-chat too), with a live `turn/metrics`
|
|
8375
|
+
* entry preferred per session over its persisted row. `undefined` means "no
|
|
8376
|
+
* measurable burst yet" — the wrapper then hides the speed instead of showing
|
|
8377
|
+
* the host's tool-inclusive number.
|
|
8378
|
+
*/
|
|
8379
|
+
function activityBurst(ui, dshSessionId) {
|
|
8380
|
+
if (ui.chatSessionsFor !== dshSessionId) return void 0;
|
|
8381
|
+
let outputTokens = 0;
|
|
8382
|
+
let burstMs = 0;
|
|
8383
|
+
for (const row of ui.chatSessions) {
|
|
8384
|
+
const measured = ui.burstStats[row.sessionId] ?? row.burst;
|
|
8385
|
+
if (measured === void 0) continue;
|
|
8386
|
+
outputTokens += measured.outputTokens;
|
|
8387
|
+
burstMs += measured.burstMs;
|
|
8388
|
+
}
|
|
8389
|
+
return outputTokens > 0 && burstMs > 0 ? {
|
|
8390
|
+
outputTokens,
|
|
8391
|
+
burstMs
|
|
8392
|
+
} : void 0;
|
|
8393
|
+
}
|
|
8394
|
+
/**
|
|
8395
|
+
* Replace ONLY the decode half of the host's session stats.
|
|
8396
|
+
*
|
|
8397
|
+
* `decodeTokens` and `decodeMs` are the two fields the host's `decodeSpeed`
|
|
8398
|
+
* reads (`decodeTokens / (decodeMs / 1000)`), so writing the plugin's aggregate
|
|
8399
|
+
* there makes the host's own formatter, dialog row and aria label display the
|
|
8400
|
+
* plugin's burst rate. Every other field — turns, steps, LLM/tool/TTFT — stays
|
|
8401
|
+
* host-native. With no measurable burst the two decode fields are zeroed: the
|
|
8402
|
+
* host then hides the speed (`decodeMs > 0` guard) while keeping the counts.
|
|
8403
|
+
*/
|
|
8404
|
+
function patchedActivityStats(native, burst, acp) {
|
|
8405
|
+
if (!acp || native === void 0) return native;
|
|
8406
|
+
if (burst !== void 0 && burst.outputTokens > 0 && burst.burstMs > 0) return {
|
|
8407
|
+
...native,
|
|
8408
|
+
decodeTokens: burst.outputTokens,
|
|
8409
|
+
decodeMs: burst.burstMs
|
|
8410
|
+
};
|
|
8411
|
+
return {
|
|
8412
|
+
...native,
|
|
8413
|
+
decodeTokens: 0,
|
|
8414
|
+
decodeMs: 0
|
|
8415
|
+
};
|
|
8416
|
+
}
|
|
8417
|
+
function ActivityPillOccupant({ host: Host, ...props }) {
|
|
8418
|
+
const ui = useUi$2(props.acp);
|
|
8419
|
+
const dshSessionId = String(props.sessionId);
|
|
8420
|
+
const acpMode = ui.acpMode[dshSessionId] === true;
|
|
8421
|
+
const native = props.useProjection("sessionStats");
|
|
8422
|
+
const burst = acpMode ? activityBurst(ui, dshSessionId) : void 0;
|
|
8423
|
+
const stats = (0, react.useMemo)(() => patchedActivityStats(native, burst, acpMode), [
|
|
8424
|
+
native,
|
|
8425
|
+
burst?.outputTokens,
|
|
8426
|
+
burst?.burstMs,
|
|
8427
|
+
acpMode
|
|
8428
|
+
]);
|
|
8429
|
+
const projection = (0, react.useMemo)(() => (key, selector) => {
|
|
8430
|
+
if (key !== "sessionStats") return void 0;
|
|
8431
|
+
return selector === void 0 ? stats : selector(stats);
|
|
8432
|
+
}, [stats]);
|
|
8433
|
+
return (0, react.createElement)(Host, {
|
|
8434
|
+
useChat: props.useChat,
|
|
8435
|
+
useProjection: acpMode ? projection : props.useProjection,
|
|
8436
|
+
usePerformanceUsage: props.usePerformanceUsage,
|
|
8437
|
+
t: props.t
|
|
8438
|
+
});
|
|
8439
|
+
}
|
|
8440
|
+
function findHostActivity(slots) {
|
|
8441
|
+
try {
|
|
8442
|
+
if (slots.spec?.("conversation.composer.dock")?.kind !== "list") return void 0;
|
|
8443
|
+
return (slots.entriesOfSlot?.("conversation.composer.dock") ?? []).find((entry) => entry.options?.id === ACTIVITY_ENTRY_ID);
|
|
8444
|
+
} catch {
|
|
8445
|
+
return;
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
/**
|
|
8449
|
+
* Take the dock's `activity` cell for one ACP-mode spell.
|
|
8450
|
+
*
|
|
8451
|
+
* Returns `undefined` on any host shape this bundle cannot serve — an older
|
|
8452
|
+
* host without the `activity` entry (0.1.5 ships `stats`), a slot kind change,
|
|
8453
|
+
* a missing inject/locale, or no `performanceUsage` source. The native pill is
|
|
8454
|
+
* then left untouched; that is the honest failure mode, not a hidden control.
|
|
8455
|
+
*/
|
|
8456
|
+
function createActivityPillClaim(options) {
|
|
8457
|
+
const host = findHostActivity(options.slots);
|
|
8458
|
+
if (host === void 0 || host.component === void 0 || typeof host.inject !== "function" || typeof host.locale !== "string" || host.locale === "") {
|
|
8459
|
+
options.onDiag?.("activity pill: host entry not available");
|
|
8460
|
+
return;
|
|
8461
|
+
}
|
|
8462
|
+
let performanceUsage;
|
|
8463
|
+
try {
|
|
8464
|
+
performanceUsage = host.inject()?.hooks?.performanceUsage;
|
|
8465
|
+
} catch {
|
|
8466
|
+
performanceUsage = void 0;
|
|
8467
|
+
}
|
|
8468
|
+
if (performanceUsage === void 0) {
|
|
8469
|
+
options.onDiag?.("activity pill: host performance hook missing");
|
|
8470
|
+
return;
|
|
8471
|
+
}
|
|
8472
|
+
const Host = host.component;
|
|
8473
|
+
return createSeatClaim({
|
|
8474
|
+
slots: options.slots,
|
|
8475
|
+
slotKey: DOCK_SLOT,
|
|
8476
|
+
entryId: ACTIVITY_ENTRY_ID,
|
|
8477
|
+
order: host.options?.order ?? 0,
|
|
8478
|
+
locale: host.locale,
|
|
8479
|
+
basePriority: -1,
|
|
8480
|
+
occupant: () => (props) => (0, react.createElement)(ActivityPillOccupant, {
|
|
8481
|
+
...props,
|
|
8482
|
+
host: Host
|
|
8483
|
+
}),
|
|
8484
|
+
inject: (sessionId, actions) => ({
|
|
8485
|
+
...options.inject(sessionId, actions),
|
|
8486
|
+
hooks: { performanceUsage }
|
|
8487
|
+
}),
|
|
8488
|
+
label: "activity pill",
|
|
8489
|
+
onDiag: options.onDiag,
|
|
8490
|
+
maxClaims: options.maxClaims ?? 1
|
|
8491
|
+
});
|
|
8492
|
+
}
|
|
8493
|
+
//#endregion
|
|
8300
8494
|
//#region src/client/slash-isolation.ts
|
|
8301
8495
|
/**
|
|
8302
8496
|
* Cross-instance marker. `apply()` can run again over the same live sources (a
|
|
@@ -8771,8 +8965,7 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
8771
8965
|
*/
|
|
8772
8966
|
const hostFork = () => {
|
|
8773
8967
|
try {
|
|
8774
|
-
|
|
8775
|
-
return typeof fork === "function" ? { fork } : void 0;
|
|
8968
|
+
return resolveHostFork(service("sessions"));
|
|
8776
8969
|
} catch {
|
|
8777
8970
|
return;
|
|
8778
8971
|
}
|
|
@@ -8935,6 +9128,22 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
8935
9128
|
*/
|
|
8936
9129
|
let permissionClaim;
|
|
8937
9130
|
/**
|
|
9131
|
+
* Claim on the dock's `activity` cell (`conversation.composer.dock`, id
|
|
9132
|
+
* `activity`), held for the same ACP-mode spell as the two seats. The
|
|
9133
|
+
* occupant renders the HOST's ActivityPill with the plugin's burst speed
|
|
9134
|
+
* patched into its `sessionStats`; it is a list cell, so the claim is
|
|
9135
|
+
* identified by id rather than by the slot's heads[0]. If the host shape is
|
|
9136
|
+
* not one this bundle knows, no claim is created and the native pill stays.
|
|
9137
|
+
*/
|
|
9138
|
+
let activityClaim;
|
|
9139
|
+
let activityDiag = "idle";
|
|
9140
|
+
const noteActivityDiag = (line) => {
|
|
9141
|
+
if (line === activityDiag) return;
|
|
9142
|
+
activityDiag = line;
|
|
9143
|
+
if (!/failed|missing|taken|gave up|still held/.test(line)) return;
|
|
9144
|
+
console.warn(`[dsh-ntr] ${line}`);
|
|
9145
|
+
};
|
|
9146
|
+
/**
|
|
8938
9147
|
* ACP-only auxiliary entries: the NTR view tab and the composer's context
|
|
8939
9148
|
* ring. Unlike the two seats these are `list` slots with no visibility
|
|
8940
9149
|
* predicate of their own, so "self-hide in the component" cannot remove a
|
|
@@ -9018,6 +9227,10 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
9018
9227
|
permissionClaim.release();
|
|
9019
9228
|
permissionClaim = void 0;
|
|
9020
9229
|
}
|
|
9230
|
+
if (activityClaim !== void 0) {
|
|
9231
|
+
activityClaim.release();
|
|
9232
|
+
activityClaim = void 0;
|
|
9233
|
+
}
|
|
9021
9234
|
};
|
|
9022
9235
|
/**
|
|
9023
9236
|
* Re-ask timer for the staged session, armed while the row is silent and a
|
|
@@ -9095,6 +9308,12 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
9095
9308
|
}
|
|
9096
9309
|
});
|
|
9097
9310
|
permissionClaim.sync();
|
|
9311
|
+
activityClaim ??= createActivityPillClaim({
|
|
9312
|
+
slots: ctx.slots,
|
|
9313
|
+
inject: (sessionId) => chatFace(sessionId),
|
|
9314
|
+
onDiag: noteActivityDiag
|
|
9315
|
+
});
|
|
9316
|
+
activityClaim?.sync();
|
|
9098
9317
|
holdAuxEntries();
|
|
9099
9318
|
if (rowAgentPreset(staged.row) === void 0) armVerdictReask();
|
|
9100
9319
|
};
|
|
@@ -9165,7 +9384,7 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
9165
9384
|
attach();
|
|
9166
9385
|
const stopStore = store.subscribe(runGate);
|
|
9167
9386
|
const stopCell = ctx.on("slots/changed", (...args) => {
|
|
9168
|
-
if (args[0] !== "conversation.input.model" && args[0] !== "conversation.input.permission") return;
|
|
9387
|
+
if (args[0] !== "conversation.input.model" && args[0] !== "conversation.input.permission" && args[0] !== "conversation.composer.dock") return;
|
|
9169
9388
|
runGate();
|
|
9170
9389
|
});
|
|
9171
9390
|
const stopService = ctx.on("internal/service", attach);
|
|
@@ -9187,6 +9406,10 @@ html[data-ntr-acp] [data-turn-tail] button[aria-label="Remove rating"] {
|
|
|
9187
9406
|
permissionClaim.release();
|
|
9188
9407
|
permissionClaim = void 0;
|
|
9189
9408
|
}
|
|
9409
|
+
if (activityClaim !== void 0) {
|
|
9410
|
+
activityClaim.release();
|
|
9411
|
+
activityClaim = void 0;
|
|
9412
|
+
}
|
|
9190
9413
|
store.noteSeatDiag("gate disposed");
|
|
9191
9414
|
store.notePermissionSeatDiag("gate disposed");
|
|
9192
9415
|
};
|