dsh-ntr 0.1.1 → 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/index.js +13 -0
- 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 +2 -0
- package/lib/client.js +276 -28
- 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 +6 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
- package/src/client/acp-ui.tsx +68 -0
- package/src/client/index.tsx +14 -0
- 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 +8 -0
package/lib/client.js
CHANGED
|
@@ -273,6 +273,7 @@ window.__ModuleLoader__.load({
|
|
|
273
273
|
localeRev: 0,
|
|
274
274
|
stream: "init",
|
|
275
275
|
agents: [],
|
|
276
|
+
agentsLoaded: false,
|
|
276
277
|
sessions: [],
|
|
277
278
|
blocks: [],
|
|
278
279
|
permissions: [],
|
|
@@ -479,6 +480,7 @@ window.__ModuleLoader__.load({
|
|
|
479
480
|
const detectedAgents = detectResult && detectResult.agents.length > 0 ? detectResult.agents : this.state.detectedAgents.length > 0 ? this.state.detectedAgents : [...DEFAULT_KNOWN_PRESETS];
|
|
480
481
|
this.mutate({
|
|
481
482
|
agents,
|
|
483
|
+
agentsLoaded: true,
|
|
482
484
|
sessions,
|
|
483
485
|
defaultAgent: defaultAgentInfo?.agentId,
|
|
484
486
|
detectedAgents,
|
|
@@ -1836,6 +1838,45 @@ window.__ModuleLoader__.load({
|
|
|
1836
1838
|
}, () => menuState);
|
|
1837
1839
|
}
|
|
1838
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
|
+
/**
|
|
1839
1880
|
* Pure popup placement, viewport coordinates: the menu docks with its right
|
|
1840
1881
|
* edge under the trigger's, `gap`px above it — and only then clamps to the
|
|
1841
1882
|
* viewport (flip below when the top has no room, slide left at the right edge).
|
|
@@ -2368,6 +2409,26 @@ window.__ModuleLoader__.load({
|
|
|
2368
2409
|
.ntr-acp-empty-hint { color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12px; }
|
|
2369
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; }
|
|
2370
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; }
|
|
2371
2432
|
`;
|
|
2372
2433
|
let styleMounted$1 = false;
|
|
2373
2434
|
/** Mount the shared stylesheet once (same pattern as the settings panel's). */
|
|
@@ -2516,16 +2577,18 @@ window.__ModuleLoader__.load({
|
|
|
2516
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); }
|
|
2517
2578
|
.dwa-inline-setting .dwa-switch { flex-shrink: 0; margin-top: 1px; }
|
|
2518
2579
|
.dwa-setting-copy { color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
2519
|
-
/*
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
.dwa-model-transfer { display: grid; grid-template-columns:
|
|
2523
|
-
/* Tall enough for a comfortable selection (about ten rows) before scrolling
|
|
2524
|
-
|
|
2525
|
-
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. */
|
|
2526
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; }
|
|
2527
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); }
|
|
2528
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; }
|
|
2529
2592
|
.dwa-model-list { display: flex; flex-direction: column; gap: 2px; padding: 5px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
2530
2593
|
.dwa-model-section { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; font-size: 12px; }
|
|
2531
2594
|
.dwa-model-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
@@ -2542,7 +2605,19 @@ window.__ModuleLoader__.load({
|
|
|
2542
2605
|
.dwa-model-row.is-on { background: #3b82f614; color: #2563eb; }
|
|
2543
2606
|
.dwa-model-row-mark { width: 12px; flex: none; text-align: center; opacity: 0.75; }
|
|
2544
2607
|
.dwa-model-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2545
|
-
|
|
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
|
+
}
|
|
2546
2621
|
.dwa-model-row-remove { flex: none; opacity: 0.5; padding: 0 2px; }
|
|
2547
2622
|
.dwa-model-row:hover .dwa-model-row-remove { opacity: 1; }
|
|
2548
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; }
|
|
@@ -2559,7 +2634,6 @@ window.__ModuleLoader__.load({
|
|
|
2559
2634
|
.dwa-page-header { flex-direction: column; }
|
|
2560
2635
|
.dwa-page-actions { width: 100%; }
|
|
2561
2636
|
.dwa-page-actions .dwa-btn { width: 100%; }
|
|
2562
|
-
.dwa-model-transfer { grid-template-columns: 1fr; }
|
|
2563
2637
|
.dwa-model-toolbar-actions { width: 100%; }
|
|
2564
2638
|
.dwa-model-search { flex: 1 1 auto; width: auto; }
|
|
2565
2639
|
.dwa-subsection-head, .dwa-preference-row { align-items: flex-start; flex-direction: column; }
|
|
@@ -2567,6 +2641,7 @@ window.__ModuleLoader__.load({
|
|
|
2567
2641
|
}
|
|
2568
2642
|
@media (prefers-reduced-motion: reduce) {
|
|
2569
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; }
|
|
2570
2645
|
}
|
|
2571
2646
|
`;
|
|
2572
2647
|
let styleMounted = false;
|
|
@@ -3861,6 +3936,22 @@ window.__ModuleLoader__.load({
|
|
|
3861
3936
|
});
|
|
3862
3937
|
}
|
|
3863
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
|
+
/**
|
|
3864
3955
|
* Model / thought-level catalog display plus the native-selector visibility
|
|
3865
3956
|
* filter, as a two-pane transfer list: every known model on the left, the
|
|
3866
3957
|
* selection that reaches the NTR surfaces on the right. Variants are still
|
|
@@ -3896,6 +3987,8 @@ window.__ModuleLoader__.load({
|
|
|
3896
3987
|
const modelChoices = result?.catalog.model?.choices ?? [];
|
|
3897
3988
|
/** Result of the last click on 「检测可用模型」 (what the refresh changed). */
|
|
3898
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);
|
|
3899
3992
|
(0, react.useEffect)(() => {
|
|
3900
3993
|
if (!active) return;
|
|
3901
3994
|
acp.loadCachedModels(agentId);
|
|
@@ -4099,7 +4192,7 @@ window.__ModuleLoader__.load({
|
|
|
4099
4192
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4100
4193
|
className: "dwa-model-transfer",
|
|
4101
4194
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4102
|
-
className:
|
|
4195
|
+
className: `dwa-model-pane${allCollapsed ? " is-collapsed" : ""}`,
|
|
4103
4196
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4104
4197
|
className: "dwa-model-pane-head",
|
|
4105
4198
|
children: [
|
|
@@ -4115,6 +4208,18 @@ window.__ModuleLoader__.load({
|
|
|
4115
4208
|
onClick: addVisible,
|
|
4116
4209
|
title: acp.label("把当前搜索范围内的模型全部加入右侧", "Add every model matching the current search"),
|
|
4117
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
|
+
})
|
|
4118
4223
|
})
|
|
4119
4224
|
]
|
|
4120
4225
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4127,6 +4232,9 @@ window.__ModuleLoader__.load({
|
|
|
4127
4232
|
className: `dwa-model-row${checked ? " is-on" : ""}`,
|
|
4128
4233
|
title,
|
|
4129
4234
|
onClick: () => toggleChoice(choice.value),
|
|
4235
|
+
onPointerEnter: (event) => {
|
|
4236
|
+
armModelIdMarquee(event.currentTarget);
|
|
4237
|
+
},
|
|
4130
4238
|
children: [
|
|
4131
4239
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4132
4240
|
className: "dwa-model-row-mark",
|
|
@@ -4141,7 +4249,14 @@ window.__ModuleLoader__.load({
|
|
|
4141
4249
|
}),
|
|
4142
4250
|
choice.value !== choice.name && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4143
4251
|
className: "dwa-model-row-value",
|
|
4144
|
-
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
|
|
4145
4260
|
})
|
|
4146
4261
|
]
|
|
4147
4262
|
}, choice.value);
|
|
@@ -4151,7 +4266,7 @@ window.__ModuleLoader__.load({
|
|
|
4151
4266
|
})]
|
|
4152
4267
|
})]
|
|
4153
4268
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4154
|
-
className:
|
|
4269
|
+
className: `dwa-model-pane${selectedCollapsed ? " is-collapsed" : ""}`,
|
|
4155
4270
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4156
4271
|
className: "dwa-model-pane-head",
|
|
4157
4272
|
children: [
|
|
@@ -4166,6 +4281,18 @@ window.__ModuleLoader__.load({
|
|
|
4166
4281
|
disabled: basis.length === 0,
|
|
4167
4282
|
onClick: clearAll,
|
|
4168
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
|
+
})
|
|
4169
4296
|
})
|
|
4170
4297
|
]
|
|
4171
4298
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4178,6 +4305,9 @@ window.__ModuleLoader__.load({
|
|
|
4178
4305
|
className: "dwa-model-row is-on",
|
|
4179
4306
|
title: choice.description === void 0 ? choice.value : `${choice.value}\n${choice.description}`,
|
|
4180
4307
|
onClick: () => toggleChoice(choice.value),
|
|
4308
|
+
onPointerEnter: (event) => {
|
|
4309
|
+
armModelIdMarquee(event.currentTarget);
|
|
4310
|
+
},
|
|
4181
4311
|
children: [
|
|
4182
4312
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4183
4313
|
className: "dwa-model-row-name",
|
|
@@ -4188,7 +4318,14 @@ window.__ModuleLoader__.load({
|
|
|
4188
4318
|
}),
|
|
4189
4319
|
choice.value !== choice.name && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4190
4320
|
className: "dwa-model-row-value",
|
|
4191
|
-
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
|
|
4192
4329
|
}),
|
|
4193
4330
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4194
4331
|
className: "dwa-model-row-remove",
|
|
@@ -4453,10 +4590,12 @@ window.__ModuleLoader__.load({
|
|
|
4453
4590
|
* different slots, so they share that module's phase clock rather than trying
|
|
4454
4591
|
* to animate across slot boundaries.
|
|
4455
4592
|
*
|
|
4456
|
-
* It is deliberately quiet:
|
|
4457
|
-
*
|
|
4458
|
-
*
|
|
4459
|
-
*
|
|
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.
|
|
4460
4599
|
*/
|
|
4461
4600
|
const STATE_COLORS = {
|
|
4462
4601
|
ready: "#22c55e",
|
|
@@ -4501,6 +4640,18 @@ window.__ModuleLoader__.load({
|
|
|
4501
4640
|
if (defaultAgent !== void 0 && agents.some((agent) => agent.id === defaultAgent)) return defaultAgent;
|
|
4502
4641
|
}
|
|
4503
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
|
+
/**
|
|
4504
4655
|
* The agent this conversation is LOCKED to, if its first ACP turn has run.
|
|
4505
4656
|
*
|
|
4506
4657
|
* The host's `lockedAgentId` is the durable answer: it is written when the
|
|
@@ -4665,12 +4816,14 @@ window.__ModuleLoader__.load({
|
|
|
4665
4816
|
* The tooltip claims only what the menu behind the chip does: switch the agent.
|
|
4666
4817
|
* The agent's models and thought levels have chips of their own (user decision,
|
|
4667
4818
|
* 2026-10-17), so the previous wording — "switch the agent, its model and the
|
|
4668
|
-
* 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.
|
|
4669
4822
|
*/
|
|
4670
|
-
function agentBaseChip(route, routed, acp) {
|
|
4823
|
+
function agentBaseChip(route, routed, acp, rosterEmpty = false) {
|
|
4671
4824
|
return {
|
|
4672
4825
|
label: route.agentTitle === void 0 ? routed ? "NTR" : acp.label("选择 Agent", "Choose agent") : `NTR · ${route.agentTitle}`,
|
|
4673
|
-
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")
|
|
4674
4827
|
};
|
|
4675
4828
|
}
|
|
4676
4829
|
/**
|
|
@@ -4781,7 +4934,7 @@ window.__ModuleLoader__.load({
|
|
|
4781
4934
|
agentId
|
|
4782
4935
|
]);
|
|
4783
4936
|
const route = describeRoute(agents, bound, lockedTo === void 0 ? ui.defaultAgent : void 0, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
|
|
4784
|
-
if (bound
|
|
4937
|
+
if (!agentBaseHasData(bound, agents, ui.agentsLoaded)) return null;
|
|
4785
4938
|
const routed = route.agentId !== void 0;
|
|
4786
4939
|
(0, react.useEffect)(() => {
|
|
4787
4940
|
if (lockedTo === void 0 || menu === null) return;
|
|
@@ -4831,7 +4984,7 @@ window.__ModuleLoader__.load({
|
|
|
4831
4984
|
}
|
|
4832
4985
|
}),
|
|
4833
4986
|
showAgentBase && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AcpChip, {
|
|
4834
|
-
...agentBaseChip(route, routed, acp),
|
|
4987
|
+
...agentBaseChip(route, routed, acp, agents.length === 0),
|
|
4835
4988
|
active: routed,
|
|
4836
4989
|
className: shattering ? "ntr-acp-shatter" : void 0,
|
|
4837
4990
|
onClick: shattering ? void 0 : (trigger) => {
|
|
@@ -4850,6 +5003,13 @@ window.__ModuleLoader__.load({
|
|
|
4850
5003
|
* `agentId@value`.
|
|
4851
5004
|
*/
|
|
4852
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__";
|
|
4853
5013
|
/** The agent roster rows: one row per configured ACP agent, current one marked. */
|
|
4854
5014
|
function agentSection(context) {
|
|
4855
5015
|
const { acp, route } = context;
|
|
@@ -4860,12 +5020,19 @@ window.__ModuleLoader__.load({
|
|
|
4860
5020
|
dot: STATE_COLORS[agent.state],
|
|
4861
5021
|
active: route.agentId === agent.id
|
|
4862
5022
|
}));
|
|
4863
|
-
if (items.length === 0)
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
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
|
+
}
|
|
4869
5036
|
return {
|
|
4870
5037
|
id: "agent",
|
|
4871
5038
|
items
|
|
@@ -5096,6 +5263,11 @@ window.__ModuleLoader__.load({
|
|
|
5096
5263
|
if (agentId !== void 0) await acp.probeModels(agentId);
|
|
5097
5264
|
return;
|
|
5098
5265
|
}
|
|
5266
|
+
if (sectionId === "agent" && itemId === "__open-settings__") {
|
|
5267
|
+
closeAcpMenu();
|
|
5268
|
+
openAcpSettingsDialog();
|
|
5269
|
+
return;
|
|
5270
|
+
}
|
|
5099
5271
|
if (sectionId === "agent") await acp.bindChatAgent(sessionId, itemId);
|
|
5100
5272
|
else if (sectionId === "model") await acp.setChatSessionModel(sessionId, itemId, agentId);
|
|
5101
5273
|
else if (sectionId === "thought") await acp.setChatSessionEffort(sessionId, itemId === "provider-default" ? void 0 : itemId, agentId);
|
|
@@ -5717,6 +5889,76 @@ window.__ModuleLoader__.load({
|
|
|
5717
5889
|
});
|
|
5718
5890
|
}
|
|
5719
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
|
|
5720
5962
|
//#region src/client/model-seat.tsx
|
|
5721
5963
|
/** Exact key of the composer's model seat (a `single`, session-scoped slot). */
|
|
5722
5964
|
const MODEL_SEAT_KEY = "conversation.input.model";
|
|
@@ -6353,6 +6595,12 @@ window.__ModuleLoader__.load({
|
|
|
6353
6595
|
label: () => "NTR",
|
|
6354
6596
|
inject: () => ({ acp: store })
|
|
6355
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");
|
|
6356
6604
|
ctx.effect(() => ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
6357
6605
|
name: "conversation.input.left",
|
|
6358
6606
|
id: "acp-mode",
|