dsh-ntr 0.1.0 → 0.1.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/lib/client/acp-ui.js +61 -0
- package/lib/client/acp-view.js +5 -4
- package/lib/client/index.js +43 -3
- package/lib/client/panel.js +48 -12
- package/lib/client/session-controls.js +59 -13
- package/lib/client/settings-dialog.js +43 -0
- package/lib/client/store.js +24 -1
- package/lib/client.js +338 -36
- package/lib/client.js.map +1 -1
- package/lib/types/client/acp-ui.d.ts +8 -0
- package/lib/types/client/session-controls.d.ts +27 -6
- package/lib/types/client/settings-dialog.d.ts +23 -0
- package/lib/types/client/store.d.ts +22 -0
- package/locale/en.json +5 -0
- package/locale/zh.json +5 -0
- package/package.json +5 -3
- package/src/client/acp-ui.tsx +68 -0
- package/src/client/acp-view.tsx +6 -5
- package/src/client/index.tsx +58 -3
- package/src/client/panel.tsx +84 -13
- package/src/client/session-controls.tsx +71 -15
- package/src/client/settings-dialog.tsx +73 -0
- package/src/client/store.ts +41 -4
package/lib/client.js
CHANGED
|
@@ -270,8 +270,10 @@ window.__ModuleLoader__.load({
|
|
|
270
270
|
locale;
|
|
271
271
|
state = {
|
|
272
272
|
open: false,
|
|
273
|
+
localeRev: 0,
|
|
273
274
|
stream: "init",
|
|
274
275
|
agents: [],
|
|
276
|
+
agentsLoaded: false,
|
|
275
277
|
sessions: [],
|
|
276
278
|
blocks: [],
|
|
277
279
|
permissions: [],
|
|
@@ -343,6 +345,8 @@ window.__ModuleLoader__.load({
|
|
|
343
345
|
seatCatalogReads = /* @__PURE__ */ new Set();
|
|
344
346
|
/** In-flight `agent/cached-models` reads, keyed by agent id. */
|
|
345
347
|
cachedModelReads = /* @__PURE__ */ new Set();
|
|
348
|
+
/** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
|
|
349
|
+
detectCounts;
|
|
346
350
|
constructor(rpc, locale) {
|
|
347
351
|
this.rpc = rpc;
|
|
348
352
|
this.locale = locale;
|
|
@@ -476,6 +480,7 @@ window.__ModuleLoader__.load({
|
|
|
476
480
|
const detectedAgents = detectResult && detectResult.agents.length > 0 ? detectResult.agents : this.state.detectedAgents.length > 0 ? this.state.detectedAgents : [...DEFAULT_KNOWN_PRESETS];
|
|
477
481
|
this.mutate({
|
|
478
482
|
agents,
|
|
483
|
+
agentsLoaded: true,
|
|
479
484
|
sessions,
|
|
480
485
|
defaultAgent: defaultAgentInfo?.agentId,
|
|
481
486
|
detectedAgents,
|
|
@@ -495,14 +500,19 @@ window.__ModuleLoader__.load({
|
|
|
495
500
|
const result = await this.call("agents/detect");
|
|
496
501
|
const agents = result.agents.length > 0 ? result.agents : [...DEFAULT_KNOWN_PRESETS];
|
|
497
502
|
const installed = agents.filter((agent) => agent.detected).length;
|
|
503
|
+
this.detectCounts = {
|
|
504
|
+
total: agents.length,
|
|
505
|
+
installed
|
|
506
|
+
};
|
|
498
507
|
this.mutate({
|
|
499
508
|
detectedAgents: agents,
|
|
500
509
|
detecting: false,
|
|
501
|
-
detectSummary: this.
|
|
510
|
+
detectSummary: this.detectSummaryOf(agents.length, installed)
|
|
502
511
|
});
|
|
503
512
|
} catch (error) {
|
|
504
513
|
const msg = error instanceof Error ? error.message : String(error);
|
|
505
514
|
const notice = msg.includes("unknown endpoint agents/detect") ? this.locale("宿主服务运行着旧版本代码,请在终端重启 dsh web 服务以激活主机检测功能。", "Host running older version. Please restart dsh web to enable agent detection.") : msg;
|
|
515
|
+
this.detectCounts = void 0;
|
|
506
516
|
this.mutate({
|
|
507
517
|
detecting: false,
|
|
508
518
|
notice,
|
|
@@ -511,6 +521,9 @@ window.__ModuleLoader__.load({
|
|
|
511
521
|
});
|
|
512
522
|
}
|
|
513
523
|
}
|
|
524
|
+
detectSummaryOf(total, installed) {
|
|
525
|
+
return this.locale(`已检测 ${String(total)} 个预设:${String(installed)} 个已安装,其余可从「添加智能体」启用。`, `Detected ${String(total)} presets: ${String(installed)} installed, enable the rest via "Add agent".`);
|
|
526
|
+
}
|
|
514
527
|
async saveAgent(payload) {
|
|
515
528
|
try {
|
|
516
529
|
await this.call("agent/save", payload);
|
|
@@ -1254,6 +1267,19 @@ window.__ModuleLoader__.load({
|
|
|
1254
1267
|
* renders or every row re-renders on every store notification.
|
|
1255
1268
|
*/
|
|
1256
1269
|
label = (zh, en) => this.locale(zh, en);
|
|
1270
|
+
/**
|
|
1271
|
+
* Republish the snapshot after a locale switch. Every `label()` resolves
|
|
1272
|
+
* against the current language while rendering, so a notification is all
|
|
1273
|
+
* that is needed — except the detect summary, which the store owns as an
|
|
1274
|
+
* already-resolved string and rebuilds from its counts here.
|
|
1275
|
+
*/
|
|
1276
|
+
refreshLocale() {
|
|
1277
|
+
const counts = this.detectCounts;
|
|
1278
|
+
this.mutate({
|
|
1279
|
+
localeRev: this.state.localeRev + 1,
|
|
1280
|
+
...counts === void 0 ? {} : { detectSummary: this.detectSummaryOf(counts.total, counts.installed) }
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1257
1283
|
clearNotice() {
|
|
1258
1284
|
this.mutate({ notice: void 0 });
|
|
1259
1285
|
}
|
|
@@ -1812,6 +1838,45 @@ window.__ModuleLoader__.load({
|
|
|
1812
1838
|
}, () => menuState);
|
|
1813
1839
|
}
|
|
1814
1840
|
/**
|
|
1841
|
+
* Open state of the frame-wide NTR settings dialog (`settings-dialog.tsx`).
|
|
1842
|
+
*
|
|
1843
|
+
* Module-level because the two ends live in unrelated slot trees: the trigger
|
|
1844
|
+
* is the composer's agent base (a session-scoped model-seat entry) and the
|
|
1845
|
+
* dialog itself rides the root `shell.overlay` layer. A React context cannot
|
|
1846
|
+
* span that boundary, so this is the same tiny external-store seam the menu
|
|
1847
|
+
* uses above.
|
|
1848
|
+
*/
|
|
1849
|
+
let settingsDialogOpen = false;
|
|
1850
|
+
const settingsDialogListeners = /* @__PURE__ */ new Set();
|
|
1851
|
+
function emitSettingsDialog() {
|
|
1852
|
+
for (const listener of [...settingsDialogListeners]) listener();
|
|
1853
|
+
}
|
|
1854
|
+
/** Open the NTR settings dialog (the empty agent roster's way forward). */
|
|
1855
|
+
function openAcpSettingsDialog() {
|
|
1856
|
+
if (settingsDialogOpen) return;
|
|
1857
|
+
settingsDialogOpen = true;
|
|
1858
|
+
emitSettingsDialog();
|
|
1859
|
+
}
|
|
1860
|
+
/** Close the NTR settings dialog (close button, mask click, Escape). */
|
|
1861
|
+
function closeAcpSettingsDialog() {
|
|
1862
|
+
if (!settingsDialogOpen) return;
|
|
1863
|
+
settingsDialogOpen = false;
|
|
1864
|
+
emitSettingsDialog();
|
|
1865
|
+
}
|
|
1866
|
+
/** Current dialog state, for tests and non-React readers. */
|
|
1867
|
+
function acpSettingsDialogIsOpen() {
|
|
1868
|
+
return settingsDialogOpen;
|
|
1869
|
+
}
|
|
1870
|
+
/** Subscribe to the dialog state (the root overlay entry reads it). */
|
|
1871
|
+
function useAcpSettingsDialogOpen() {
|
|
1872
|
+
return (0, react.useSyncExternalStore)((cb) => {
|
|
1873
|
+
settingsDialogListeners.add(cb);
|
|
1874
|
+
return () => {
|
|
1875
|
+
settingsDialogListeners.delete(cb);
|
|
1876
|
+
};
|
|
1877
|
+
}, acpSettingsDialogIsOpen);
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1815
1880
|
* Pure popup placement, viewport coordinates: the menu docks with its right
|
|
1816
1881
|
* edge under the trigger's, `gap`px above it — and only then clamps to the
|
|
1817
1882
|
* viewport (flip below when the top has no room, slide left at the right edge).
|
|
@@ -2344,6 +2409,26 @@ window.__ModuleLoader__.load({
|
|
|
2344
2409
|
.ntr-acp-empty-hint { color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12px; }
|
|
2345
2410
|
.ntr-acp-seat-diag { font-family: var(--dsw-alias-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); opacity: 0.72; word-break: break-word; }
|
|
2346
2411
|
.ntr-acp-empty-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
|
|
2412
|
+
/* ---- NTR settings dialog (shell.overlay) ---- */
|
|
2413
|
+
/* Frame-wide surface opened from the agent base's empty-roster menu. The
|
|
2414
|
+
overlay layer itself is click-through, so the layer opts back in and blocks
|
|
2415
|
+
the app while it is up. */
|
|
2416
|
+
.ntr-acp-dialog-layer { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; pointer-events: auto; }
|
|
2417
|
+
.ntr-acp-dialog-mask { position: absolute; inset: 0; background: var(--dsw-alias-bg-mask-1, rgba(0, 0, 0, 0.4)); }
|
|
2418
|
+
.ntr-acp-dialog {
|
|
2419
|
+
position: relative; display: flex; flex-direction: column; overflow: hidden;
|
|
2420
|
+
width: min(920px, calc(100vw - 48px)); height: min(820px, calc(100vh - 48px));
|
|
2421
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: inherit;
|
|
2422
|
+
border-radius: 24px; box-shadow: var(--dsw-elevation-prominent, 0 24px 60px rgba(0, 0, 0, 0.25));
|
|
2423
|
+
}
|
|
2424
|
+
.ntr-acp-dialog-head { flex: none; display: flex; align-items: center; gap: 8px; padding: 16px 16px 8px 24px; }
|
|
2425
|
+
.ntr-acp-dialog-title { flex: 1; font-size: 16px; font-weight: 600; }
|
|
2426
|
+
.ntr-acp-dialog-close { font: inherit; font-size: 14px; line-height: 1; width: 30px; height: 30px; padding: 0; border: none; border-radius: 999px; background: none; color: inherit; cursor: pointer; }
|
|
2427
|
+
.ntr-acp-dialog-close:hover { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
|
|
2428
|
+
.ntr-acp-dialog-body { flex: 1; min-height: 0; overflow: auto; }
|
|
2429
|
+
/* The section brings its own page scroll for the settings shell's content
|
|
2430
|
+
column; the dialog body is the scroll owner here. */
|
|
2431
|
+
.ntr-acp-dialog-body > .dwa-root { max-width: none; overflow: visible; }
|
|
2347
2432
|
`;
|
|
2348
2433
|
let styleMounted$1 = false;
|
|
2349
2434
|
/** Mount the shared stylesheet once (same pattern as the settings panel's). */
|
|
@@ -2492,16 +2577,18 @@ window.__ModuleLoader__.load({
|
|
|
2492
2577
|
.dwa-inline-setting { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; border-radius: var(--ntr-radius-sm); background: var(--dsw-alias-bg-layer-2, #f9fafb); }
|
|
2493
2578
|
.dwa-inline-setting .dwa-switch { flex-shrink: 0; margin-top: 1px; }
|
|
2494
2579
|
.dwa-setting-copy { color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
2495
|
-
/*
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
.dwa-model-transfer { display: grid; grid-template-columns:
|
|
2499
|
-
/* Tall enough for a comfortable selection (about ten rows) before scrolling
|
|
2500
|
-
|
|
2501
|
-
slack on the shorter side instead of a whole empty half. */
|
|
2580
|
+
/* Stacked, not side by side: the rows carry name, id and credit, and a
|
|
2581
|
+
half-width pane truncated them at every realistic settings width. Each list
|
|
2582
|
+
gets the whole column instead. */
|
|
2583
|
+
.dwa-model-transfer { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
|
|
2584
|
+
/* Tall enough for a comfortable selection (about ten rows) before scrolling;
|
|
2585
|
+
the cap keeps the two stacked panes from pushing the rest of the card away. */
|
|
2502
2586
|
.dwa-model-pane { min-width: 0; max-height: 360px; display: flex; flex-direction: column; border: var(--ntr-line); border-radius: var(--ntr-radius-sm); background: var(--dsw-alias-bg-layer-1, #fff); overflow: hidden; }
|
|
2503
2587
|
.dwa-model-pane-head { display: flex; align-items: center; gap: 6px; padding: 7px 9px; border-bottom: var(--ntr-line); font-size: var(--ntr-font-xs); font-weight: 600; color: var(--dsw-alias-text-secondary, #4b5563); }
|
|
2504
2588
|
.dwa-model-pane-head .dwa-btn { margin-left: auto; min-height: 26px; padding: 2px 8px; font-size: var(--ntr-font-xs); }
|
|
2589
|
+
.dwa-model-pane-head .dwa-caret-btn { flex: none; width: 22px; height: 22px; background: transparent; }
|
|
2590
|
+
.dwa-model-pane-head .dwa-caret-btn:hover { background: var(--dsw-alias-interactive-bg-active, #e5e7eb); }
|
|
2591
|
+
.dwa-model-pane.is-collapsed .dwa-model-list { display: none; }
|
|
2505
2592
|
.dwa-model-list { display: flex; flex-direction: column; gap: 2px; padding: 5px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
2506
2593
|
.dwa-model-section { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; font-size: 12px; }
|
|
2507
2594
|
.dwa-model-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
@@ -2518,7 +2605,19 @@ window.__ModuleLoader__.load({
|
|
|
2518
2605
|
.dwa-model-row.is-on { background: #3b82f614; color: #2563eb; }
|
|
2519
2606
|
.dwa-model-row-mark { width: 12px; flex: none; text-align: center; opacity: 0.75; }
|
|
2520
2607
|
.dwa-model-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2521
|
-
|
|
2608
|
+
/* Name and credit keep their room; the id is the only cell that collapses. */
|
|
2609
|
+
.dwa-model-row-desc { flex: 0 0 auto; max-width: 45%; opacity: 0.75; font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2610
|
+
/* The trailing 14px is reserved for the fade mask: while the id fits, the
|
|
2611
|
+
gradient only covers empty padding; once it overflows, its tail dissolves
|
|
2612
|
+
instead of ending in a hard ellipsis. armModelIdMarquee reads the same
|
|
2613
|
+
padding to scroll the full text under the pointer. */
|
|
2614
|
+
.dwa-model-row-value { flex: 0 1 auto; flex-shrink: 1000; box-sizing: border-box; min-width: 0; max-width: 45%; padding-right: 14px; opacity: 0.55; font-size: 10px; overflow: hidden; white-space: nowrap; mask-image: linear-gradient(to right, #000 calc(100% - 14px), transparent); -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 14px), transparent); }
|
|
2615
|
+
.dwa-model-row-value-text { display: inline-block; white-space: nowrap; }
|
|
2616
|
+
.dwa-model-row:hover .dwa-model-row-value-text { animation: dwa-model-id-marquee var(--dwa-id-duration, 1.4s) ease-in-out 0.2s infinite alternate; }
|
|
2617
|
+
@keyframes dwa-model-id-marquee {
|
|
2618
|
+
from { transform: translateX(0); }
|
|
2619
|
+
to { transform: translateX(var(--dwa-id-shift, 0px)); }
|
|
2620
|
+
}
|
|
2522
2621
|
.dwa-model-row-remove { flex: none; opacity: 0.5; padding: 0 2px; }
|
|
2523
2622
|
.dwa-model-row:hover .dwa-model-row-remove { opacity: 1; }
|
|
2524
2623
|
.dwa-seg { display: inline-flex; border-radius: var(--ntr-radius-sm); border: 1px solid var(--dsw-alias-border-l2, #d1d5db); overflow: hidden; vertical-align: middle; }
|
|
@@ -2535,7 +2634,6 @@ window.__ModuleLoader__.load({
|
|
|
2535
2634
|
.dwa-page-header { flex-direction: column; }
|
|
2536
2635
|
.dwa-page-actions { width: 100%; }
|
|
2537
2636
|
.dwa-page-actions .dwa-btn { width: 100%; }
|
|
2538
|
-
.dwa-model-transfer { grid-template-columns: 1fr; }
|
|
2539
2637
|
.dwa-model-toolbar-actions { width: 100%; }
|
|
2540
2638
|
.dwa-model-search { flex: 1 1 auto; width: auto; }
|
|
2541
2639
|
.dwa-subsection-head, .dwa-preference-row { align-items: flex-start; flex-direction: column; }
|
|
@@ -2543,6 +2641,7 @@ window.__ModuleLoader__.load({
|
|
|
2543
2641
|
}
|
|
2544
2642
|
@media (prefers-reduced-motion: reduce) {
|
|
2545
2643
|
.dwa-root *, .dwa-root *::before, .dwa-root *::after { transition: none !important; }
|
|
2644
|
+
.dwa-model-row:hover .dwa-model-row-value-text { animation: none !important; }
|
|
2546
2645
|
}
|
|
2547
2646
|
`;
|
|
2548
2647
|
let styleMounted = false;
|
|
@@ -3837,6 +3936,22 @@ window.__ModuleLoader__.load({
|
|
|
3837
3936
|
});
|
|
3838
3937
|
}
|
|
3839
3938
|
/**
|
|
3939
|
+
* Measure the id cell on row hover so its CSS marquee can reach the end of the
|
|
3940
|
+
* text. The id is the only cell that yields width, and the panel is resizable,
|
|
3941
|
+
* so the overflow is re-read on every entry rather than cached. The animation
|
|
3942
|
+
* itself lives in CSS and only runs while the row is hovered; `title` on the
|
|
3943
|
+
* row stays the fallback for reduced-motion users.
|
|
3944
|
+
*/
|
|
3945
|
+
function armModelIdMarquee(row) {
|
|
3946
|
+
const view = row.querySelector(".dwa-model-row-value");
|
|
3947
|
+
const text = view?.querySelector(".dwa-model-row-value-text");
|
|
3948
|
+
if (view === null || view === void 0 || text === null || text === void 0) return;
|
|
3949
|
+
const fade = Number.parseFloat(getComputedStyle(view).paddingRight) || 0;
|
|
3950
|
+
const shift = Math.max(0, text.offsetWidth - (view.clientWidth - fade));
|
|
3951
|
+
row.style.setProperty("--dwa-id-shift", `${String(-shift)}px`);
|
|
3952
|
+
row.style.setProperty("--dwa-id-duration", `${String(Math.min(4, Math.max(1.2, shift / 30)))}s`);
|
|
3953
|
+
}
|
|
3954
|
+
/**
|
|
3840
3955
|
* Model / thought-level catalog display plus the native-selector visibility
|
|
3841
3956
|
* filter, as a two-pane transfer list: every known model on the left, the
|
|
3842
3957
|
* selection that reaches the NTR surfaces on the right. Variants are still
|
|
@@ -3872,6 +3987,8 @@ window.__ModuleLoader__.load({
|
|
|
3872
3987
|
const modelChoices = result?.catalog.model?.choices ?? [];
|
|
3873
3988
|
/** Result of the last click on 「检测可用模型」 (what the refresh changed). */
|
|
3874
3989
|
const [detectNote, setDetectNote] = (0, react.useState)(void 0);
|
|
3990
|
+
const [allCollapsed, setAllCollapsed] = (0, react.useState)(false);
|
|
3991
|
+
const [selectedCollapsed, setSelectedCollapsed] = (0, react.useState)(false);
|
|
3875
3992
|
(0, react.useEffect)(() => {
|
|
3876
3993
|
if (!active) return;
|
|
3877
3994
|
acp.loadCachedModels(agentId);
|
|
@@ -4075,7 +4192,7 @@ window.__ModuleLoader__.load({
|
|
|
4075
4192
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4076
4193
|
className: "dwa-model-transfer",
|
|
4077
4194
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4078
|
-
className:
|
|
4195
|
+
className: `dwa-model-pane${allCollapsed ? " is-collapsed" : ""}`,
|
|
4079
4196
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4080
4197
|
className: "dwa-model-pane-head",
|
|
4081
4198
|
children: [
|
|
@@ -4091,6 +4208,18 @@ window.__ModuleLoader__.load({
|
|
|
4091
4208
|
onClick: addVisible,
|
|
4092
4209
|
title: acp.label("把当前搜索范围内的模型全部加入右侧", "Add every model matching the current search"),
|
|
4093
4210
|
children: acp.label("全部加入", "Add all")
|
|
4211
|
+
}),
|
|
4212
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4213
|
+
type: "button",
|
|
4214
|
+
className: "dwa-caret-btn",
|
|
4215
|
+
"aria-expanded": !allCollapsed,
|
|
4216
|
+
onClick: () => setAllCollapsed((value) => !value),
|
|
4217
|
+
title: allCollapsed ? acp.label("展开全部模型", "Expand all models") : acp.label("收起全部模型", "Collapse all models"),
|
|
4218
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4219
|
+
className: "dwa-caret",
|
|
4220
|
+
"aria-hidden": true,
|
|
4221
|
+
children: allCollapsed ? "▸" : "▾"
|
|
4222
|
+
})
|
|
4094
4223
|
})
|
|
4095
4224
|
]
|
|
4096
4225
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4103,6 +4232,9 @@ window.__ModuleLoader__.load({
|
|
|
4103
4232
|
className: `dwa-model-row${checked ? " is-on" : ""}`,
|
|
4104
4233
|
title,
|
|
4105
4234
|
onClick: () => toggleChoice(choice.value),
|
|
4235
|
+
onPointerEnter: (event) => {
|
|
4236
|
+
armModelIdMarquee(event.currentTarget);
|
|
4237
|
+
},
|
|
4106
4238
|
children: [
|
|
4107
4239
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4108
4240
|
className: "dwa-model-row-mark",
|
|
@@ -4117,7 +4249,14 @@ window.__ModuleLoader__.load({
|
|
|
4117
4249
|
}),
|
|
4118
4250
|
choice.value !== choice.name && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4119
4251
|
className: "dwa-model-row-value",
|
|
4120
|
-
children:
|
|
4252
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4253
|
+
className: "dwa-model-row-value-text",
|
|
4254
|
+
children: choice.value
|
|
4255
|
+
})
|
|
4256
|
+
}),
|
|
4257
|
+
choice.description !== void 0 && choice.description !== choice.group && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4258
|
+
className: "dwa-model-row-desc",
|
|
4259
|
+
children: choice.description
|
|
4121
4260
|
})
|
|
4122
4261
|
]
|
|
4123
4262
|
}, choice.value);
|
|
@@ -4127,7 +4266,7 @@ window.__ModuleLoader__.load({
|
|
|
4127
4266
|
})]
|
|
4128
4267
|
})]
|
|
4129
4268
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4130
|
-
className:
|
|
4269
|
+
className: `dwa-model-pane${selectedCollapsed ? " is-collapsed" : ""}`,
|
|
4131
4270
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4132
4271
|
className: "dwa-model-pane-head",
|
|
4133
4272
|
children: [
|
|
@@ -4142,6 +4281,18 @@ window.__ModuleLoader__.load({
|
|
|
4142
4281
|
disabled: basis.length === 0,
|
|
4143
4282
|
onClick: clearAll,
|
|
4144
4283
|
children: acp.label("清空", "Clear")
|
|
4284
|
+
}),
|
|
4285
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4286
|
+
type: "button",
|
|
4287
|
+
className: "dwa-caret-btn",
|
|
4288
|
+
"aria-expanded": !selectedCollapsed,
|
|
4289
|
+
onClick: () => setSelectedCollapsed((value) => !value),
|
|
4290
|
+
title: selectedCollapsed ? acp.label("展开已选模型", "Expand selected models") : acp.label("收起已选模型", "Collapse selected models"),
|
|
4291
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4292
|
+
className: "dwa-caret",
|
|
4293
|
+
"aria-hidden": true,
|
|
4294
|
+
children: selectedCollapsed ? "▸" : "▾"
|
|
4295
|
+
})
|
|
4145
4296
|
})
|
|
4146
4297
|
]
|
|
4147
4298
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4154,6 +4305,9 @@ window.__ModuleLoader__.load({
|
|
|
4154
4305
|
className: "dwa-model-row is-on",
|
|
4155
4306
|
title: choice.description === void 0 ? choice.value : `${choice.value}\n${choice.description}`,
|
|
4156
4307
|
onClick: () => toggleChoice(choice.value),
|
|
4308
|
+
onPointerEnter: (event) => {
|
|
4309
|
+
armModelIdMarquee(event.currentTarget);
|
|
4310
|
+
},
|
|
4157
4311
|
children: [
|
|
4158
4312
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4159
4313
|
className: "dwa-model-row-name",
|
|
@@ -4164,7 +4318,14 @@ window.__ModuleLoader__.load({
|
|
|
4164
4318
|
}),
|
|
4165
4319
|
choice.value !== choice.name && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4166
4320
|
className: "dwa-model-row-value",
|
|
4167
|
-
children:
|
|
4321
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4322
|
+
className: "dwa-model-row-value-text",
|
|
4323
|
+
children: choice.value
|
|
4324
|
+
})
|
|
4325
|
+
}),
|
|
4326
|
+
choice.description !== void 0 && choice.description !== choice.group && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4327
|
+
className: "dwa-model-row-desc",
|
|
4328
|
+
children: choice.description
|
|
4168
4329
|
}),
|
|
4169
4330
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4170
4331
|
className: "dwa-model-row-remove",
|
|
@@ -4429,10 +4590,12 @@ window.__ModuleLoader__.load({
|
|
|
4429
4590
|
* different slots, so they share that module's phase clock rather than trying
|
|
4430
4591
|
* to animate across slot boundaries.
|
|
4431
4592
|
*
|
|
4432
|
-
* It is deliberately quiet:
|
|
4433
|
-
*
|
|
4434
|
-
*
|
|
4435
|
-
*
|
|
4593
|
+
* It is deliberately quiet: outside ACP mode the gate never mounts it, and
|
|
4594
|
+
* before the host has answered the agent roster it renders nothing. Once the
|
|
4595
|
+
* roster is KNOWN to be empty the base stays and its picker carries the
|
|
4596
|
+
* "open NTR settings" action — an NTR conversation with nothing configured
|
|
4597
|
+
* still has a way forward instead of an empty cell. The ACP view tab remains
|
|
4598
|
+
* the entry point for conversations no agent has served.
|
|
4436
4599
|
*/
|
|
4437
4600
|
const STATE_COLORS = {
|
|
4438
4601
|
ready: "#22c55e",
|
|
@@ -4477,6 +4640,18 @@ window.__ModuleLoader__.load({
|
|
|
4477
4640
|
if (defaultAgent !== void 0 && agents.some((agent) => agent.id === defaultAgent)) return defaultAgent;
|
|
4478
4641
|
}
|
|
4479
4642
|
/**
|
|
4643
|
+
* Whether the agent base has anything honest to show.
|
|
4644
|
+
*
|
|
4645
|
+
* An empty roster has two meanings, and they must not be conflated: before the
|
|
4646
|
+
* host answers `agents/list` it means "not read yet" (rendering a setup entry
|
|
4647
|
+
* there would accuse a correctly configured deployment on every page load),
|
|
4648
|
+
* while after the answer it means "nothing is configured" — exactly the state
|
|
4649
|
+
* that must keep the base and offer the way into NTR settings.
|
|
4650
|
+
*/
|
|
4651
|
+
function agentBaseHasData(bound, agents, rosterLoaded) {
|
|
4652
|
+
return bound !== void 0 || agents.length > 0 || rosterLoaded;
|
|
4653
|
+
}
|
|
4654
|
+
/**
|
|
4480
4655
|
* The agent this conversation is LOCKED to, if its first ACP turn has run.
|
|
4481
4656
|
*
|
|
4482
4657
|
* The host's `lockedAgentId` is the durable answer: it is written when the
|
|
@@ -4641,12 +4816,14 @@ window.__ModuleLoader__.load({
|
|
|
4641
4816
|
* The tooltip claims only what the menu behind the chip does: switch the agent.
|
|
4642
4817
|
* The agent's models and thought levels have chips of their own (user decision,
|
|
4643
4818
|
* 2026-10-17), so the previous wording — "switch the agent, its model and the
|
|
4644
|
-
* thought level" — promised a menu that holds the roster alone.
|
|
4819
|
+
* thought level" — promised a menu that holds the roster alone. An EMPTY
|
|
4820
|
+
* roster is its own case: the menu cannot switch to anything, it offers the
|
|
4821
|
+
* way into NTR settings, so the tooltip says that instead of promising a pick.
|
|
4645
4822
|
*/
|
|
4646
|
-
function agentBaseChip(route, routed, acp) {
|
|
4823
|
+
function agentBaseChip(route, routed, acp, rosterEmpty = false) {
|
|
4647
4824
|
return {
|
|
4648
4825
|
label: route.agentTitle === void 0 ? routed ? "NTR" : acp.label("选择 Agent", "Choose agent") : `NTR · ${route.agentTitle}`,
|
|
4649
|
-
title: route.agentId === void 0 ? acp.label("这条会话当前没有 NTR agent 服务;点开可选择 agent", "No NTR agent is serving this conversation right now — open to choose one") : acp.label("这条会话由外接 NTR agent 服务;点开可切换 agent", "This conversation is served by an external NTR agent — open to switch the agent")
|
|
4826
|
+
title: route.agentId === void 0 ? rosterEmpty ? acp.label("这条会话当前没有 NTR agent;点开可前往 NTR 设置添加", "No NTR agent is serving this conversation — open to set one up in NTR settings") : acp.label("这条会话当前没有 NTR agent 服务;点开可选择 agent", "No NTR agent is serving this conversation right now — open to choose one") : acp.label("这条会话由外接 NTR agent 服务;点开可切换 agent", "This conversation is served by an external NTR agent — open to switch the agent")
|
|
4650
4827
|
};
|
|
4651
4828
|
}
|
|
4652
4829
|
/**
|
|
@@ -4757,7 +4934,7 @@ window.__ModuleLoader__.load({
|
|
|
4757
4934
|
agentId
|
|
4758
4935
|
]);
|
|
4759
4936
|
const route = describeRoute(agents, bound, lockedTo === void 0 ? ui.defaultAgent : void 0, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
|
|
4760
|
-
if (bound
|
|
4937
|
+
if (!agentBaseHasData(bound, agents, ui.agentsLoaded)) return null;
|
|
4761
4938
|
const routed = route.agentId !== void 0;
|
|
4762
4939
|
(0, react.useEffect)(() => {
|
|
4763
4940
|
if (lockedTo === void 0 || menu === null) return;
|
|
@@ -4807,7 +4984,7 @@ window.__ModuleLoader__.load({
|
|
|
4807
4984
|
}
|
|
4808
4985
|
}),
|
|
4809
4986
|
showAgentBase && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AcpChip, {
|
|
4810
|
-
...agentBaseChip(route, routed, acp),
|
|
4987
|
+
...agentBaseChip(route, routed, acp, agents.length === 0),
|
|
4811
4988
|
active: routed,
|
|
4812
4989
|
className: shattering ? "ntr-acp-shatter" : void 0,
|
|
4813
4990
|
onClick: shattering ? void 0 : (trigger) => {
|
|
@@ -4826,6 +5003,13 @@ window.__ModuleLoader__.load({
|
|
|
4826
5003
|
* `agentId@value`.
|
|
4827
5004
|
*/
|
|
4828
5005
|
const REFRESH_CATALOG_ITEM = "__refresh-catalog__";
|
|
5006
|
+
/**
|
|
5007
|
+
* The empty roster's setup action. Like the model picker's refresh row it is an
|
|
5008
|
+
* ACTION inside the picker, not an agent: agent ids match
|
|
5009
|
+
* `[a-z0-9][a-z0-9._-]{0,63}` (no leading underscore), so no real id can
|
|
5010
|
+
* collide with it.
|
|
5011
|
+
*/
|
|
5012
|
+
const OPEN_SETTINGS_ITEM = "__open-settings__";
|
|
4829
5013
|
/** The agent roster rows: one row per configured ACP agent, current one marked. */
|
|
4830
5014
|
function agentSection(context) {
|
|
4831
5015
|
const { acp, route } = context;
|
|
@@ -4836,12 +5020,19 @@ window.__ModuleLoader__.load({
|
|
|
4836
5020
|
dot: STATE_COLORS[agent.state],
|
|
4837
5021
|
active: route.agentId === agent.id
|
|
4838
5022
|
}));
|
|
4839
|
-
if (items.length === 0)
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
5023
|
+
if (items.length === 0) {
|
|
5024
|
+
items.push({
|
|
5025
|
+
id: "none",
|
|
5026
|
+
label: acp.label("还没有配置 NTR agent", "No NTR agent configured yet"),
|
|
5027
|
+
detail: acp.label("添加并启用一个 agent 后即可开始对话", "Add and enable an agent to start"),
|
|
5028
|
+
disabled: true
|
|
5029
|
+
});
|
|
5030
|
+
items.push({
|
|
5031
|
+
id: OPEN_SETTINGS_ITEM,
|
|
5032
|
+
label: acp.label("打开 NTR 设置", "Open NTR settings"),
|
|
5033
|
+
detail: acp.label("添加或启用 agent", "Add or enable an agent")
|
|
5034
|
+
});
|
|
5035
|
+
}
|
|
4845
5036
|
return {
|
|
4846
5037
|
id: "agent",
|
|
4847
5038
|
items
|
|
@@ -5072,6 +5263,11 @@ window.__ModuleLoader__.load({
|
|
|
5072
5263
|
if (agentId !== void 0) await acp.probeModels(agentId);
|
|
5073
5264
|
return;
|
|
5074
5265
|
}
|
|
5266
|
+
if (sectionId === "agent" && itemId === "__open-settings__") {
|
|
5267
|
+
closeAcpMenu();
|
|
5268
|
+
openAcpSettingsDialog();
|
|
5269
|
+
return;
|
|
5270
|
+
}
|
|
5075
5271
|
if (sectionId === "agent") await acp.bindChatAgent(sessionId, itemId);
|
|
5076
5272
|
else if (sectionId === "model") await acp.setChatSessionModel(sessionId, itemId, agentId);
|
|
5077
5273
|
else if (sectionId === "thought") await acp.setChatSessionEffort(sessionId, itemId === "provider-default" ? void 0 : itemId, agentId);
|
|
@@ -5436,9 +5632,10 @@ window.__ModuleLoader__.load({
|
|
|
5436
5632
|
});
|
|
5437
5633
|
}
|
|
5438
5634
|
/**
|
|
5439
|
-
* One stream block, dispatched by kind. Memoized on its props — `block
|
|
5440
|
-
*
|
|
5441
|
-
*
|
|
5635
|
+
* One stream block, dispatched by kind. Memoized on its props — `block`, the
|
|
5636
|
+
* store's instance-bound `label`, and `localeRev` (it moves on a language
|
|
5637
|
+
* switch, so translated copy re-renders) — so a folded batch re-renders only
|
|
5638
|
+
* the rows whose block object changed (`foldUpdate` replaces blocks immutably)
|
|
5442
5639
|
* instead of the whole transcript on every store notification. `key` stays the
|
|
5443
5640
|
* index on purpose: blocks fold append-only, and identity is the block object.
|
|
5444
5641
|
*/
|
|
@@ -5685,12 +5882,83 @@ window.__ModuleLoader__.load({
|
|
|
5685
5882
|
children: t("还没有消息:在下面的输入框里发给它。", "Nothing yet — send it a message below.")
|
|
5686
5883
|
}) : transcript.blocks.map((block, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockView, {
|
|
5687
5884
|
block,
|
|
5688
|
-
label: acp.label
|
|
5885
|
+
label: acp.label,
|
|
5886
|
+
localeRev: ui.localeRev
|
|
5689
5887
|
}, index))
|
|
5690
5888
|
})]
|
|
5691
5889
|
});
|
|
5692
5890
|
}
|
|
5693
5891
|
//#endregion
|
|
5892
|
+
//#region src/client/settings-dialog.tsx
|
|
5893
|
+
/**
|
|
5894
|
+
* The NTR settings dialog, opened from the agent base's empty-roster menu.
|
|
5895
|
+
*
|
|
5896
|
+
* WHY A DIALOG OF OUR OWN: the host settings shell owns its open state and
|
|
5897
|
+
* exposes no programmatic way in. The sidebar seat that receives `openSettings`
|
|
5898
|
+
* (`settings.launcher`) is a single-occupant slot already held by the account
|
|
5899
|
+
* menu, and `openSection` is only handed to `settings.onboarding` registrants —
|
|
5900
|
+
* so a conversation surface cannot drive the shell's modal. This dialog mounts
|
|
5901
|
+
* the SAME `AcpSettingsSection` the shell mounts, on the frame's overlay layer
|
|
5902
|
+
* (`shell.overlay`): one settings surface, two entrances, no duplicated
|
|
5903
|
+
* configuration logic.
|
|
5904
|
+
*
|
|
5905
|
+
* The trigger (composer model seat) and this dialog (root overlay) live in
|
|
5906
|
+
* unrelated slot trees, so the open state is the module-level store in
|
|
5907
|
+
* `acp-ui.tsx` (`openAcpSettingsDialog`).
|
|
5908
|
+
*/
|
|
5909
|
+
/** Frame-wide NTR settings surface; renders nothing while closed. */
|
|
5910
|
+
function AcpSettingsDialog(props) {
|
|
5911
|
+
ensureAcpStyle();
|
|
5912
|
+
const { acp } = props;
|
|
5913
|
+
const open = useAcpSettingsDialogOpen();
|
|
5914
|
+
(0, react.useEffect)(() => {
|
|
5915
|
+
if (!open) return;
|
|
5916
|
+
const onKey = (event) => {
|
|
5917
|
+
if (event.key !== "Escape") return;
|
|
5918
|
+
event.preventDefault();
|
|
5919
|
+
event.stopPropagation();
|
|
5920
|
+
closeAcpSettingsDialog();
|
|
5921
|
+
};
|
|
5922
|
+
document.addEventListener("keydown", onKey, true);
|
|
5923
|
+
return () => {
|
|
5924
|
+
document.removeEventListener("keydown", onKey, true);
|
|
5925
|
+
};
|
|
5926
|
+
}, [open]);
|
|
5927
|
+
if (!open) return null;
|
|
5928
|
+
const title = acp.label("NTR 设置", "NTR settings");
|
|
5929
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5930
|
+
className: "ntr-acp-dialog-layer",
|
|
5931
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5932
|
+
className: "ntr-acp-dialog-mask",
|
|
5933
|
+
"aria-hidden": true,
|
|
5934
|
+
onClick: closeAcpSettingsDialog
|
|
5935
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5936
|
+
className: "ntr-acp-dialog",
|
|
5937
|
+
role: "dialog",
|
|
5938
|
+
"aria-modal": "true",
|
|
5939
|
+
"aria-label": title,
|
|
5940
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5941
|
+
className: "ntr-acp-dialog-head",
|
|
5942
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5943
|
+
className: "ntr-acp-dialog-title",
|
|
5944
|
+
children: title
|
|
5945
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5946
|
+
type: "button",
|
|
5947
|
+
className: "ntr-acp-dialog-close",
|
|
5948
|
+
title: acp.label("关闭", "Close"),
|
|
5949
|
+
"aria-label": acp.label("关闭 NTR 设置", "Close NTR settings"),
|
|
5950
|
+
autoFocus: true,
|
|
5951
|
+
onClick: closeAcpSettingsDialog,
|
|
5952
|
+
children: "✕"
|
|
5953
|
+
})]
|
|
5954
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5955
|
+
className: "ntr-acp-dialog-body",
|
|
5956
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AcpSettingsSection, { acp })
|
|
5957
|
+
})]
|
|
5958
|
+
})]
|
|
5959
|
+
});
|
|
5960
|
+
}
|
|
5961
|
+
//#endregion
|
|
5694
5962
|
//#region src/client/model-seat.tsx
|
|
5695
5963
|
/** Exact key of the composer's model seat (a `single`, session-scoped slot). */
|
|
5696
5964
|
const MODEL_SEAT_KEY = "conversation.input.model";
|
|
@@ -6237,7 +6505,7 @@ window.__ModuleLoader__.load({
|
|
|
6237
6505
|
//#region src/client/index.tsx
|
|
6238
6506
|
const name = "dsh-ntr-client";
|
|
6239
6507
|
const inject = ["slots", "connection"];
|
|
6240
|
-
const
|
|
6508
|
+
const browserPrefersZh = typeof navigator !== "undefined" && /^zh\b/i.test(navigator.language);
|
|
6241
6509
|
/**
|
|
6242
6510
|
* How long a host verdict may stand as the gate's ONLY fact source before the
|
|
6243
6511
|
* gate asks the host again.
|
|
@@ -6269,8 +6537,6 @@ window.__ModuleLoader__.load({
|
|
|
6269
6537
|
const rpc = ctx.get("connection")?.rpc;
|
|
6270
6538
|
if (rpc === void 0) throw new Error("dsh-ntr: browser connection.rpc is unavailable");
|
|
6271
6539
|
const call = (channel, endpoint, payload) => rpc.call(channel, endpoint, payload);
|
|
6272
|
-
const store = new AcpStore(call, (zhText, enText) => zh ? zhText : enText);
|
|
6273
|
-
store.start();
|
|
6274
6540
|
/**
|
|
6275
6541
|
* Resolve one optional service without injecting it. A plain `ctx.get` at
|
|
6276
6542
|
* call time keeps the plugin loadable when the service is absent AND picks it
|
|
@@ -6278,6 +6544,36 @@ window.__ModuleLoader__.load({
|
|
|
6278
6544
|
* also making its absence fatal.
|
|
6279
6545
|
*/
|
|
6280
6546
|
const service = (name) => ctx.get(name);
|
|
6547
|
+
/**
|
|
6548
|
+
* Resolve a Chinese/English pair against the shell's active locale. Read per
|
|
6549
|
+
* call, so a language switch reaches every render-time `label()` call without
|
|
6550
|
+
* rebuilding the store. A host without the locale service keeps the
|
|
6551
|
+
* browser-derived choice the pairs shipped with.
|
|
6552
|
+
*/
|
|
6553
|
+
const label = (zhText, enText) => {
|
|
6554
|
+
const resolved = service("locale")?.resolveText?.({
|
|
6555
|
+
zh: zhText,
|
|
6556
|
+
en: enText
|
|
6557
|
+
});
|
|
6558
|
+
return typeof resolved === "string" && resolved !== "" ? resolved : browserPrefersZh ? zhText : enText;
|
|
6559
|
+
};
|
|
6560
|
+
const store = new AcpStore(call, label);
|
|
6561
|
+
store.start();
|
|
6562
|
+
ctx.effect(() => {
|
|
6563
|
+
let stop;
|
|
6564
|
+
const attach = () => {
|
|
6565
|
+
if (stop !== void 0) return;
|
|
6566
|
+
stop = service("locale")?.subscribe?.(() => {
|
|
6567
|
+
store.refreshLocale();
|
|
6568
|
+
});
|
|
6569
|
+
};
|
|
6570
|
+
attach();
|
|
6571
|
+
const stopService = ctx.on("internal/service", attach);
|
|
6572
|
+
return () => {
|
|
6573
|
+
stopService();
|
|
6574
|
+
stop?.();
|
|
6575
|
+
};
|
|
6576
|
+
}, "dsh-ntr: locale switch re-render");
|
|
6281
6577
|
/** This session's command/projection face (`ctx.sessions`); `undefined` before it materializes. */
|
|
6282
6578
|
const faceFor = (sessionId) => {
|
|
6283
6579
|
try {
|
|
@@ -6299,6 +6595,12 @@ window.__ModuleLoader__.load({
|
|
|
6299
6595
|
label: () => "NTR",
|
|
6300
6596
|
inject: () => ({ acp: store })
|
|
6301
6597
|
}, AcpSettingsSection)), "dsh-ntr: settings section");
|
|
6598
|
+
ctx.effect(() => ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
6599
|
+
name: "shell.overlay",
|
|
6600
|
+
id: "acp-settings-dialog",
|
|
6601
|
+
order: 30,
|
|
6602
|
+
inject: () => ({ acp: store })
|
|
6603
|
+
}, AcpSettingsDialog)), "dsh-ntr: NTR settings dialog");
|
|
6302
6604
|
ctx.effect(() => ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
6303
6605
|
name: "conversation.input.left",
|
|
6304
6606
|
id: "acp-mode",
|