dsh-ntr 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +8 -4
- package/README.md +8 -4
- package/lib/bridge.js +116 -28
- package/lib/client/acp-ui.js +3 -1
- package/lib/client/panel.js +6 -4
- package/lib/client/session-controls.js +27 -11
- package/lib/client.js +40 -17
- package/lib/client.js.map +1 -1
- package/lib/impl.js +102 -9
- package/lib/types/bridge.d.ts +32 -6
- package/lib/types/client/acp-ui.d.ts +6 -0
- package/lib/types/client/session-controls.d.ts +2 -0
- package/lib/types/client/store.d.ts +1 -1
- package/lib/types/impl.d.ts +36 -1
- package/lib/types/protocol.d.ts +22 -8
- package/package.json +1 -1
- package/src/bridge.ts +116 -32
- package/src/client/acp-ui.tsx +13 -2
- package/src/client/panel.tsx +6 -4
- package/src/client/session-controls.tsx +29 -11
- package/src/client/store.ts +1 -1
- package/src/impl.ts +101 -10
- package/src/protocol.ts +22 -8
package/README.en.md
CHANGED
|
@@ -22,7 +22,11 @@ In one sentence: your host agent acts as an ACP client, negotiates capabilities
|
|
|
22
22
|
|
|
23
23
|
This plugin is a DSH **bundle**: `package.json`'s `dsh.bundle.patch` declares the two patches shipped with the package — the plugin's own `cordis.patch.yml`, and the NTR-mode preset at `presets/acp/acp.patch.yml`. After `dsh plugin` installs it, the package joins the profile's `dsh.profile.bundles` and the patch layers take effect **automatically** — no config file needs editing by hand.
|
|
24
24
|
|
|
25
|
-
### Option 1: from the
|
|
25
|
+
### Option 1: from the official UI
|
|
26
|
+
|
|
27
|
+
Open the **Plugins** page in DSH's sidebar, click **Add plugin**, enter `dsh-ntr`, and click install. Refresh the page and it's ready to use.
|
|
28
|
+
|
|
29
|
+
### Option 2: from the npm registry
|
|
26
30
|
|
|
27
31
|
```bash
|
|
28
32
|
dsh plugin --profile web add dsh-ntr@latest
|
|
@@ -32,7 +36,7 @@ Run the same command again to upgrade.
|
|
|
32
36
|
|
|
33
37
|
> **Always pass `@latest`**: without it the package manager keeps reusing the version it wrote the first time, and `update` won't get you off it afterwards.
|
|
34
38
|
|
|
35
|
-
### Option
|
|
39
|
+
### Option 3: via the [dsh-market](https://github.com/dsh-market/dsh-market) plugin market
|
|
36
40
|
|
|
37
41
|
First install the market itself:
|
|
38
42
|
|
|
@@ -42,9 +46,9 @@ dsh plugin --profile web add dshmarket
|
|
|
42
46
|
|
|
43
47
|
Restart `dsh web` and open the **Plugins** page: on dsh 0.2.1+ the market is the **Plugin Market** tab next to **Installed**; on older hosts it lives under **Settings → Plugin Market**. Search for `dsh-ntr`, confirm the source, and install in one click — refresh the page and it's live; update checks and upgrades afterwards also happen in the market. The official DSH Desktop app ships with this market preset, so just search and install from the app's **Plugins** page.
|
|
44
48
|
|
|
45
|
-
> The market's catalog comes from the curated [awesome-dsh-plugin](https://awesome-dsh-plugin.com) registry and refreshes daily. If `dsh-ntr` doesn't show up in search yet, install it from npm with Option
|
|
49
|
+
> The market's catalog comes from the curated [awesome-dsh-plugin](https://awesome-dsh-plugin.com) registry and refreshes daily. If `dsh-ntr` doesn't show up in search yet, install it from npm with Option 2 — nothing is lost.
|
|
46
50
|
|
|
47
|
-
### Option
|
|
51
|
+
### Option 4: from source
|
|
48
52
|
|
|
49
53
|
Requires Node.js ≥ 22.12.0.
|
|
50
54
|
|
package/README.md
CHANGED
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
|
|
23
23
|
本插件是一个 DSH **bundle**:`package.json` 的 `dsh.bundle.patch` 声明了随包分发的两层补丁——插件本体的 `cordis.patch.yml`,与 NTR 模式 preset 的 `presets/acp/acp.patch.yml`。用 `dsh plugin` 安装后,包会加入 profile 的 `dsh.profile.bundles`,补丁层**自动生效**,无需手动编辑任何配置文件。
|
|
24
24
|
|
|
25
|
-
###
|
|
25
|
+
### 方式一:从官方界面安装
|
|
26
|
+
|
|
27
|
+
打开 DSH 侧边栏的**插件**页,点**添加插件**,输入 `dsh-ntr`,点安装。装完刷新页面即可使用。
|
|
28
|
+
|
|
29
|
+
### 方式二:从 npm registry 安装
|
|
26
30
|
|
|
27
31
|
```bash
|
|
28
32
|
dsh plugin --profile web add dsh-ntr@latest
|
|
@@ -32,7 +36,7 @@ dsh plugin --profile web add dsh-ntr@latest
|
|
|
32
36
|
|
|
33
37
|
> `add` 时**务必带 `@latest`**:不带的话包管理器会一直复用首次写入的版本号,之后连 `update` 也升不上去。
|
|
34
38
|
|
|
35
|
-
###
|
|
39
|
+
### 方式三:经 [dsh-market](https://github.com/dsh-market/dsh-market) 插件市场安装
|
|
36
40
|
|
|
37
41
|
先装市场本体:
|
|
38
42
|
|
|
@@ -42,9 +46,9 @@ dsh plugin --profile web add dshmarket
|
|
|
42
46
|
|
|
43
47
|
重启 `dsh web`,打开**插件**页:dsh 0.2.1 起市场是「已安装」旁边的**插件市场**标签页;更早的版本在**设置 → 插件市场**。搜索 `dsh-ntr`,确认来源后一键安装,装完刷新页面即可使用;之后的更新检查与升级也在市场里完成。官方 DSH 桌面 App 自带这个市场,直接在 App 的**插件**页里搜索安装即可。
|
|
44
48
|
|
|
45
|
-
> 市场的插件清单来自 [awesome-dsh-plugin](https://awesome-dsh-plugin.com) 精选目录,每天自动刷新。若搜索结果里暂时没有 `dsh-ntr
|
|
49
|
+
> 市场的插件清单来自 [awesome-dsh-plugin](https://awesome-dsh-plugin.com) 精选目录,每天自动刷新。若搜索结果里暂时没有 `dsh-ntr`,用方式二从 npm 安装,不影响使用。
|
|
46
50
|
|
|
47
|
-
###
|
|
51
|
+
### 方式四:从源码安装
|
|
48
52
|
|
|
49
53
|
需要 Node.js ≥ 22.12.0。
|
|
50
54
|
|
package/lib/bridge.js
CHANGED
|
@@ -12,7 +12,7 @@ import { delimiter, dirname, join } from 'node:path';
|
|
|
12
12
|
import { Writable, Readable } from 'node:stream';
|
|
13
13
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { client, methods, ndJsonStream, PROTOCOL_VERSION, RequestError, } from '@agentclientprotocol/sdk';
|
|
15
|
-
import { AcpError, EventHub, PermissionLedger, Transcript, acpToolCallDelta, acpToolCallFromUpdate, buildAdapterModelId, childEnv, isAcpToolContinuation, isUnderPath, parseCatalogOptions, quoteCommandToken, randomUUID, resolveFsPath, resolveSessionCwd, selectForwardPrompt, acpPresetStatusFromRoster, splitAdapterModelId, updateContentToText, describeUnhandledUpdate, BOOKKEEPING_UPDATES, validateAgentDef, validateTagHalo, } from "./impl.js";
|
|
15
|
+
import { AcpError, EventHub, PermissionLedger, Transcript, acpToolCallDelta, acpToolCallFromUpdate, buildAdapterModelId, childEnv, isAcpToolContinuation, isUnderPath, mergeCatalogShape, parseCatalogOptions, usableConfigOptions, quoteCommandToken, randomUUID, resolveFsPath, resolveSessionCwd, selectForwardPrompt, acpPresetStatusFromRoster, splitAdapterModelId, updateContentToText, describeUnhandledUpdate, BOOKKEEPING_UPDATES, validateAgentDef, validateTagHalo, } from "./impl.js";
|
|
16
16
|
import { DEFAULT_TAG_HALO } from "./protocol.js";
|
|
17
17
|
const STDERR_TAIL_LINES = 40;
|
|
18
18
|
/** Single fs/read_text_file answer cap — agents reading huge files must page via `line`/`limit`. */
|
|
@@ -101,7 +101,13 @@ export class AgentBridge {
|
|
|
101
101
|
constructor(def, hub, transcriptMax,
|
|
102
102
|
/** Called when the connection dies so the manager can settle outstanding prompts. */
|
|
103
103
|
onConnectionLost,
|
|
104
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Observes `configOptions` from any real session so the manager's model
|
|
106
|
+
* catalog stays warm. `shapeOnly` marks a per-session answer (a
|
|
107
|
+
* `session/set_config_option` response): only the option SHAPE belongs in
|
|
108
|
+
* the agent-level catalog, never its `currentValue` — see
|
|
109
|
+
* {@link BridgeManager.absorbCatalogShape}.
|
|
110
|
+
*/
|
|
105
111
|
onCatalog,
|
|
106
112
|
/** Observes `availableCommands` from any real session so the agent's slash menu survives its sessions. */
|
|
107
113
|
onCommands, tuning,
|
|
@@ -464,7 +470,14 @@ export class AgentBridge {
|
|
|
464
470
|
protocolVersion: PROTOCOL_VERSION,
|
|
465
471
|
// The fs handlers are implemented below behind the same workspace
|
|
466
472
|
// sandbox that gates session cwds; terminal is not implemented.
|
|
467
|
-
|
|
473
|
+
// `session.configOptions.boolean` advertises the toggle form of
|
|
474
|
+
// `configOptions`; agents that gate `type: "boolean"` entries on it
|
|
475
|
+
// would otherwise never publish them.
|
|
476
|
+
clientCapabilities: {
|
|
477
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
478
|
+
terminal: false,
|
|
479
|
+
session: { configOptions: { boolean: {} } },
|
|
480
|
+
},
|
|
468
481
|
clientInfo: { name: 'dsh-ntr', title: 'DSH NTR', version: '0.1.0' },
|
|
469
482
|
}), 'initialize');
|
|
470
483
|
this.initResult = init;
|
|
@@ -516,7 +529,8 @@ export class AgentBridge {
|
|
|
516
529
|
throw new AcpError('agent_unreachable', 'acp: connection is not open');
|
|
517
530
|
const resolved = resolveSessionCwd(this.def, cwd, p => realpathSync(p), extraRoots);
|
|
518
531
|
const result = await this.withStartupTimeout(conn.agent.request(methods.agent.session.new, { cwd: resolved, mcpServers: [] }), 'session/new');
|
|
519
|
-
|
|
532
|
+
// An empty `configOptions` answer is a non-answer — see `usableConfigOptions`.
|
|
533
|
+
if (this.onCatalog !== undefined && usableConfigOptions(result.configOptions)) {
|
|
520
534
|
this.onCatalog(this.def.id, result.configOptions);
|
|
521
535
|
}
|
|
522
536
|
const session = {
|
|
@@ -541,7 +555,7 @@ export class AgentBridge {
|
|
|
541
555
|
},
|
|
542
556
|
}
|
|
543
557
|
: {}),
|
|
544
|
-
...(result.configOptions
|
|
558
|
+
...(usableConfigOptions(result.configOptions) ? { configOptions: result.configOptions } : {}),
|
|
545
559
|
};
|
|
546
560
|
this.sessions.set(session.sessionId, session);
|
|
547
561
|
this.acpIndex.set(session.acpSessionId, session);
|
|
@@ -720,7 +734,7 @@ export class AgentBridge {
|
|
|
720
734
|
this.acpIndex.set(session.acpSessionId, session);
|
|
721
735
|
this.hub.broadcast({ type: 'session/reset', sessionId: session.sessionId, session: snapshot(session) });
|
|
722
736
|
const result = await this.withStartupTimeout(conn.agent.request(methods.agent.session.load, { sessionId: session.acpSessionId, cwd: resolved, mcpServers: [] }), 'session/load');
|
|
723
|
-
if (this.onCatalog !== undefined && result.configOptions
|
|
737
|
+
if (this.onCatalog !== undefined && usableConfigOptions(result.configOptions)) {
|
|
724
738
|
this.onCatalog(this.def.id, result.configOptions);
|
|
725
739
|
}
|
|
726
740
|
session.modes = result.modes !== undefined && result.modes !== null
|
|
@@ -733,7 +747,7 @@ export class AgentBridge {
|
|
|
733
747
|
})),
|
|
734
748
|
}
|
|
735
749
|
: session.modes;
|
|
736
|
-
if (
|
|
750
|
+
if (usableConfigOptions(result.configOptions))
|
|
737
751
|
session.configOptions = result.configOptions;
|
|
738
752
|
this.sessions.set(session.sessionId, session);
|
|
739
753
|
this.released.delete(session.sessionId);
|
|
@@ -825,15 +839,26 @@ export class AgentBridge {
|
|
|
825
839
|
const conn = this.conn;
|
|
826
840
|
if (conn === undefined)
|
|
827
841
|
throw new AcpError('agent_unreachable', 'acp: connection is not open');
|
|
828
|
-
|
|
842
|
+
// A toggle must say so on the wire: the request union discriminates on
|
|
843
|
+
// `type`, and a bare boolean `value` is not a select value id.
|
|
844
|
+
const params = typeof value === 'boolean'
|
|
845
|
+
? { sessionId: session.acpSessionId, configId, value, type: 'boolean' }
|
|
846
|
+
: { sessionId: session.acpSessionId, configId, value };
|
|
847
|
+
const result = await this.runCommand(() => conn.agent.request(methods.agent.session.setConfigOption, params));
|
|
829
848
|
// The response carries the full authoritative option set; some agents
|
|
830
849
|
// (pi-acp observed) never re-notify after a client-driven set. It feeds the
|
|
831
|
-
// SESSION snapshot
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
|
|
850
|
+
// SESSION snapshot first — `currentValue` in a per-session answer describes
|
|
851
|
+
// this session's pick, and promoting it to the catalog would turn the
|
|
852
|
+
// seat's own choice into `reasoning.defaultEffort` for every other
|
|
853
|
+
// conversation (and back onto itself next turn). The option SHAPE still
|
|
854
|
+
// reaches the agent-level catalog (`shapeOnly`): this response is often the
|
|
855
|
+
// only place an agent publishes how its option set changes with the model
|
|
856
|
+
// (opencode offers `thought_level` just for models with variants), and the
|
|
857
|
+
// composer's thought chip is driven by the catalog.
|
|
858
|
+
if (usableConfigOptions(result.configOptions)) {
|
|
836
859
|
session.configOptions = result.configOptions;
|
|
860
|
+
this.onCatalog?.(this.def.id, result.configOptions, true);
|
|
861
|
+
}
|
|
837
862
|
this.publishMeta(session);
|
|
838
863
|
}
|
|
839
864
|
publishMeta(session) {
|
|
@@ -1115,7 +1140,10 @@ export class AgentBridge {
|
|
|
1115
1140
|
session.modes = { ...session.modes, currentModeId: update.currentModeId };
|
|
1116
1141
|
break;
|
|
1117
1142
|
case 'config_option_update':
|
|
1118
|
-
|
|
1143
|
+
// Empty answers never overwrite the last known set — see `usableConfigOptions`.
|
|
1144
|
+
if (!usableConfigOptions(update.configOptions))
|
|
1145
|
+
break;
|
|
1146
|
+
session.configOptions = update.configOptions;
|
|
1119
1147
|
this.onCatalog?.(this.def.id, session.configOptions);
|
|
1120
1148
|
break;
|
|
1121
1149
|
case 'available_commands_update':
|
|
@@ -2864,8 +2892,19 @@ export class BridgeManager {
|
|
|
2864
2892
|
if (option === undefined) {
|
|
2865
2893
|
throw new AcpError('unknown_config', `acp: agent "${bound}" publishes no extra config option "${configId}"`);
|
|
2866
2894
|
}
|
|
2867
|
-
if (
|
|
2868
|
-
|
|
2895
|
+
if (option.kind === 'boolean') {
|
|
2896
|
+
// The browser sends 'true' / 'false' from a checkbox row; the wire and
|
|
2897
|
+
// the persisted pick both use the real boolean.
|
|
2898
|
+
const toggle = typeof value === 'boolean'
|
|
2899
|
+
? value
|
|
2900
|
+
: value === 'true' ? true : value === 'false' ? false : undefined;
|
|
2901
|
+
if (toggle === undefined) {
|
|
2902
|
+
throw new AcpError('bad_config', `acp: "${String(value)}" is not a boolean value of config option "${configId}"`);
|
|
2903
|
+
}
|
|
2904
|
+
value = toggle;
|
|
2905
|
+
}
|
|
2906
|
+
else if (typeof value !== 'string' || !option.choices.some(choice => choice.value === value)) {
|
|
2907
|
+
throw new AcpError('bad_config', `acp: "${String(value)}" is not a value of config option "${configId}"`);
|
|
2869
2908
|
}
|
|
2870
2909
|
const current = this.chatAgentIntent.get(dshSessionId);
|
|
2871
2910
|
const extraConfigs = current?.agentId === bound ? { ...current.extraConfigs } : {};
|
|
@@ -2962,12 +3001,17 @@ export class BridgeManager {
|
|
|
2962
3001
|
});
|
|
2963
3002
|
continue;
|
|
2964
3003
|
}
|
|
2965
|
-
|
|
3004
|
+
// A toggle's truth is its boolean itself; a select's is membership in
|
|
3005
|
+
// the choices the agent still offers.
|
|
3006
|
+
const usable = option.kind === 'boolean'
|
|
3007
|
+
? typeof value === 'boolean'
|
|
3008
|
+
: typeof value === 'string' && option.choices.some(entry => entry.value === value);
|
|
3009
|
+
if (!usable) {
|
|
2966
3010
|
this.hub.broadcast({
|
|
2967
3011
|
type: 'agent/notice',
|
|
2968
3012
|
agentId: session.agentId,
|
|
2969
3013
|
level: 'error',
|
|
2970
|
-
text: `acp: agent "${session.agentId}" no longer offers "${value}" for config "${configId}"; keeping its own`,
|
|
3014
|
+
text: `acp: agent "${session.agentId}" no longer offers "${String(value)}" for config "${configId}"; keeping its own`,
|
|
2971
3015
|
});
|
|
2972
3016
|
continue;
|
|
2973
3017
|
}
|
|
@@ -3194,7 +3238,7 @@ export class BridgeManager {
|
|
|
3194
3238
|
continue;
|
|
3195
3239
|
const configs = extraConfigs !== null && typeof extraConfigs === 'object' && !Array.isArray(extraConfigs)
|
|
3196
3240
|
? Object.fromEntries(Object.entries(extraConfigs)
|
|
3197
|
-
.filter(([, value]) => typeof value === 'string' && value !== ''))
|
|
3241
|
+
.filter(([, value]) => typeof value === 'boolean' || (typeof value === 'string' && value !== '')))
|
|
3198
3242
|
: {};
|
|
3199
3243
|
this.chatAgentIntent.set(dshSessionId, {
|
|
3200
3244
|
agentId,
|
|
@@ -3359,6 +3403,9 @@ export class BridgeManager {
|
|
|
3359
3403
|
* current `parseCatalogOptions` has now seen this agent's real answer.
|
|
3360
3404
|
*/
|
|
3361
3405
|
absorbCatalog(agentId, configOptions) {
|
|
3406
|
+
// Empty / absurd answers never overwrite known state — see `usableConfigOptions`.
|
|
3407
|
+
if (!usableConfigOptions(configOptions))
|
|
3408
|
+
return this.catalogs.get(agentId) ?? { catalog: {}, updatedAt: 0 };
|
|
3362
3409
|
const catalog = parseCatalogOptions(configOptions);
|
|
3363
3410
|
this.catalogStale.delete(agentId);
|
|
3364
3411
|
const existing = this.catalogs.get(agentId);
|
|
@@ -3386,6 +3433,30 @@ export class BridgeManager {
|
|
|
3386
3433
|
this.broadcastExposure(agentId);
|
|
3387
3434
|
return entry;
|
|
3388
3435
|
}
|
|
3436
|
+
/**
|
|
3437
|
+
* Fold a per-session option answer's SHAPE into the agent's catalog.
|
|
3438
|
+
*
|
|
3439
|
+
* The counterpart of {@link absorbCatalog} for `session/set_config_option`
|
|
3440
|
+
* responses: which options exist and which choices they offer is agent
|
|
3441
|
+
* knowledge the seat must show (the composer's thought chip only renders
|
|
3442
|
+
* when the catalog has `thought_level`, and some agents publish it only in
|
|
3443
|
+
* the answer to a model change), while a per-session `currentValue` stays
|
|
3444
|
+
* out of it. No auto-exposure and no staleness discharge either — this is
|
|
3445
|
+
* one session's answer, not the agent-level observation those marks track.
|
|
3446
|
+
*/
|
|
3447
|
+
absorbCatalogShape(agentId, configOptions) {
|
|
3448
|
+
if (!usableConfigOptions(configOptions))
|
|
3449
|
+
return;
|
|
3450
|
+
const observed = parseCatalogOptions(configOptions);
|
|
3451
|
+
const existing = this.catalogs.get(agentId);
|
|
3452
|
+
const catalog = mergeCatalogShape(existing?.catalog, observed);
|
|
3453
|
+
if (existing !== undefined && JSON.stringify(existing.catalog) === JSON.stringify(catalog))
|
|
3454
|
+
return;
|
|
3455
|
+
const entry = { catalog, updatedAt: Date.now() };
|
|
3456
|
+
this.catalogs.set(agentId, entry);
|
|
3457
|
+
this.persistCatalogs();
|
|
3458
|
+
this.hub.broadcast({ type: 'catalog/updated', agentId, updatedAt: entry.updatedAt });
|
|
3459
|
+
}
|
|
3389
3460
|
/**
|
|
3390
3461
|
* Remember the slash commands an agent declared in a real session.
|
|
3391
3462
|
*
|
|
@@ -3418,11 +3489,12 @@ export class BridgeManager {
|
|
|
3418
3489
|
* selection would resurrect a model the user deliberately unchecked every
|
|
3419
3490
|
* time the agent repeats its catalog. The first observation has no baseline
|
|
3420
3491
|
* (`before === undefined`), so it adds nothing; the user can still add the
|
|
3421
|
-
* whole list with one click in the settings card.
|
|
3492
|
+
* whole list with one click in the settings card. Discovery is ON unless the
|
|
3493
|
+
* agent definition explicitly opts out with `autoDiscoverModels: false`.
|
|
3422
3494
|
*/
|
|
3423
3495
|
autoExposeNewModels(agentId, next, before) {
|
|
3424
3496
|
const def = this.defsMap.get(agentId);
|
|
3425
|
-
if (def
|
|
3497
|
+
if (def === undefined || def.autoDiscoverModels === false || before === undefined)
|
|
3426
3498
|
return false;
|
|
3427
3499
|
const known = new Set((before.model?.choices ?? []).map(choice => choice.value));
|
|
3428
3500
|
const exposed = new Set(def.exposedModels ?? []);
|
|
@@ -3444,7 +3516,7 @@ export class BridgeManager {
|
|
|
3444
3516
|
type: 'agent/exposure',
|
|
3445
3517
|
agentId,
|
|
3446
3518
|
exposedModels: [...(def.exposedModels ?? [])],
|
|
3447
|
-
autoDiscoverModels: def.autoDiscoverModels
|
|
3519
|
+
autoDiscoverModels: def.autoDiscoverModels !== false,
|
|
3448
3520
|
});
|
|
3449
3521
|
}
|
|
3450
3522
|
catalogFor(agentId) {
|
|
@@ -3618,6 +3690,7 @@ export class BridgeManager {
|
|
|
3618
3690
|
extras: extraOptions.map(option => ({
|
|
3619
3691
|
configId: option.configId,
|
|
3620
3692
|
name: option.name,
|
|
3693
|
+
...(option.kind === 'boolean' ? { kind: 'boolean' } : {}),
|
|
3621
3694
|
...(option.currentValue === null ? {} : { currentValue: option.currentValue }),
|
|
3622
3695
|
choices: option.choices.map(choice => ({
|
|
3623
3696
|
id: choice.value,
|
|
@@ -3841,8 +3914,11 @@ export class BridgeManager {
|
|
|
3841
3914
|
if (bridge === undefined) {
|
|
3842
3915
|
bridge = this.makeBridge(def, (b, reason) => {
|
|
3843
3916
|
this.connectionLost(b, reason);
|
|
3844
|
-
}, (observedAgentId, configOptions) => {
|
|
3845
|
-
|
|
3917
|
+
}, (observedAgentId, configOptions, shapeOnly) => {
|
|
3918
|
+
if (shapeOnly)
|
|
3919
|
+
this.absorbCatalogShape(observedAgentId, configOptions);
|
|
3920
|
+
else
|
|
3921
|
+
this.absorbCatalog(observedAgentId, configOptions);
|
|
3846
3922
|
}, (observedAgentId, commands) => {
|
|
3847
3923
|
this.absorbCommands(observedAgentId, commands);
|
|
3848
3924
|
});
|
|
@@ -3970,16 +4046,17 @@ export class BridgeManager {
|
|
|
3970
4046
|
* Toggle auto-discovery of newly published models. Lightweight like
|
|
3971
4047
|
* `setExposedModels`: the def is mutated in place (live bridges share the
|
|
3972
4048
|
* object) and persisted; no process is restarted, and the current selection
|
|
3973
|
-
* is left exactly as it is.
|
|
4049
|
+
* is left exactly as it is. On is the default, so only the opt-out
|
|
4050
|
+
* (`false`) is written to the file.
|
|
3974
4051
|
*/
|
|
3975
4052
|
async setAutoDiscoverModels(agentId, autoDiscoverModels) {
|
|
3976
4053
|
const def = this.defsMap.get(agentId);
|
|
3977
4054
|
if (def === undefined)
|
|
3978
4055
|
throw new AcpError('unknown_agent', `acp: agent "${agentId}" is not configured`);
|
|
3979
4056
|
if (autoDiscoverModels)
|
|
3980
|
-
def.autoDiscoverModels = true;
|
|
3981
|
-
else
|
|
3982
4057
|
delete def.autoDiscoverModels;
|
|
4058
|
+
else
|
|
4059
|
+
def.autoDiscoverModels = false;
|
|
3983
4060
|
this.persistAgents();
|
|
3984
4061
|
this.broadcastExposure(agentId);
|
|
3985
4062
|
}
|
|
@@ -4237,6 +4314,13 @@ export class BridgeManager {
|
|
|
4237
4314
|
const extraConfigs = {};
|
|
4238
4315
|
for (const option of catalog.extras ?? []) {
|
|
4239
4316
|
const value = option.currentValue;
|
|
4317
|
+
if (option.kind === 'boolean') {
|
|
4318
|
+
// The catalog encodes a toggle as 'true' / 'false'; the pick stores the
|
|
4319
|
+
// real boolean, exactly like an explicit checkbox click.
|
|
4320
|
+
if (value === 'true' || value === 'false')
|
|
4321
|
+
extraConfigs[option.configId] = value === 'true';
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4240
4324
|
if (value !== null && value !== undefined && value !== '')
|
|
4241
4325
|
extraConfigs[option.configId] = value;
|
|
4242
4326
|
}
|
|
@@ -4290,7 +4374,11 @@ export class BridgeManager {
|
|
|
4290
4374
|
const option = options.extras?.find(entry => entry.configId === configId);
|
|
4291
4375
|
if (option === undefined)
|
|
4292
4376
|
continue;
|
|
4293
|
-
|
|
4377
|
+
// The fresh session's own option set is the authority for BOTH forms:
|
|
4378
|
+
// a toggle must still be a toggle, a select must still offer the value.
|
|
4379
|
+
if (option.kind === 'boolean'
|
|
4380
|
+
? typeof value !== 'boolean'
|
|
4381
|
+
: typeof value !== 'string' || (option.choices.length > 0 && !option.choices.some(choice => choice.value === value)))
|
|
4294
4382
|
continue;
|
|
4295
4383
|
extraConfigs[configId] = value;
|
|
4296
4384
|
changed = true;
|
package/lib/client/acp-ui.js
CHANGED
|
@@ -350,7 +350,7 @@ export function AcpMenu({ model, anchor, onPick, onClose }) {
|
|
|
350
350
|
}, [model, onClose]);
|
|
351
351
|
if (model === undefined)
|
|
352
352
|
return null;
|
|
353
|
-
return (_jsxs("div", { ref: rootRef, className: "ntr-acp-menu", role: "menu", "aria-label": model.title, children: [_jsxs("div", { className: "ntr-acp-menu-head", children: [_jsx("span", { className: "ntr-acp-menu-title", children: model.title }), model.loading && _jsx("span", { className: "ntr-acp-menu-spin", "aria-hidden": true, children: "\u2026" })] }), model.note !== undefined && _jsx("div", { className: "ntr-acp-menu-note", children: model.note }), model.error !== undefined && _jsx("div", { className: "ntr-acp-menu-error", children: model.error }), _jsxs("div", { className: "ntr-acp-menu-body", children: [model.sections.map(section => (_jsxs("div", { className: "ntr-acp-menu-section", children: [section.title !== undefined && _jsx("div", { className: "ntr-acp-menu-section-title", children: section.title }), section.note !== undefined && _jsx("div", { className: "ntr-acp-menu-note", children: section.note }), section.items.map(item => (_jsxs("button", { type: "button", role:
|
|
353
|
+
return (_jsxs("div", { ref: rootRef, className: "ntr-acp-menu", role: "menu", "aria-label": model.title, children: [_jsxs("div", { className: "ntr-acp-menu-head", children: [_jsx("span", { className: "ntr-acp-menu-title", children: model.title }), model.loading && _jsx("span", { className: "ntr-acp-menu-spin", "aria-hidden": true, children: "\u2026" })] }), model.note !== undefined && _jsx("div", { className: "ntr-acp-menu-note", children: model.note }), model.error !== undefined && _jsx("div", { className: "ntr-acp-menu-error", children: model.error }), _jsxs("div", { className: "ntr-acp-menu-body", children: [model.sections.map(section => (_jsxs("div", { className: "ntr-acp-menu-section", children: [section.title !== undefined && _jsx("div", { className: "ntr-acp-menu-section-title", children: section.title }), section.note !== undefined && _jsx("div", { className: "ntr-acp-menu-note", children: section.note }), section.items.map(item => (_jsxs("button", { type: "button", role: item.checkbox === true ? 'menuitemcheckbox' : 'menuitemradio', "aria-checked": item.active === true, className: `ntr-acp-menu-item${item.active === true ? ' is-active' : ''}`, disabled: item.disabled === true, onClick: () => { onPick(item.id, section.id); }, children: [item.dot !== undefined && _jsx("span", { className: "ntr-acp-dot", style: { background: item.dot }, "aria-hidden": true }), item.checkbox === true && (_jsx("span", { className: `ntr-acp-check${item.active === true ? ' is-on' : ''}`, "aria-hidden": true, children: item.active === true ? '✓' : '' })), _jsx("span", { className: "ntr-acp-menu-item-label", children: item.label }), item.active === true && item.checkbox !== true && _jsx("span", { className: "ntr-acp-tick", "aria-hidden": true, children: "\u2713" }), item.detail !== undefined && _jsx("span", { className: "ntr-acp-menu-item-detail", children: item.detail })] }, `${section.id}/${item.id}`)))] }, section.id))), model.sections.every(section => section.items.length === 0) && (_jsx("div", { className: "ntr-acp-menu-empty", children: "\u2014" }))] })] }));
|
|
354
354
|
}
|
|
355
355
|
// ---- Chips ----
|
|
356
356
|
/**
|
|
@@ -490,6 +490,8 @@ const STYLE = `
|
|
|
490
490
|
.ntr-acp-seat-sub { color: var(--dsw-alias-text-secondary, #6b7280); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
491
491
|
.ntr-acp-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
|
|
492
492
|
.ntr-acp-tick { margin-left: auto; color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
493
|
+
.ntr-acp-check { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 13px; height: 13px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-radius: 3px; font-size: 10px; line-height: 1; color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
494
|
+
.ntr-acp-check.is-on { border-color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
493
495
|
.ntr-acp-menu { position: absolute; bottom: calc(100% + 8px); left: 8px; z-index: 100; width: min(360px, 100%); max-height: min(420px, 60vh); display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-radius: 10px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.16); font-size: 12px; overflow: hidden; }
|
|
494
496
|
.ntr-acp-menu-head { padding: 8px 10px 6px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
|
|
495
497
|
.ntr-acp-menu-title { flex: 1; }
|
package/lib/client/panel.js
CHANGED
|
@@ -349,7 +349,7 @@ function payloadEquals(payload, current) {
|
|
|
349
349
|
return false;
|
|
350
350
|
if (exposureKey(payload.exposedModels) !== exposureKey(current.exposedModels))
|
|
351
351
|
return false;
|
|
352
|
-
if ((payload.autoDiscoverModels
|
|
352
|
+
if ((payload.autoDiscoverModels !== false) !== (current.autoDiscoverModels !== false))
|
|
353
353
|
return false;
|
|
354
354
|
// An absent field means "use the host default" — which is what `current`
|
|
355
355
|
// already resolved to (see `resolvedIdleTimeoutMs`), so it is not a change.
|
|
@@ -428,7 +428,7 @@ function AgentRow({ detected, activeAgent, acp, isDefault, synthesized, newCusto
|
|
|
428
428
|
// Re-sync the editor only when the persisted filter actually changes, so
|
|
429
429
|
// background resyncs never clobber in-progress checkbox edits.
|
|
430
430
|
}, [serverExposureKey]);
|
|
431
|
-
const serverAutoDiscover = detected.currentConfig?.autoDiscoverModels
|
|
431
|
+
const serverAutoDiscover = detected.currentConfig?.autoDiscoverModels !== false;
|
|
432
432
|
useEffect(() => {
|
|
433
433
|
// The switch saves itself through the lightweight RPC, so the host is the
|
|
434
434
|
// truth; this keeps the form (and therefore a later full save) in step.
|
|
@@ -536,7 +536,7 @@ function initialForm(detected, activeAgent) {
|
|
|
536
536
|
roots,
|
|
537
537
|
envRows: Object.entries(config?.env ?? {}).map(([key, value]) => ({ key, value })),
|
|
538
538
|
exposed: config?.exposedModels === undefined ? undefined : [...config.exposedModels],
|
|
539
|
-
autoDiscoverModels: config?.autoDiscoverModels
|
|
539
|
+
autoDiscoverModels: config?.autoDiscoverModels !== false,
|
|
540
540
|
// Prefer the host's resolved value: it is the one actually in effect.
|
|
541
541
|
idleStr: idleText(activeAgent?.idleTimeoutMs ?? config?.idleTimeoutMs),
|
|
542
542
|
};
|
|
@@ -590,7 +590,9 @@ function buildPayload(form, detected, newCustom) {
|
|
|
590
590
|
...(form.npmPackage.trim() === '' ? {} : { npmPackage: form.npmPackage.trim() }),
|
|
591
591
|
env,
|
|
592
592
|
...(form.exposed === undefined ? {} : { exposedModels: form.exposed }),
|
|
593
|
-
|
|
593
|
+
// Auto-discovery is ON by default: only an unchecked switch records the
|
|
594
|
+
// opt-out, otherwise a full save would silently re-enable it.
|
|
595
|
+
...(form.autoDiscoverModels ? {} : { autoDiscoverModels: false }),
|
|
594
596
|
// `''` sends nothing: the host then applies its own default.
|
|
595
597
|
...(idleRaw === '' || !idleValid(idleRaw) ? {} : { idleTimeoutMs: Number(idleRaw) }),
|
|
596
598
|
};
|
|
@@ -286,10 +286,14 @@ export function advancedOptions(catalog, choice) {
|
|
|
286
286
|
return [];
|
|
287
287
|
const picked = choice !== undefined && choice.agentId === catalog.agentId ? choice.extraConfigs : undefined;
|
|
288
288
|
return extras.map(option => {
|
|
289
|
-
const
|
|
289
|
+
const raw = picked?.[option.configId] ?? option.currentValue;
|
|
290
|
+
// A toggle's catalog value is 'true' / 'false'; a conversation pick stores
|
|
291
|
+
// the real boolean. Both collapse to the string form the menu speaks.
|
|
292
|
+
const value = typeof raw === 'boolean' ? (raw ? 'true' : 'false') : raw;
|
|
290
293
|
return {
|
|
291
294
|
configId: option.configId,
|
|
292
295
|
name: option.name,
|
|
296
|
+
...(option.kind === 'boolean' ? { kind: 'boolean' } : {}),
|
|
293
297
|
...(value === undefined ? {} : { value }),
|
|
294
298
|
choices: option.choices,
|
|
295
299
|
};
|
|
@@ -694,16 +698,28 @@ function thoughtSection(context) {
|
|
|
694
698
|
* and the pick would then be read as the primary model axis.
|
|
695
699
|
*/
|
|
696
700
|
function advancedSections(context) {
|
|
697
|
-
return advancedOptions(context.catalog, context.choice).map(option =>
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
701
|
+
return advancedOptions(context.catalog, context.choice).map(option => {
|
|
702
|
+
if (option.kind === 'boolean') {
|
|
703
|
+
// A toggle is ONE checkbox row, not an On/Off pair: the row's check is
|
|
704
|
+
// the current value and the pick sends the OPPOSITE, which `onPick`
|
|
705
|
+
// forwards verbatim (the host normalizes 'true' / 'false' to a boolean).
|
|
706
|
+
const checked = option.value === 'true';
|
|
707
|
+
return {
|
|
708
|
+
id: `config:${option.configId}`,
|
|
709
|
+
items: [{ id: checked ? 'false' : 'true', label: option.name, active: checked, checkbox: true }],
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
id: `config:${option.configId}`,
|
|
714
|
+
title: option.name,
|
|
715
|
+
items: option.choices.map(choice => ({
|
|
716
|
+
id: choice.id,
|
|
717
|
+
label: choice.name,
|
|
718
|
+
...(choice.description === undefined ? {} : { detail: choice.description }),
|
|
719
|
+
active: choice.id === option.value,
|
|
720
|
+
})),
|
|
721
|
+
};
|
|
722
|
+
});
|
|
707
723
|
}
|
|
708
724
|
/**
|
|
709
725
|
* REMOVED on purpose (2026-10-17): the agent's own `session/set_mode` list used
|
package/lib/client.js
CHANGED
|
@@ -1988,7 +1988,7 @@ window.__ModuleLoader__.load({
|
|
|
1988
1988
|
}),
|
|
1989
1989
|
section.items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1990
1990
|
type: "button",
|
|
1991
|
-
role: "menuitemradio",
|
|
1991
|
+
role: item.checkbox === true ? "menuitemcheckbox" : "menuitemradio",
|
|
1992
1992
|
"aria-checked": item.active === true,
|
|
1993
1993
|
className: `ntr-acp-menu-item${item.active === true ? " is-active" : ""}`,
|
|
1994
1994
|
disabled: item.disabled === true,
|
|
@@ -2001,11 +2001,16 @@ window.__ModuleLoader__.load({
|
|
|
2001
2001
|
style: { background: item.dot },
|
|
2002
2002
|
"aria-hidden": true
|
|
2003
2003
|
}),
|
|
2004
|
+
item.checkbox === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2005
|
+
className: `ntr-acp-check${item.active === true ? " is-on" : ""}`,
|
|
2006
|
+
"aria-hidden": true,
|
|
2007
|
+
children: item.active === true ? "✓" : ""
|
|
2008
|
+
}),
|
|
2004
2009
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2005
2010
|
className: "ntr-acp-menu-item-label",
|
|
2006
2011
|
children: item.label
|
|
2007
2012
|
}),
|
|
2008
|
-
item.active === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2013
|
+
item.active === true && item.checkbox !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2009
2014
|
className: "ntr-acp-tick",
|
|
2010
2015
|
"aria-hidden": true,
|
|
2011
2016
|
children: "✓"
|
|
@@ -2331,6 +2336,8 @@ window.__ModuleLoader__.load({
|
|
|
2331
2336
|
.ntr-acp-seat-sub { color: var(--dsw-alias-text-secondary, #6b7280); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2332
2337
|
.ntr-acp-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
|
|
2333
2338
|
.ntr-acp-tick { margin-left: auto; color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
2339
|
+
.ntr-acp-check { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 13px; height: 13px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-radius: 3px; font-size: 10px; line-height: 1; color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
2340
|
+
.ntr-acp-check.is-on { border-color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
2334
2341
|
.ntr-acp-menu { position: absolute; bottom: calc(100% + 8px); left: 8px; z-index: 100; width: min(360px, 100%); max-height: min(420px, 60vh); display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-radius: 10px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.16); font-size: 12px; overflow: hidden; }
|
|
2335
2342
|
.ntr-acp-menu-head { padding: 8px 10px 6px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
|
|
2336
2343
|
.ntr-acp-menu-title { flex: 1; }
|
|
@@ -2777,7 +2784,7 @@ window.__ModuleLoader__.load({
|
|
|
2777
2784
|
if (!sameStringArray(payload.allowedCwdRoots ?? [], current.allowedCwdRoots ?? [])) return false;
|
|
2778
2785
|
if (!sameStringMap(payload.env ?? {}, current.env ?? {})) return false;
|
|
2779
2786
|
if (exposureKey(payload.exposedModels) !== exposureKey(current.exposedModels)) return false;
|
|
2780
|
-
if (payload.autoDiscoverModels
|
|
2787
|
+
if (payload.autoDiscoverModels !== false !== (current.autoDiscoverModels !== false)) return false;
|
|
2781
2788
|
if (payload.idleTimeoutMs !== void 0 && payload.idleTimeoutMs !== current.idleTimeoutMs) return false;
|
|
2782
2789
|
return true;
|
|
2783
2790
|
}
|
|
@@ -3142,7 +3149,7 @@ window.__ModuleLoader__.load({
|
|
|
3142
3149
|
exposed: server === void 0 ? void 0 : [...server]
|
|
3143
3150
|
}));
|
|
3144
3151
|
}, [serverExposureKey]);
|
|
3145
|
-
const serverAutoDiscover = detected.currentConfig?.autoDiscoverModels
|
|
3152
|
+
const serverAutoDiscover = detected.currentConfig?.autoDiscoverModels !== false;
|
|
3146
3153
|
(0, react.useEffect)(() => {
|
|
3147
3154
|
setForm((prev) => ({
|
|
3148
3155
|
...prev,
|
|
@@ -3396,7 +3403,7 @@ window.__ModuleLoader__.load({
|
|
|
3396
3403
|
value
|
|
3397
3404
|
})),
|
|
3398
3405
|
exposed: config?.exposedModels === void 0 ? void 0 : [...config.exposedModels],
|
|
3399
|
-
autoDiscoverModels: config?.autoDiscoverModels
|
|
3406
|
+
autoDiscoverModels: config?.autoDiscoverModels !== false,
|
|
3400
3407
|
idleStr: idleText(activeAgent?.idleTimeoutMs ?? config?.idleTimeoutMs)
|
|
3401
3408
|
};
|
|
3402
3409
|
}
|
|
@@ -3444,7 +3451,7 @@ window.__ModuleLoader__.load({
|
|
|
3444
3451
|
...form.npmPackage.trim() === "" ? {} : { npmPackage: form.npmPackage.trim() },
|
|
3445
3452
|
env,
|
|
3446
3453
|
...form.exposed === void 0 ? {} : { exposedModels: form.exposed },
|
|
3447
|
-
...form.autoDiscoverModels ? {
|
|
3454
|
+
...form.autoDiscoverModels ? {} : { autoDiscoverModels: false },
|
|
3448
3455
|
...idleRaw === "" || !idleValid(idleRaw) ? {} : { idleTimeoutMs: Number(idleRaw) }
|
|
3449
3456
|
};
|
|
3450
3457
|
if (form.rootsOpen) {
|
|
@@ -4794,10 +4801,12 @@ window.__ModuleLoader__.load({
|
|
|
4794
4801
|
if (catalog === void 0 || extras.length === 0) return [];
|
|
4795
4802
|
const picked = choice !== void 0 && choice.agentId === catalog.agentId ? choice.extraConfigs : void 0;
|
|
4796
4803
|
return extras.map((option) => {
|
|
4797
|
-
const
|
|
4804
|
+
const raw = picked?.[option.configId] ?? option.currentValue;
|
|
4805
|
+
const value = typeof raw === "boolean" ? raw ? "true" : "false" : raw;
|
|
4798
4806
|
return {
|
|
4799
4807
|
configId: option.configId,
|
|
4800
4808
|
name: option.name,
|
|
4809
|
+
...option.kind === "boolean" ? { kind: "boolean" } : {},
|
|
4801
4810
|
...value === void 0 ? {} : { value },
|
|
4802
4811
|
choices: option.choices
|
|
4803
4812
|
};
|
|
@@ -5140,16 +5149,30 @@ window.__ModuleLoader__.load({
|
|
|
5140
5149
|
* and the pick would then be read as the primary model axis.
|
|
5141
5150
|
*/
|
|
5142
5151
|
function advancedSections(context) {
|
|
5143
|
-
return advancedOptions(context.catalog, context.choice).map((option) =>
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5152
|
+
return advancedOptions(context.catalog, context.choice).map((option) => {
|
|
5153
|
+
if (option.kind === "boolean") {
|
|
5154
|
+
const checked = option.value === "true";
|
|
5155
|
+
return {
|
|
5156
|
+
id: `config:${option.configId}`,
|
|
5157
|
+
items: [{
|
|
5158
|
+
id: checked ? "false" : "true",
|
|
5159
|
+
label: option.name,
|
|
5160
|
+
active: checked,
|
|
5161
|
+
checkbox: true
|
|
5162
|
+
}]
|
|
5163
|
+
};
|
|
5164
|
+
}
|
|
5165
|
+
return {
|
|
5166
|
+
id: `config:${option.configId}`,
|
|
5167
|
+
title: option.name,
|
|
5168
|
+
items: option.choices.map((choice) => ({
|
|
5169
|
+
id: choice.id,
|
|
5170
|
+
label: choice.name,
|
|
5171
|
+
...choice.description === void 0 ? {} : { detail: choice.description },
|
|
5172
|
+
active: choice.id === option.value
|
|
5173
|
+
}))
|
|
5174
|
+
};
|
|
5175
|
+
});
|
|
5153
5176
|
}
|
|
5154
5177
|
/**
|
|
5155
5178
|
* REMOVED on purpose (2026-10-17): the agent's own `session/set_mode` list used
|