dsh-ntr 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +6 -0
- package/README.md +6 -0
- 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 +4 -3
- 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/README.en.md
CHANGED
|
@@ -58,6 +58,8 @@ dsh plugin --profile web add ./
|
|
|
58
58
|
|
|
59
59
|
To upgrade later: `git pull` → `npm run build` → restart `dsh web`. `add .` installs the directory as `link:`, so a rebuild plus a restart picks up the changes — no re-add needed.
|
|
60
60
|
|
|
61
|
+
> Before switching from the source install to the npm/market build, uninstall the old version and delete `node_modules/dsh-ntr` from the profile. pnpm's hoisted layout leaves the linked checkout's `node_modules` inside the package directory, and the stale dependencies shadow the newly installed SDK — see the `EPERM ... zod` entry in the FAQ.
|
|
62
|
+
|
|
61
63
|
> `dsh plugin` is a pnpm forwarder — `--profile <name>` is required, and `add .` installs the current directory as `link:`. The official desktop app's profile is owned by its own plugin manager; don't run the CLI against it.
|
|
62
64
|
|
|
63
65
|
---
|
|
@@ -194,6 +196,10 @@ That's the agent's own permission mode; the option text comes straight from it.
|
|
|
194
196
|
|
|
195
197
|
Normal. Mode lists are declared by each agent itself (sent via `session/set_mode`); the plugin lists them verbatim — no unified translation, no defaults filled in. To change the wording, edit the agent's own config.
|
|
196
198
|
|
|
199
|
+
**Activation fails with `EPERM ... zod` after switching from the source install to npm / the market?**
|
|
200
|
+
|
|
201
|
+
That's stale dependency state, not a missing plugin file: the failing path will contain `node_modules/dsh-ntr/node_modules/.pnpm/...`. The source install uses `link:` to your checkout; pnpm's hoisted layout replaces the package directory with the registry files but leaves the link target's `node_modules` behind. That old tree (an older `@agentclientprotocol/sdk` plus its broken `zod` symlink) shadows the freshly installed dependency, so DSH's import hits the bad link and fails with `EPERM`. Fix: uninstall `dsh-ntr` in the plugin manager, remove `~/.dsh/profiles/<profile>/node_modules/dsh-ntr` (or just its nested `node_modules`), then reinstall and restart. For the desktop app's profile, uninstall/reinstall from the app's Plugins page instead of the CLI.
|
|
202
|
+
|
|
197
203
|
---
|
|
198
204
|
|
|
199
205
|
## What it can do (feature list)
|
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ dsh plugin --profile web add ./
|
|
|
58
58
|
|
|
59
59
|
之后升级:`git pull` → `npm run build` → 重启 `dsh web`。`add .` 以 `link:` 方式安装,重建后重启即生效,不需要重新 add。
|
|
60
60
|
|
|
61
|
+
> 从源码安装切到 npm / 市场版前,先在插件管理器里卸载旧版,并删掉 profile 里的 `node_modules/dsh-ntr`。pnpm 的 hoisted 布局会把 `link:` 目标里的 `node_modules` 留在包目录内,旧依赖会遮蔽新装的 SDK,典型报错见「常见问题」里的 `EPERM ... zod`。
|
|
62
|
+
|
|
61
63
|
> `dsh plugin` 是 pnpm 转发器,`--profile <name>` 必填;`add .` 会把当前目录以 `link:` 方式装进 profile。官方桌面 App 的 profile 由它自己的插件管理器掌管,不要对着它的 profile 跑命令行。
|
|
62
64
|
|
|
63
65
|
---
|
|
@@ -194,6 +196,10 @@ agent 要动敏感的东西时会问你。审批卡直接出现在聊天输入
|
|
|
194
196
|
|
|
195
197
|
正常。模式清单由每个 agent 自己声明(`session/set_mode` 下发),插件照原样列出,不统一翻译、也不补默认值。想改叫法要在 agent 自己的配置里改。
|
|
196
198
|
|
|
199
|
+
**从源码安装切到 npm / 市场版后,启用时报 `EPERM ... zod`?**
|
|
200
|
+
|
|
201
|
+
这是旧依赖残留,不是插件缺文件:错误路径里会出现 `node_modules/dsh-ntr/node_modules/.pnpm/...`。源码安装用 `link:` 指向你的仓库;pnpm 的 hoisted 布局在切到 registry 版时会把包目录换成真实文件,但会留下链接目标里的 `node_modules`。这份旧树(旧版 `@agentclientprotocol/sdk` 及其失效的 `zod` 软链)会遮蔽新装的依赖,DSH 导入时 stat 到坏链就报 `EPERM`。处理:在插件管理器里卸载 `dsh-ntr`,删除 `~/.dsh/profiles/<profile>/node_modules/dsh-ntr`(或只删它下面的 `node_modules`),再重装并重启。桌面 App 的 profile 请直接在 App 的插件页卸载/重装,不要用命令行。
|
|
202
|
+
|
|
197
203
|
---
|
|
198
204
|
|
|
199
205
|
## 它能做什么(能力清单)
|
package/lib/client/acp-ui.js
CHANGED
|
@@ -230,6 +230,47 @@ export function useAcpMenu() {
|
|
|
230
230
|
return () => { menuListeners.delete(cb); };
|
|
231
231
|
}, () => menuState);
|
|
232
232
|
}
|
|
233
|
+
// ---- The NTR settings dialog ----
|
|
234
|
+
/**
|
|
235
|
+
* Open state of the frame-wide NTR settings dialog (`settings-dialog.tsx`).
|
|
236
|
+
*
|
|
237
|
+
* Module-level because the two ends live in unrelated slot trees: the trigger
|
|
238
|
+
* is the composer's agent base (a session-scoped model-seat entry) and the
|
|
239
|
+
* dialog itself rides the root `shell.overlay` layer. A React context cannot
|
|
240
|
+
* span that boundary, so this is the same tiny external-store seam the menu
|
|
241
|
+
* uses above.
|
|
242
|
+
*/
|
|
243
|
+
let settingsDialogOpen = false;
|
|
244
|
+
const settingsDialogListeners = new Set();
|
|
245
|
+
function emitSettingsDialog() {
|
|
246
|
+
for (const listener of [...settingsDialogListeners])
|
|
247
|
+
listener();
|
|
248
|
+
}
|
|
249
|
+
/** Open the NTR settings dialog (the empty agent roster's way forward). */
|
|
250
|
+
export function openAcpSettingsDialog() {
|
|
251
|
+
if (settingsDialogOpen)
|
|
252
|
+
return;
|
|
253
|
+
settingsDialogOpen = true;
|
|
254
|
+
emitSettingsDialog();
|
|
255
|
+
}
|
|
256
|
+
/** Close the NTR settings dialog (close button, mask click, Escape). */
|
|
257
|
+
export function closeAcpSettingsDialog() {
|
|
258
|
+
if (!settingsDialogOpen)
|
|
259
|
+
return;
|
|
260
|
+
settingsDialogOpen = false;
|
|
261
|
+
emitSettingsDialog();
|
|
262
|
+
}
|
|
263
|
+
/** Current dialog state, for tests and non-React readers. */
|
|
264
|
+
export function acpSettingsDialogIsOpen() {
|
|
265
|
+
return settingsDialogOpen;
|
|
266
|
+
}
|
|
267
|
+
/** Subscribe to the dialog state (the root overlay entry reads it). */
|
|
268
|
+
export function useAcpSettingsDialogOpen() {
|
|
269
|
+
return useSyncExternalStore(cb => {
|
|
270
|
+
settingsDialogListeners.add(cb);
|
|
271
|
+
return () => { settingsDialogListeners.delete(cb); };
|
|
272
|
+
}, acpSettingsDialogIsOpen);
|
|
273
|
+
}
|
|
233
274
|
/**
|
|
234
275
|
* Pure popup placement, viewport coordinates: the menu docks with its right
|
|
235
276
|
* edge under the trigger's, `gap`px above it — and only then clamps to the
|
|
@@ -527,6 +568,26 @@ const STYLE = `
|
|
|
527
568
|
.ntr-acp-empty-hint { color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12px; }
|
|
528
569
|
.ntr-acp-seat-diag { font-family: var(--dsw-alias-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); opacity: 0.72; word-break: break-word; }
|
|
529
570
|
.ntr-acp-empty-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
|
|
571
|
+
/* ---- NTR settings dialog (shell.overlay) ---- */
|
|
572
|
+
/* Frame-wide surface opened from the agent base's empty-roster menu. The
|
|
573
|
+
overlay layer itself is click-through, so the layer opts back in and blocks
|
|
574
|
+
the app while it is up. */
|
|
575
|
+
.ntr-acp-dialog-layer { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; pointer-events: auto; }
|
|
576
|
+
.ntr-acp-dialog-mask { position: absolute; inset: 0; background: var(--dsw-alias-bg-mask-1, rgba(0, 0, 0, 0.4)); }
|
|
577
|
+
.ntr-acp-dialog {
|
|
578
|
+
position: relative; display: flex; flex-direction: column; overflow: hidden;
|
|
579
|
+
width: min(920px, calc(100vw - 48px)); height: min(820px, calc(100vh - 48px));
|
|
580
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: inherit;
|
|
581
|
+
border-radius: 24px; box-shadow: var(--dsw-elevation-prominent, 0 24px 60px rgba(0, 0, 0, 0.25));
|
|
582
|
+
}
|
|
583
|
+
.ntr-acp-dialog-head { flex: none; display: flex; align-items: center; gap: 8px; padding: 16px 16px 8px 24px; }
|
|
584
|
+
.ntr-acp-dialog-title { flex: 1; font-size: 16px; font-weight: 600; }
|
|
585
|
+
.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; }
|
|
586
|
+
.ntr-acp-dialog-close:hover { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
|
|
587
|
+
.ntr-acp-dialog-body { flex: 1; min-height: 0; overflow: auto; }
|
|
588
|
+
/* The section brings its own page scroll for the settings shell's content
|
|
589
|
+
column; the dialog body is the scroll owner here. */
|
|
590
|
+
.ntr-acp-dialog-body > .dwa-root { max-width: none; overflow: visible; }
|
|
530
591
|
`;
|
|
531
592
|
let styleMounted = false;
|
|
532
593
|
/** Mount the shared stylesheet once (same pattern as the settings panel's). */
|
package/lib/client/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { AcpSettingsSection } from "./panel.js";
|
|
|
10
10
|
import { AcpComposerMode } from "./composer-mode.js";
|
|
11
11
|
import { AcpComposerAgentTag, AcpSessionControls, AcpSessionMenu } from "./session-controls.js";
|
|
12
12
|
import { AcpConversationView } from "./acp-view.js";
|
|
13
|
+
import { AcpSettingsDialog } from "./settings-dialog.js";
|
|
13
14
|
import { createModelSeatClaim, createPermissionSeatClaim, MODEL_SEAT_KEY, PERMISSION_SEAT_KEY, } from "./model-seat.js";
|
|
14
15
|
import { acpModeFromPresetEvent, rowAgentPreset, stagedIsAcpMode, stagedSessionOf, } from "./acp-ui.js";
|
|
15
16
|
import { createSlashIsolation, slashAcpVerdict } from "./slash-isolation.js";
|
|
@@ -108,6 +109,18 @@ export function apply(ctx) {
|
|
|
108
109
|
label: () => 'NTR',
|
|
109
110
|
inject: () => ({ acp: store }),
|
|
110
111
|
}, AcpSettingsSection)), 'dsh-ntr: settings section');
|
|
112
|
+
// The same NTR settings, opened from the composer when the agent roster is
|
|
113
|
+
// empty (the agent base's picker action). It rides the frame's overlay layer
|
|
114
|
+
// because the composer seat and the dialog live in unrelated slot trees; the
|
|
115
|
+
// host settings shell has no programmatic open seam, so the dialog mounts the
|
|
116
|
+
// same section the shell mounts (see settings-dialog.tsx). Renders nothing
|
|
117
|
+
// while closed.
|
|
118
|
+
ctx.effect(() => ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
119
|
+
name: 'shell.overlay',
|
|
120
|
+
id: 'acp-settings-dialog',
|
|
121
|
+
order: 30,
|
|
122
|
+
inject: () => ({ acp: store }),
|
|
123
|
+
}, AcpSettingsDialog)), 'dsh-ntr: NTR settings dialog');
|
|
111
124
|
// The agent's OWN modes sit in the composer tool row — the seat the slot
|
|
112
125
|
// contract reserves for exactly this ("entries sit beside that chrome, never
|
|
113
126
|
// replace it"). They are a different layer from everything the host controls:
|
package/lib/client/panel.js
CHANGED
|
@@ -138,16 +138,18 @@ const STYLE = `
|
|
|
138
138
|
.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); }
|
|
139
139
|
.dwa-inline-setting .dwa-switch { flex-shrink: 0; margin-top: 1px; }
|
|
140
140
|
.dwa-setting-copy { color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
141
|
-
/*
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.dwa-model-transfer { display: grid; grid-template-columns:
|
|
145
|
-
/* Tall enough for a comfortable selection (about ten rows) before scrolling
|
|
146
|
-
|
|
147
|
-
slack on the shorter side instead of a whole empty half. */
|
|
141
|
+
/* Stacked, not side by side: the rows carry name, id and credit, and a
|
|
142
|
+
half-width pane truncated them at every realistic settings width. Each list
|
|
143
|
+
gets the whole column instead. */
|
|
144
|
+
.dwa-model-transfer { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
|
|
145
|
+
/* Tall enough for a comfortable selection (about ten rows) before scrolling;
|
|
146
|
+
the cap keeps the two stacked panes from pushing the rest of the card away. */
|
|
148
147
|
.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; }
|
|
149
148
|
.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); }
|
|
150
149
|
.dwa-model-pane-head .dwa-btn { margin-left: auto; min-height: 26px; padding: 2px 8px; font-size: var(--ntr-font-xs); }
|
|
150
|
+
.dwa-model-pane-head .dwa-caret-btn { flex: none; width: 22px; height: 22px; background: transparent; }
|
|
151
|
+
.dwa-model-pane-head .dwa-caret-btn:hover { background: var(--dsw-alias-interactive-bg-active, #e5e7eb); }
|
|
152
|
+
.dwa-model-pane.is-collapsed .dwa-model-list { display: none; }
|
|
151
153
|
.dwa-model-list { display: flex; flex-direction: column; gap: 2px; padding: 5px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
152
154
|
.dwa-model-section { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; font-size: 12px; }
|
|
153
155
|
.dwa-model-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
@@ -164,7 +166,19 @@ const STYLE = `
|
|
|
164
166
|
.dwa-model-row.is-on { background: #3b82f614; color: #2563eb; }
|
|
165
167
|
.dwa-model-row-mark { width: 12px; flex: none; text-align: center; opacity: 0.75; }
|
|
166
168
|
.dwa-model-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
167
|
-
|
|
169
|
+
/* Name and credit keep their room; the id is the only cell that collapses. */
|
|
170
|
+
.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; }
|
|
171
|
+
/* The trailing 14px is reserved for the fade mask: while the id fits, the
|
|
172
|
+
gradient only covers empty padding; once it overflows, its tail dissolves
|
|
173
|
+
instead of ending in a hard ellipsis. armModelIdMarquee reads the same
|
|
174
|
+
padding to scroll the full text under the pointer. */
|
|
175
|
+
.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); }
|
|
176
|
+
.dwa-model-row-value-text { display: inline-block; white-space: nowrap; }
|
|
177
|
+
.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; }
|
|
178
|
+
@keyframes dwa-model-id-marquee {
|
|
179
|
+
from { transform: translateX(0); }
|
|
180
|
+
to { transform: translateX(var(--dwa-id-shift, 0px)); }
|
|
181
|
+
}
|
|
168
182
|
.dwa-model-row-remove { flex: none; opacity: 0.5; padding: 0 2px; }
|
|
169
183
|
.dwa-model-row:hover .dwa-model-row-remove { opacity: 1; }
|
|
170
184
|
.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; }
|
|
@@ -181,7 +195,6 @@ const STYLE = `
|
|
|
181
195
|
.dwa-page-header { flex-direction: column; }
|
|
182
196
|
.dwa-page-actions { width: 100%; }
|
|
183
197
|
.dwa-page-actions .dwa-btn { width: 100%; }
|
|
184
|
-
.dwa-model-transfer { grid-template-columns: 1fr; }
|
|
185
198
|
.dwa-model-toolbar-actions { width: 100%; }
|
|
186
199
|
.dwa-model-search { flex: 1 1 auto; width: auto; }
|
|
187
200
|
.dwa-subsection-head, .dwa-preference-row { align-items: flex-start; flex-direction: column; }
|
|
@@ -189,6 +202,7 @@ const STYLE = `
|
|
|
189
202
|
}
|
|
190
203
|
@media (prefers-reduced-motion: reduce) {
|
|
191
204
|
.dwa-root *, .dwa-root *::before, .dwa-root *::after { transition: none !important; }
|
|
205
|
+
.dwa-model-row:hover .dwa-model-row-value-text { animation: none !important; }
|
|
192
206
|
}
|
|
193
207
|
`;
|
|
194
208
|
let styleMounted = false;
|
|
@@ -678,6 +692,23 @@ function RowMenu({ acp, canStop, onStop, onRemove, }) {
|
|
|
678
692
|
void onRemove();
|
|
679
693
|
}, children: acp.label('确认移除?', 'Confirm remove?') }), _jsx("button", { type: "button", className: "dwa-btn", onClick: () => setConfirming(false), children: acp.label('取消', 'Cancel') })] })) : (_jsx("button", { type: "button", role: "menuitem", className: "dwa-btn dwa-menu-item dwa-menu-item-danger", onClick: () => setConfirming(true), children: acp.label('移除配置', 'Remove') }))] }))] }));
|
|
680
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* Measure the id cell on row hover so its CSS marquee can reach the end of the
|
|
697
|
+
* text. The id is the only cell that yields width, and the panel is resizable,
|
|
698
|
+
* so the overflow is re-read on every entry rather than cached. The animation
|
|
699
|
+
* itself lives in CSS and only runs while the row is hovered; `title` on the
|
|
700
|
+
* row stays the fallback for reduced-motion users.
|
|
701
|
+
*/
|
|
702
|
+
function armModelIdMarquee(row) {
|
|
703
|
+
const view = row.querySelector('.dwa-model-row-value');
|
|
704
|
+
const text = view?.querySelector('.dwa-model-row-value-text');
|
|
705
|
+
if (view === null || view === undefined || text === null || text === undefined)
|
|
706
|
+
return;
|
|
707
|
+
const fade = Number.parseFloat(getComputedStyle(view).paddingRight) || 0;
|
|
708
|
+
const shift = Math.max(0, text.offsetWidth - (view.clientWidth - fade));
|
|
709
|
+
row.style.setProperty('--dwa-id-shift', `${String(-shift)}px`);
|
|
710
|
+
row.style.setProperty('--dwa-id-duration', `${String(Math.min(4, Math.max(1.2, shift / 30)))}s`);
|
|
711
|
+
}
|
|
681
712
|
/**
|
|
682
713
|
* Model / thought-level catalog display plus the native-selector visibility
|
|
683
714
|
* filter, as a two-pane transfer list: every known model on the left, the
|
|
@@ -718,6 +749,11 @@ function ModelCatalogBlock({ agentId, detected, acp, active, exposed, onExposedC
|
|
|
718
749
|
const modelChoices = result?.catalog.model?.choices ?? [];
|
|
719
750
|
/** Result of the last click on 「检测可用模型」 (what the refresh changed). */
|
|
720
751
|
const [detectNote, setDetectNote] = useState(undefined);
|
|
752
|
+
// Each list folds on its own so a long selection can be parked while the
|
|
753
|
+
// other pane keeps the full column. The list stays mounted (display-toggled)
|
|
754
|
+
// to keep its scroll position.
|
|
755
|
+
const [allCollapsed, setAllCollapsed] = useState(false);
|
|
756
|
+
const [selectedCollapsed, setSelectedCollapsed] = useState(false);
|
|
721
757
|
// Display read, not a probe: the host answers from its persisted catalog
|
|
722
758
|
// cache, so the panes already list the known models the first time the card
|
|
723
759
|
// opens. Gated on `active` because every configured agent's body stays
|
|
@@ -860,7 +896,7 @@ function ModelCatalogBlock({ agentId, detected, acp, active, exposed, onExposedC
|
|
|
860
896
|
: acp.label('该 Agent 未暴露模型选择器(category: model)。', 'This agent exposes no model selector (category: model).');
|
|
861
897
|
return (_jsxs("div", { children: [!isDefault && (_jsx("div", { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }, children: _jsx("button", { className: "dwa-btn", onClick: () => void acp.setDefaultAgent(agentId), children: acp.label('设为默认智能体', 'Set as default agent') }) })), isDefault && (
|
|
862
898
|
// This default belongs to the plugin, not the deployment model route.
|
|
863
|
-
_jsx("div", { className: "dwa-hint", style: { marginTop: 8 }, children: acp.label('当前为默认智能体:新的 NTR 会话由它服务,不影响其他模式的默认模型。', "This is the default agent for new NTR sessions; other modes' default models are untouched.") })), _jsxs("div", { className: "dwa-inline-setting", style: { marginTop: 12 }, children: [_jsxs("label", { className: "dwa-switch", title: acp.label('Agent 新发布的模型会自动加入右侧已选列表;首次检测到的目录只作为基线,不会一次性全选。', 'Models the agent newly publishes are added to the selected list; the first observed catalog is only a baseline and is not selected wholesale.'), children: [_jsx("input", { type: "checkbox", checked: autoDiscover, onChange: e => void toggleAutoDiscover(e.target.checked), style: { margin: 0, accentColor: '#2563eb' } }), acp.label('自动发现新模型', 'Auto-discover new models')] }), _jsx("span", { className: "dwa-setting-copy", children: acp.label('新增模型自动加入已选列表,现有勾选不受影响。', 'New models join the selected list automatically; existing picks are untouched.') })] }), saveError !== undefined && _jsx("div", { className: "dwa-error", style: { marginTop: 4 }, children: saveError }), probeError !== undefined && (_jsxs("div", { className: "dwa-error", style: { marginTop: 8, wordBreak: 'break-word' }, children: ["\u26A0 ", acp.label('模型检测失败:', 'Model detection failed: '), probeError] })), _jsxs("div", { className: "dwa-model-section", children: [_jsxs("div", { className: "dwa-model-toolbar", children: [_jsxs("span", { className: "dwa-form-label", children: [acp.label('对话中可见的模型', 'Models offered in the conversation'), _jsx("span", { className: "dwa-hint", style: { marginLeft: 6, fontWeight: 400 }, title: acp.label('NTR 模式下的对话只列出这里勾选的模型;未勾选的仍可用——正在生效的那个也会以「· 当前」列出来,其余不列出。', 'The NTR surfaces only list what is checked here; unchecked models still work, and the one currently in force is still listed with a "· current" row — the rest are not offered.'), children: acp.label('(NTR 座位 / 芯片的下拉列表)', '(the NTR seat and chip pickers)') })] }), _jsx("span", { className: "dwa-badge dwa-badge-info", children: selectionBadge }), catalogStatus !== undefined && _jsx("span", { className: "dwa-hint dwa-model-status", children: catalogStatus }), _jsxs("div", { className: "dwa-model-toolbar-actions", children: [_jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", disabled: probing, onClick: () => void detect(), title: acp.label('重新读取 Agent 的模型目录:新增的加入左侧,已下架的从左侧移除;已选列表不受影响。', 'Re-read the agent catalog: newly published models join the left list, withdrawn ones drop out. The selection is untouched.'), children: probing ? acp.label('模型检测中...', 'Probing models...') : acp.label('检测可用模型', 'Detect available models') }), _jsx("input", { className: "dwa-input dwa-model-search", value: search, onChange: e => setSearch(e.target.value), placeholder: acp.label('搜索模型…', 'Search models…') })] })] }), _jsxs("div", { className: "dwa-model-transfer", children: [_jsxs("div", { className:
|
|
899
|
+
_jsx("div", { className: "dwa-hint", style: { marginTop: 8 }, children: acp.label('当前为默认智能体:新的 NTR 会话由它服务,不影响其他模式的默认模型。', "This is the default agent for new NTR sessions; other modes' default models are untouched.") })), _jsxs("div", { className: "dwa-inline-setting", style: { marginTop: 12 }, children: [_jsxs("label", { className: "dwa-switch", title: acp.label('Agent 新发布的模型会自动加入右侧已选列表;首次检测到的目录只作为基线,不会一次性全选。', 'Models the agent newly publishes are added to the selected list; the first observed catalog is only a baseline and is not selected wholesale.'), children: [_jsx("input", { type: "checkbox", checked: autoDiscover, onChange: e => void toggleAutoDiscover(e.target.checked), style: { margin: 0, accentColor: '#2563eb' } }), acp.label('自动发现新模型', 'Auto-discover new models')] }), _jsx("span", { className: "dwa-setting-copy", children: acp.label('新增模型自动加入已选列表,现有勾选不受影响。', 'New models join the selected list automatically; existing picks are untouched.') })] }), saveError !== undefined && _jsx("div", { className: "dwa-error", style: { marginTop: 4 }, children: saveError }), probeError !== undefined && (_jsxs("div", { className: "dwa-error", style: { marginTop: 8, wordBreak: 'break-word' }, children: ["\u26A0 ", acp.label('模型检测失败:', 'Model detection failed: '), probeError] })), _jsxs("div", { className: "dwa-model-section", children: [_jsxs("div", { className: "dwa-model-toolbar", children: [_jsxs("span", { className: "dwa-form-label", children: [acp.label('对话中可见的模型', 'Models offered in the conversation'), _jsx("span", { className: "dwa-hint", style: { marginLeft: 6, fontWeight: 400 }, title: acp.label('NTR 模式下的对话只列出这里勾选的模型;未勾选的仍可用——正在生效的那个也会以「· 当前」列出来,其余不列出。', 'The NTR surfaces only list what is checked here; unchecked models still work, and the one currently in force is still listed with a "· current" row — the rest are not offered.'), children: acp.label('(NTR 座位 / 芯片的下拉列表)', '(the NTR seat and chip pickers)') })] }), _jsx("span", { className: "dwa-badge dwa-badge-info", children: selectionBadge }), catalogStatus !== undefined && _jsx("span", { className: "dwa-hint dwa-model-status", children: catalogStatus }), _jsxs("div", { className: "dwa-model-toolbar-actions", children: [_jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", disabled: probing, onClick: () => void detect(), title: acp.label('重新读取 Agent 的模型目录:新增的加入左侧,已下架的从左侧移除;已选列表不受影响。', 'Re-read the agent catalog: newly published models join the left list, withdrawn ones drop out. The selection is untouched.'), children: probing ? acp.label('模型检测中...', 'Probing models...') : acp.label('检测可用模型', 'Detect available models') }), _jsx("input", { className: "dwa-input dwa-model-search", value: search, onChange: e => setSearch(e.target.value), placeholder: acp.label('搜索模型…', 'Search models…') })] })] }), _jsxs("div", { className: "dwa-model-transfer", children: [_jsxs("div", { className: `dwa-model-pane${allCollapsed ? ' is-collapsed' : ''}`, children: [_jsxs("div", { className: "dwa-model-pane-head", children: [_jsx("span", { children: acp.label('全部模型', 'All models') }), result !== undefined && _jsx("span", { className: "dwa-badge", children: modelChoices.length }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", disabled: visibleChoices.length === 0, onClick: addVisible, title: acp.label('把当前搜索范围内的模型全部加入右侧', 'Add every model matching the current search'), children: acp.label('全部加入', 'Add all') }), _jsx("button", { type: "button", className: "dwa-caret-btn", "aria-expanded": !allCollapsed, onClick: () => setAllCollapsed(value => !value), title: allCollapsed ? acp.label('展开全部模型', 'Expand all models') : acp.label('收起全部模型', 'Collapse all models'), children: _jsx("span", { className: "dwa-caret", "aria-hidden": true, children: allCollapsed ? '▸' : '▾' }) })] }), _jsxs("div", { className: "dwa-model-list", children: [visibleChoices.map(choice => {
|
|
864
900
|
const checked = selected.has(choice.value);
|
|
865
901
|
// group 前缀仅来自 agent 返回的 SessionConfigSelectGroup(见 impl.ts
|
|
866
902
|
// pushCatalogChoice);供应商可见性兜底靠原样展示 value,严禁从
|
|
@@ -868,6 +904,6 @@ function ModelCatalogBlock({ agentId, detected, acp, active, exposed, onExposedC
|
|
|
868
904
|
const title = choice.description === undefined
|
|
869
905
|
? choice.value
|
|
870
906
|
: `${choice.value}\n${choice.description}`;
|
|
871
|
-
return (_jsxs("button", { type: "button", className: `dwa-model-row${checked ? ' is-on' : ''}`, title: title, onClick: () => toggleChoice(choice.value), children: [_jsx("span", { className: "dwa-model-row-mark", children: checked ? '✓' : '+' }), _jsxs("span", { className: "dwa-model-row-name", children: [choice.group !== undefined && _jsxs("span", { style: { opacity: 0.7 }, children: [choice.group, " \u00B7 "] }), choice.name] }), choice.value !== choice.name && (_jsx("span", { className: "dwa-model-row-value", children: choice.value }))] }, choice.value));
|
|
872
|
-
}), leftEmptyCopy !== undefined && _jsx("span", { className: "dwa-hint dwa-model-empty", children: leftEmptyCopy })] })] }), _jsxs("div", { className:
|
|
907
|
+
return (_jsxs("button", { type: "button", className: `dwa-model-row${checked ? ' is-on' : ''}`, title: title, onClick: () => toggleChoice(choice.value), onPointerEnter: event => { armModelIdMarquee(event.currentTarget); }, children: [_jsx("span", { className: "dwa-model-row-mark", children: checked ? '✓' : '+' }), _jsxs("span", { className: "dwa-model-row-name", children: [choice.group !== undefined && _jsxs("span", { style: { opacity: 0.7 }, children: [choice.group, " \u00B7 "] }), choice.name] }), choice.value !== choice.name && (_jsx("span", { className: "dwa-model-row-value", children: _jsx("span", { className: "dwa-model-row-value-text", children: choice.value }) })), choice.description !== undefined && choice.description !== choice.group && (_jsx("span", { className: "dwa-model-row-desc", children: choice.description }))] }, choice.value));
|
|
908
|
+
}), leftEmptyCopy !== undefined && _jsx("span", { className: "dwa-hint dwa-model-empty", children: leftEmptyCopy })] })] }), _jsxs("div", { className: `dwa-model-pane${selectedCollapsed ? ' is-collapsed' : ''}`, children: [_jsxs("div", { className: "dwa-model-pane-head", children: [_jsx("span", { children: acp.label('已选模型', 'Selected models') }), _jsx("span", { className: "dwa-badge dwa-badge-info", children: basis.length }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", disabled: basis.length === 0, onClick: clearAll, children: acp.label('清空', 'Clear') }), _jsx("button", { type: "button", className: "dwa-caret-btn", "aria-expanded": !selectedCollapsed, onClick: () => setSelectedCollapsed(value => !value), title: selectedCollapsed ? acp.label('展开已选模型', 'Expand selected models') : acp.label('收起已选模型', 'Collapse selected models'), children: _jsx("span", { className: "dwa-caret", "aria-hidden": true, children: selectedCollapsed ? '▸' : '▾' }) })] }), _jsxs("div", { className: "dwa-model-list", children: [selectedChoices.length === 0 && (_jsx("span", { className: "dwa-hint dwa-model-empty", children: acp.label('还没有选择模型(点左侧的模型加入)', 'Nothing selected yet — pick models on the left') })), selectedChoices.map(choice => (_jsxs("button", { type: "button", className: "dwa-model-row is-on", title: choice.description === undefined ? choice.value : `${choice.value}\n${choice.description}`, onClick: () => toggleChoice(choice.value), onPointerEnter: event => { armModelIdMarquee(event.currentTarget); }, children: [_jsxs("span", { className: "dwa-model-row-name", children: [choice.group !== undefined && _jsxs("span", { style: { opacity: 0.7 }, children: [choice.group, " \u00B7 "] }), choice.name] }), choice.value !== choice.name && (_jsx("span", { className: "dwa-model-row-value", children: _jsx("span", { className: "dwa-model-row-value-text", children: choice.value }) })), choice.description !== undefined && choice.description !== choice.group && (_jsx("span", { className: "dwa-model-row-desc", children: choice.description })), _jsx("span", { className: "dwa-model-row-remove", "aria-hidden": "true", children: "\u00D7" })] }, choice.value)))] })] })] }), result?.catalog.thought !== undefined && (_jsxs("div", { style: { display: 'flex', gap: 4, flexWrap: 'wrap', alignItems: 'center' }, children: [_jsx("span", { className: "dwa-form-label", children: acp.label('思考等级', 'Thought Levels') }), result.catalog.thought.choices.map(choice => (_jsx("span", { className: "dwa-badge", title: choice.description, children: choice.name }, choice.value)))] }))] })] }));
|
|
873
909
|
}
|
|
@@ -50,13 +50,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
50
50
|
* different slots, so they share that module's phase clock rather than trying
|
|
51
51
|
* to animate across slot boundaries.
|
|
52
52
|
*
|
|
53
|
-
* It is deliberately quiet:
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* It is deliberately quiet: outside ACP mode the gate never mounts it, and
|
|
54
|
+
* before the host has answered the agent roster it renders nothing. Once the
|
|
55
|
+
* roster is KNOWN to be empty the base stays and its picker carries the
|
|
56
|
+
* "open NTR settings" action — an NTR conversation with nothing configured
|
|
57
|
+
* still has a way forward instead of an empty cell. The ACP view tab remains
|
|
58
|
+
* the entry point for conversations no agent has served.
|
|
57
59
|
*/
|
|
58
60
|
import { useEffect, useState } from 'react';
|
|
59
|
-
import { AcpChip, AcpMenu, AcpShatterParts, buildAcpModelId, closeAcpMenu, ensureAcpStyle, openAcpMenu, setAcpMenuError, splitAcpModelId, useAcpMenu, useUi, } from "./acp-ui.js";
|
|
61
|
+
import { AcpChip, AcpMenu, AcpShatterParts, buildAcpModelId, closeAcpMenu, ensureAcpStyle, openAcpMenu, openAcpSettingsDialog, setAcpMenuError, splitAcpModelId, useAcpMenu, useUi, } from "./acp-ui.js";
|
|
60
62
|
import { playAgentLockAnim, useAgentLockPhase } from "./agent-lock-anim.js";
|
|
61
63
|
const STATE_COLORS = {
|
|
62
64
|
ready: '#22c55e',
|
|
@@ -105,6 +107,18 @@ function routeAgentId(boundAgentId, bound, agents, defaultAgent) {
|
|
|
105
107
|
return defaultAgent;
|
|
106
108
|
return undefined;
|
|
107
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Whether the agent base has anything honest to show.
|
|
112
|
+
*
|
|
113
|
+
* An empty roster has two meanings, and they must not be conflated: before the
|
|
114
|
+
* host answers `agents/list` it means "not read yet" (rendering a setup entry
|
|
115
|
+
* there would accuse a correctly configured deployment on every page load),
|
|
116
|
+
* while after the answer it means "nothing is configured" — exactly the state
|
|
117
|
+
* that must keep the base and offer the way into NTR settings.
|
|
118
|
+
*/
|
|
119
|
+
export function agentBaseHasData(bound, agents, rosterLoaded) {
|
|
120
|
+
return bound !== undefined || agents.length > 0 || rosterLoaded;
|
|
121
|
+
}
|
|
108
122
|
/**
|
|
109
123
|
* The agent this conversation is LOCKED to, if its first ACP turn has run.
|
|
110
124
|
*
|
|
@@ -294,14 +308,18 @@ export function advancedOptions(catalog, choice) {
|
|
|
294
308
|
* The tooltip claims only what the menu behind the chip does: switch the agent.
|
|
295
309
|
* The agent's models and thought levels have chips of their own (user decision,
|
|
296
310
|
* 2026-10-17), so the previous wording — "switch the agent, its model and the
|
|
297
|
-
* thought level" — promised a menu that holds the roster alone.
|
|
311
|
+
* thought level" — promised a menu that holds the roster alone. An EMPTY
|
|
312
|
+
* roster is its own case: the menu cannot switch to anything, it offers the
|
|
313
|
+
* way into NTR settings, so the tooltip says that instead of promising a pick.
|
|
298
314
|
*/
|
|
299
|
-
export function agentBaseChip(route, routed, acp) {
|
|
315
|
+
export function agentBaseChip(route, routed, acp, rosterEmpty = false) {
|
|
300
316
|
const label = route.agentTitle === undefined
|
|
301
317
|
? (routed ? 'NTR' : acp.label('选择 Agent', 'Choose agent'))
|
|
302
318
|
: `NTR · ${route.agentTitle}`;
|
|
303
319
|
const title = route.agentId === undefined
|
|
304
|
-
?
|
|
320
|
+
? rosterEmpty
|
|
321
|
+
? acp.label('这条会话当前没有 NTR agent;点开可前往 NTR 设置添加', 'No NTR agent is serving this conversation — open to set one up in NTR settings')
|
|
322
|
+
: acp.label('这条会话当前没有 NTR agent 服务;点开可选择 agent', 'No NTR agent is serving this conversation right now — open to choose one')
|
|
305
323
|
: acp.label('这条会话由外接 NTR agent 服务;点开可切换 agent', 'This conversation is served by an external NTR agent — open to switch the agent');
|
|
306
324
|
return { label, title };
|
|
307
325
|
}
|
|
@@ -424,9 +442,12 @@ export function AcpSessionControls(props) {
|
|
|
424
442
|
const route = describeRoute(agents, bound,
|
|
425
443
|
// A locked conversation has no default fallback: the lock is the answer.
|
|
426
444
|
lockedTo === undefined ? ui.defaultAgent : undefined, catalog, lockedTo ?? ui.chatBoundAgent[sessionId], choice);
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
445
|
+
// An empty roster is the SETUP case, not "no story": the base stays and its
|
|
446
|
+
// picker carries the way into NTR settings. Only the window before the host
|
|
447
|
+
// has answered `agents/list` stays blank — `agents: []` there means "not read
|
|
448
|
+
// yet", and the setup entry would accuse a configured deployment (see
|
|
449
|
+
// `agentBaseHasData`).
|
|
450
|
+
if (!agentBaseHasData(bound, agents, ui.agentsLoaded))
|
|
430
451
|
return null;
|
|
431
452
|
// Set up = an agent serves this conversation: a live session, a persisted
|
|
432
453
|
// pick, the host's optimistic binding, OR the plugin default the guard's
|
|
@@ -471,7 +492,7 @@ export function AcpSessionControls(props) {
|
|
|
471
492
|
// agent's extra options never push the primary axes around. Once the first
|
|
472
493
|
// turn locks the conversation the base retires (2026-10-20) and the
|
|
473
494
|
// model / thought pair inherits the right edge.
|
|
474
|
-
return (_jsxs("span", { className: "ntr-acp-cluster", children: [showAdvanced && (_jsx(AcpChip, { label: `${acp.label('高级', 'Advanced')} · ${String(extras.length)}`, title: acp.label(`agent 的额外配置项:${extras.map(option => option.name).join('、')}`, `extra agent config options: ${extras.map(option => option.name).join(', ')}`), active: menu?.sessionId === sessionId && menu.kind === 'advanced', onClick: (trigger) => { open('advanced', trigger); } })), showModel && modelLabel !== undefined && (_jsx(AcpChip, { label: modelLabel, title: acp.label('agent 自己的模型目录', "the agent's own model directory"), active: menu?.sessionId === sessionId && menu.kind === 'model', onClick: (trigger) => { open('model', trigger); } })), showThought && (_jsx(AcpChip, { label: `${acp.label('思考', 'think')} · ${route.effortLabel ?? acp.label('默认', 'default')}`, title: acp.label('思考等级(agent 的 thought_level)', "Thought level (the agent's thought_level)"), active: menu?.sessionId === sessionId && menu.kind === 'thought', onClick: (trigger) => { open('thought', trigger); } })), showAgentBase && (_jsx(AcpChip, { ...agentBaseChip(route, routed, acp), active: routed, className: shattering ? 'ntr-acp-shatter' : undefined, onClick: shattering ? undefined : (trigger) => { open('agent', trigger); }, children: shattering && _jsx(AcpShatterParts, { mode: "out" }) }))] }));
|
|
495
|
+
return (_jsxs("span", { className: "ntr-acp-cluster", children: [showAdvanced && (_jsx(AcpChip, { label: `${acp.label('高级', 'Advanced')} · ${String(extras.length)}`, title: acp.label(`agent 的额外配置项:${extras.map(option => option.name).join('、')}`, `extra agent config options: ${extras.map(option => option.name).join(', ')}`), active: menu?.sessionId === sessionId && menu.kind === 'advanced', onClick: (trigger) => { open('advanced', trigger); } })), showModel && modelLabel !== undefined && (_jsx(AcpChip, { label: modelLabel, title: acp.label('agent 自己的模型目录', "the agent's own model directory"), active: menu?.sessionId === sessionId && menu.kind === 'model', onClick: (trigger) => { open('model', trigger); } })), showThought && (_jsx(AcpChip, { label: `${acp.label('思考', 'think')} · ${route.effortLabel ?? acp.label('默认', 'default')}`, title: acp.label('思考等级(agent 的 thought_level)', "Thought level (the agent's thought_level)"), active: menu?.sessionId === sessionId && menu.kind === 'thought', onClick: (trigger) => { open('thought', trigger); } })), showAgentBase && (_jsx(AcpChip, { ...agentBaseChip(route, routed, acp, agents.length === 0), active: routed, className: shattering ? 'ntr-acp-shatter' : undefined, onClick: shattering ? undefined : (trigger) => { open('agent', trigger); }, children: shattering && _jsx(AcpShatterParts, { mode: "out" }) }))] }));
|
|
475
496
|
}
|
|
476
497
|
/**
|
|
477
498
|
* The model picker's explicit refresh row. It is an ACTION, not a model: the
|
|
@@ -481,6 +502,13 @@ export function AcpSessionControls(props) {
|
|
|
481
502
|
* `agentId@value`.
|
|
482
503
|
*/
|
|
483
504
|
export const REFRESH_CATALOG_ITEM = '__refresh-catalog__';
|
|
505
|
+
/**
|
|
506
|
+
* The empty roster's setup action. Like the model picker's refresh row it is an
|
|
507
|
+
* ACTION inside the picker, not an agent: agent ids match
|
|
508
|
+
* `[a-z0-9][a-z0-9._-]{0,63}` (no leading underscore), so no real id can
|
|
509
|
+
* collide with it.
|
|
510
|
+
*/
|
|
511
|
+
export const OPEN_SETTINGS_ITEM = '__open-settings__';
|
|
484
512
|
/** The agent roster rows: one row per configured ACP agent, current one marked. */
|
|
485
513
|
function agentSection(context) {
|
|
486
514
|
const { acp, route } = context;
|
|
@@ -492,12 +520,21 @@ function agentSection(context) {
|
|
|
492
520
|
active: route.agentId === agent.id,
|
|
493
521
|
}));
|
|
494
522
|
if (items.length === 0) {
|
|
523
|
+
// The base stays on an empty roster (see `agentBaseHasData`), so this menu
|
|
524
|
+
// is the setup surface: a status row that says what is missing, then an
|
|
525
|
+
// ACTION row that opens the NTR settings. The action is a row of its own
|
|
526
|
+
// because a disabled status row cannot be the way forward.
|
|
495
527
|
items.push({
|
|
496
528
|
id: 'none',
|
|
497
529
|
label: acp.label('还没有配置 NTR agent', 'No NTR agent configured yet'),
|
|
498
|
-
detail: acp.label('
|
|
530
|
+
detail: acp.label('添加并启用一个 agent 后即可开始对话', 'Add and enable an agent to start'),
|
|
499
531
|
disabled: true,
|
|
500
532
|
});
|
|
533
|
+
items.push({
|
|
534
|
+
id: OPEN_SETTINGS_ITEM,
|
|
535
|
+
label: acp.label('打开 NTR 设置', 'Open NTR settings'),
|
|
536
|
+
detail: acp.label('添加或启用 agent', 'Add or enable an agent'),
|
|
537
|
+
});
|
|
501
538
|
}
|
|
502
539
|
return {
|
|
503
540
|
id: 'agent',
|
|
@@ -787,6 +824,15 @@ export function AcpSessionMenu(props) {
|
|
|
787
824
|
await acp.probeModels(agentId);
|
|
788
825
|
return;
|
|
789
826
|
}
|
|
827
|
+
if (sectionId === 'agent' && itemId === OPEN_SETTINGS_ITEM) {
|
|
828
|
+
// The empty roster's way out (see `agentSection`): there is no agent to
|
|
829
|
+
// bind, so this row opens the NTR settings surface instead. Close the
|
|
830
|
+
// picker first — the dialog is a frame overlay and the menu must not
|
|
831
|
+
// linger behind it.
|
|
832
|
+
closeAcpMenu();
|
|
833
|
+
openAcpSettingsDialog();
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
790
836
|
if (sectionId === 'agent') {
|
|
791
837
|
await acp.bindChatAgent(sessionId, itemId);
|
|
792
838
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The NTR settings dialog, opened from the agent base's empty-roster menu.
|
|
4
|
+
*
|
|
5
|
+
* WHY A DIALOG OF OUR OWN: the host settings shell owns its open state and
|
|
6
|
+
* exposes no programmatic way in. The sidebar seat that receives `openSettings`
|
|
7
|
+
* (`settings.launcher`) is a single-occupant slot already held by the account
|
|
8
|
+
* menu, and `openSection` is only handed to `settings.onboarding` registrants —
|
|
9
|
+
* so a conversation surface cannot drive the shell's modal. This dialog mounts
|
|
10
|
+
* the SAME `AcpSettingsSection` the shell mounts, on the frame's overlay layer
|
|
11
|
+
* (`shell.overlay`): one settings surface, two entrances, no duplicated
|
|
12
|
+
* configuration logic.
|
|
13
|
+
*
|
|
14
|
+
* The trigger (composer model seat) and this dialog (root overlay) live in
|
|
15
|
+
* unrelated slot trees, so the open state is the module-level store in
|
|
16
|
+
* `acp-ui.tsx` (`openAcpSettingsDialog`).
|
|
17
|
+
*/
|
|
18
|
+
import { useEffect } from 'react';
|
|
19
|
+
import { AcpSettingsSection } from "./panel.js";
|
|
20
|
+
import { closeAcpSettingsDialog, ensureAcpStyle, useAcpSettingsDialogOpen, } from "./acp-ui.js";
|
|
21
|
+
/** Frame-wide NTR settings surface; renders nothing while closed. */
|
|
22
|
+
export function AcpSettingsDialog(props) {
|
|
23
|
+
ensureAcpStyle();
|
|
24
|
+
const { acp } = props;
|
|
25
|
+
const open = useAcpSettingsDialogOpen();
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!open)
|
|
28
|
+
return;
|
|
29
|
+
const onKey = (event) => {
|
|
30
|
+
if (event.key !== 'Escape')
|
|
31
|
+
return;
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
event.stopPropagation();
|
|
34
|
+
closeAcpSettingsDialog();
|
|
35
|
+
};
|
|
36
|
+
document.addEventListener('keydown', onKey, true);
|
|
37
|
+
return () => { document.removeEventListener('keydown', onKey, true); };
|
|
38
|
+
}, [open]);
|
|
39
|
+
if (!open)
|
|
40
|
+
return null;
|
|
41
|
+
const title = acp.label('NTR 设置', 'NTR settings');
|
|
42
|
+
return (_jsxs("div", { className: "ntr-acp-dialog-layer", children: [_jsx("div", { className: "ntr-acp-dialog-mask", "aria-hidden": true, onClick: closeAcpSettingsDialog }), _jsxs("div", { className: "ntr-acp-dialog", role: "dialog", "aria-modal": "true", "aria-label": title, children: [_jsxs("div", { className: "ntr-acp-dialog-head", children: [_jsx("span", { className: "ntr-acp-dialog-title", children: title }), _jsx("button", { type: "button", className: "ntr-acp-dialog-close", title: acp.label('关闭', 'Close'), "aria-label": acp.label('关闭 NTR 设置', 'Close NTR settings'), autoFocus: true, onClick: closeAcpSettingsDialog, children: "\u2715" })] }), _jsx("div", { className: "ntr-acp-dialog-body", children: _jsx(AcpSettingsSection, { acp: acp }) })] })] }));
|
|
43
|
+
}
|
package/lib/client/store.js
CHANGED
|
@@ -287,6 +287,7 @@ export class AcpStore {
|
|
|
287
287
|
localeRev: 0,
|
|
288
288
|
stream: 'init',
|
|
289
289
|
agents: [],
|
|
290
|
+
agentsLoaded: false,
|
|
290
291
|
sessions: [],
|
|
291
292
|
blocks: [],
|
|
292
293
|
permissions: [],
|
|
@@ -517,6 +518,7 @@ export class AcpStore {
|
|
|
517
518
|
// Absent on an older host: keep the settings-panel card (degraded path).
|
|
518
519
|
this.mutate({
|
|
519
520
|
agents,
|
|
521
|
+
agentsLoaded: true,
|
|
520
522
|
sessions,
|
|
521
523
|
defaultAgent: defaultAgentInfo?.agentId,
|
|
522
524
|
detectedAgents,
|