dsh-ntr 0.0.0-stage → 0.1.0

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +265 -0
  3. package/README.md +264 -2
  4. package/cordis.patch.yml +72 -0
  5. package/lib/bridge.js +4795 -0
  6. package/lib/client/acp-ui.js +540 -0
  7. package/lib/client/acp-view.js +331 -0
  8. package/lib/client/agent-lock-anim.js +113 -0
  9. package/lib/client/composer-mode.js +128 -0
  10. package/lib/client/index.js +627 -0
  11. package/lib/client/model-seat.js +292 -0
  12. package/lib/client/panel.js +873 -0
  13. package/lib/client/session-controls.js +823 -0
  14. package/lib/client/slash-isolation.js +441 -0
  15. package/lib/client/store.js +1749 -0
  16. package/lib/client.js +6690 -0
  17. package/lib/client.js.map +1 -0
  18. package/lib/ids.js +5 -0
  19. package/lib/impl.js +1441 -0
  20. package/lib/index.js +334 -0
  21. package/lib/protocol.js +19 -0
  22. package/lib/types/bridge.d.ts +1517 -0
  23. package/lib/types/client/acp-ui.d.ts +371 -0
  24. package/lib/types/client/acp-view.d.ts +105 -0
  25. package/lib/types/client/agent-lock-anim.d.ts +56 -0
  26. package/lib/types/client/composer-mode.d.ts +53 -0
  27. package/lib/types/client/index.d.ts +11 -0
  28. package/lib/types/client/model-seat.d.ts +265 -0
  29. package/lib/types/client/panel.d.ts +43 -0
  30. package/lib/types/client/session-controls.d.ts +295 -0
  31. package/lib/types/client/slash-isolation.d.ts +122 -0
  32. package/lib/types/client/store.d.ts +603 -0
  33. package/lib/types/ids.d.ts +1 -0
  34. package/lib/types/impl.d.ts +615 -0
  35. package/lib/types/index.d.ts +10 -0
  36. package/lib/types/protocol.d.ts +976 -0
  37. package/package.json +79 -4
  38. package/presets/acp/acp-guard.mjs +307 -0
  39. package/presets/acp/acp.patch.yml +79 -0
  40. package/src/bridge.ts +5118 -0
  41. package/src/client/acp-ui.tsx +897 -0
  42. package/src/client/acp-view.tsx +554 -0
  43. package/src/client/agent-lock-anim.ts +127 -0
  44. package/src/client/composer-mode.tsx +177 -0
  45. package/src/client/index.tsx +661 -0
  46. package/src/client/model-seat.tsx +427 -0
  47. package/src/client/panel.tsx +1949 -0
  48. package/src/client/session-controls.tsx +1025 -0
  49. package/src/client/slash-isolation.ts +511 -0
  50. package/src/client/store.ts +1968 -0
  51. package/src/ids.ts +6 -0
  52. package/src/impl.ts +1689 -0
  53. package/src/index.ts +422 -0
  54. package/src/protocol.ts +812 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GDWhisper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,265 @@
1
+ # dsh-ntr
2
+
3
+ English | [简体中文](./README.md)
4
+
5
+ Chat with any **ACP agent** (CodeBuddy, Pi, Oh My Pi, Claude Code, OpenCode, Qoder, …) right inside the web/desktop app of [DSH (DeepSeek Harness)](https://github.com/deepseek-ai/deepseek-harness), using the interface you already know.
6
+
7
+ **DSH provides the interface; the agent provides the capability.** The conversation list, composer, side panel, and message rendering all come from DSH; reasoning, tool execution, file access, and permission decisions all belong to the agent you configured — it runs in its own process, fully autonomous. This plugin does exactly one thing: wire the two sides together, and render the agent's output faithfully in DSH's UI.
8
+
9
+ > Model selection, thinking level, and work modes are **the agent's own concepts**, not DSH's model catalog. The plugin never swaps models for you, and never writes the agent's choices into DSH's global settings.
10
+
11
+ ---
12
+
13
+ ## What is NTR
14
+
15
+ **NTR = Negotiated Target Routing.**
16
+
17
+ In one sentence: your host agent acts as an ACP client, negotiates capabilities and permissions with a target agent, and hands the turn over to it.
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
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
+
25
+ ### Option 1: from the npm registry
26
+
27
+ ```bash
28
+ dsh plugin --profile web add dsh-ntr@latest
29
+ ```
30
+
31
+ Run the same command again to upgrade.
32
+
33
+ > **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
+
35
+ ### Option 2: via the [dsh-market](https://github.com/dsh-market/dsh-market) plugin market
36
+
37
+ First install the market itself:
38
+
39
+ ```bash
40
+ dsh plugin --profile web add dshmarket
41
+ ```
42
+
43
+ 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
+
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 1 — nothing is lost.
46
+
47
+ ### Option 3: from source
48
+
49
+ Requires Node.js ≥ 22.12.0.
50
+
51
+ ```bash
52
+ git clone https://github.com/GDWhisper/dsh-ntr.git
53
+ cd dsh-ntr
54
+ npm install # build dependencies (typescript, …)
55
+ npm run build # compile host TS + browser bundle into lib/ (build output is not committed — this step is required)
56
+ dsh plugin --profile web add ./
57
+ ```
58
+
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
+
61
+ > `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
+
63
+ ---
64
+
65
+ ## Getting started
66
+
67
+ ### What you need
68
+
69
+ 1. A working DSH install (web or desktop).
70
+ 2. At least one ACP agent CLI. The plugin ships presets for common ones, and accepts any stdio-capable ACP agent.
71
+
72
+ ### Three steps
73
+
74
+ 1. Open **Settings → NTR**.
75
+ 2. Pick a preset from the agent list to fill it in, or click "Create a custom agent from scratch" and type the launch command yourself.
76
+ 3. Click **Test**. Success shows the agent's name and the handshake latency; failure shows an actionable error plus the tail of stderr.
77
+
78
+ After saving, the agent shows up in the NTR base at the end of the composer.
79
+
80
+ ### Start talking
81
+
82
+ 1. Create a conversation and pick **NTR mode** in the mode selector.
83
+ 2. Pick an agent in the NTR base at the end of the composer.
84
+ 3. Just talk.
85
+
86
+ The agent's replies and thinking appear in the main chat. Tool activity is interleaved in order: a line of `🛠️ **tool** · args`, with the output in a code block below. For full tool outputs, plans, and diffs, switch to the **NTR** tab in the conversation header.
87
+
88
+ ---
89
+
90
+ ## The UI you'll use
91
+
92
+ ### NTR base (end of the composer)
93
+
94
+ Click it to choose which agent serves this conversation. The menu lists only agents — one control, one job. The agent's own model and thinking level are separate chips on the same row, each with its own menu; if the agent also publishes extra config options (temperature, provider toggles, …), an **Advanced** base appears to the left of the model chip, where you set them one by one. Before the first turn, the agent base sits pinned at the far right; once locked, it retires and model/thinking take over the right end. Those lists are reported by the agent itself — they are not your familiar DSH model list.
95
+
96
+ The base appears only in **NTR-mode** conversations. Normal conversations show DSH's native model selector.
97
+
98
+ **One conversation, one agent.** Until the first message is sent, the base stays put: pick, or change your mind freely. The moment the first turn **starts**, the conversation **locks** to that agent: the base collapses, and the name becomes a **small tag on the top edge of the composer card** (`NTR · <agent>`, straddling the edge) — display-only, not clickable. To switch agents, start a new conversation. The model, thinking-level, and Advanced chips stay in place; they belong to the same agent, so no context moves. If the first turn fails or is cancelled before producing any visible output, the lock rolls back automatically and the base returns for another pick.
99
+
100
+ ### `/` commands belong to the agent
101
+
102
+ Type `/` in an NTR conversation and what pops up is **the agent's own command list** — the ones it declares over ACP (e.g. `compact`, `session`, `model`), not DSH's. Selecting one just inserts the command name into the composer; pressing Enter sends the whole line to the agent as-is, and it executes it. The list works **before the first message**: the host remembers the commands this agent declared in its last session (an agent that has never run has to wait until after the first turn).
103
+
104
+ DSH's own `/model`, `/permission`, `/export`, etc. are **not** intercepted by the host in this mode: same-named commands go to the agent too, and the host's command menu and skill completions never appear. Slash behavior in normal conversations is unchanged.
105
+
106
+ ### Approvals and the agent's own permission modes
107
+
108
+ When the agent wants to touch something sensitive, it asks you. The approval card shows up right in the chat composer; you click Allow or Deny, and it obeys.
109
+
110
+ Every agent asks differently: some offer a set of options, some only allow/deny, some attach the exact command they are about to run. The plugin presents these as-is, and never pre-decides for you.
111
+
112
+ **Permission modes are defined by the agent.** Each agent puts its own mode list (e.g. `Always Ask`, `Accept Edits`, `Plan`, `Bypass Permissions`) in the composer's tool row; the option text is exactly what the agent sent. Switch a mode, and the agent decides accordingly when to ask you. The plugin adds no permission tier of its own, and doesn't translate mode names.
113
+
114
+ > **Note**: DSH's own "Access mode" (read-only / workspace write / full) has no power over external agents. The agent lives in its own process; DSH's file sandbox can't reach it. What actually applies is the agent's own permission mode, plus the approvals it sends you.
115
+
116
+ ### NTR tab
117
+
118
+ The **NTR** tab in the conversation header shows the agent's raw stream:
119
+
120
+ - Plans and todo lists
121
+ - Every tool call's name, status, and output (collapsed by default; click to expand)
122
+ - Unified diffs of code changes, with `+N -M` line counts
123
+ - Usage readouts
124
+
125
+ The tab is read-only: it sends no messages, cancels no tasks, launches no processes. It follows live — no manual refresh.
126
+
127
+ **Division of labor between the main chat and the NTR tab**: your conversation with the agent lives in the main chat, and tool activity is inlined there in time order; the NTR tab holds what the main chat can't — plans, full outputs, diffs, usage.
128
+
129
+ ### Settings → NTR
130
+
131
+ - **Connected NTR agents**: one row per agent, with process status. Click "Expand config & models" for the full detail.
132
+ - **`★ Default`**: which agent new NTR conversations use by default.
133
+ - **Each agent card**: display name, launch command, npm package, environment variables, working directory — all in plain sight, no hidden defaults.
134
+ - **The launch command is one line**: program plus arguments, e.g. `codebuddy --acp`. Quote paths that contain spaces. Below the input, the plugin live-shows the program/arguments it parsed.
135
+ - **Test button**: launches a one-shot process with the current form values to do a handshake — nothing saved, nothing registered. Good for validating before you commit a config change.
136
+ - **Workspace directory**: the directory the agent works in.
137
+ - **Models visible in conversations**: when an agent reports hundreds of models, check here which ones appear in the model chip's menu. Unchecked ones still work; they're just not listed.
138
+
139
+ ### Your workspace
140
+
141
+ An NTR conversation started in chat uses the directory you selected in this DSH session (the one you clicked in the side panel) as its working directory. The agent's file reads, searches, and command execution all happen there; its process is also launched from there.
142
+
143
+ One class of agents pins the workspace the moment the process starts, and ignores session-parameter changes afterwards. The plugin relocates such an agent to the new directory when it has no active conversations; while conversations are live, it keeps the original process and doesn't interrupt you.
144
+
145
+ ---
146
+
147
+ ## FAQ
148
+
149
+ **Will the platform risk-control me? Will I get banned? Is this a reverse proxy?**
150
+
151
+ No, and it isn't a reverse proxy. dsh-ntr talks to agents over ACP — the officially supported protocol entry point of those agents. The plugin only launches your local agent process and talks to it; it never touches platform APIs and forwards no requests. Model calls are made by the agent itself, no different from you using it directly in a terminal. Reasonable use within what NTR provides is officially allowed, and won't get you banned.
152
+
153
+ **Why is DSH's model selector gone from the composer after I pick an agent?**
154
+
155
+ By design. In NTR conversations, models are the agent's own concept, so that spot becomes the NTR base. Switch back to a normal-mode conversation, and the native selector returns.
156
+
157
+ **Why does the agent name at the composer's end disappear once the first turn starts?**
158
+
159
+ One conversation, one agent. Before the first turn, the base stays so you can change your pick; the moment the first turn starts, the conversation locks to that agent, and the name becomes the read-only `NTR · <agent>` tag on the top edge of the composer card. Switching agents would lose this side's context, so once locked, you must start a new conversation. The model, thinking-level, and advanced-config chips are unaffected. If the first turn fails or is cancelled with no visible output, the lock rolls back and the base returns.
160
+
161
+ **How do I switch agents after locking?**
162
+
163
+ Start a new conversation and pick again. A locked conversation refuses rebinding via `session/chat-bind` (the host returns `agent_locked`) — deliberately: the new agent has no context of this conversation, and a fake switch would only yield an amnesiac chat. One exception: if the first turn failed without producing any visible output, the lock auto-rolls-back, and you can re-pick directly.
164
+
165
+ **Can't see the NTR tab at the top?**
166
+
167
+ Every conversation has this tab. If the conversation has never been bound to an agent, it's empty and prompts you to pick one. First select "NTR mode" in the mode picker, then pick an agent in the NTR base.
168
+
169
+ **Agent stuck?**
170
+
171
+ Click Stop without worry. The plugin cancels the turn, and force-finalizes it if the agent ignores the cancel — a conversation never hangs forever. If the agent process crashed, the next message automatically brings it back up and replays the context; history survives.
172
+
173
+ **Agent replied with an empty bubble?**
174
+
175
+ Some agents treat upstream failures (auth, billing, quota) as "normal completion". When the plugin detects this, it appends a warning instead of letting the failure pass silently.
176
+
177
+ **Agent unresponsive, and no error either?**
178
+
179
+ Check the Test button in Settings. It runs a full handshake on the host side and surfaces the real error (including the tail of stderr).
180
+
181
+ **Model list too long or too short?**
182
+
183
+ The model chip's menu has no search box, so models are **opt-in**. Check the ones you want on that settings card. Unchecked ones simply don't appear, sparing you a wall of hundreds of models.
184
+
185
+ **Why don't my messages appear in the NTR tab?**
186
+
187
+ That tab shows only the agent's raw stream (its plans, full tool outputs, diffs, usage). Your conversation with it lives in the main chat, with tool activity inlined there in order.
188
+
189
+ **Why is there an extra dropdown in the composer, with options I've never seen?**
190
+
191
+ That's the agent's own permission mode; the option text comes straight from it. Each agent names them differently, and the plugin lists them verbatim. It governs when the agent asks you — unrelated to DSH's "Access mode".
192
+
193
+ **Imported an agent, and its permission modes differ from other agents'?**
194
+
195
+ 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
+
197
+ ---
198
+
199
+ ## What it can do (feature list)
200
+
201
+ - **Any ACP agent**: common presets built in, and any stdio-capable ACP agent works. Custom agents carry a "Custom" badge.
202
+ - **Multi-conversation isolation**: several conversations can talk to different agents at the same time, fully independent.
203
+ - **Streaming everything**: user messages, agent replies, thinking, tool execution, and task plans are all streamed.
204
+ - **Inlined tool activity**: tool calls are inserted into the main chat in the order they happened, with thinking and replies on either side — no layering.
205
+ - **Real permissions**: the agent's permission modes are listed verbatim, and every permission request is presented as-is. What you answer is what it receives.
206
+ - **Slash commands belong to the agent**: in NTR conversations, `/` pops the agent's own declared commands; DSH's same-named commands don't hijack your input.
207
+ - **Reconnect**: after a browser refresh or a network drop, history backfills in order. If the agent process crashed, the next message recovers it.
208
+ - **Idle release**: agent processes with no activity exit automatically after an idle window, freeing memory. Processes with live conversations are never released.
209
+ - **Windows works**: npm-globally-installed agents launch and get reaped correctly on Windows, leaving no stray background processes.
210
+
211
+ ---
212
+
213
+ ## Configuration reference (for developers)
214
+
215
+ That was the user part. What follows is for people modifying this plugin, or writing config files by hand.
216
+
217
+ ### Minimal config
218
+
219
+ Mount it in your profile's `cordis.patch.yml`. When `cwd` / `allowedCwdRoots` are omitted, the plugin's own workspace `~/.dsh/dsh-ntr/workspaces` is used (created automatically on save); `command` is **one full command line** — quote paths that contain spaces.
220
+
221
+ ```yaml
222
+ - id: dsh-ntr
223
+ name: dsh-ntr
224
+ config:
225
+ agents:
226
+ - id: pi
227
+ title: pi coding agent (ACP)
228
+ command: /path/to/bin/pi-acp # e.g. "codebuddy --acp"
229
+ cwd: ~/.dsh/dsh-ntr/workspaces
230
+ allowedCwdRoots: [~/.dsh/dsh-ntr/workspaces]
231
+ ```
232
+
233
+ Optional agent fields: `npmPackage` (auto `npm install -g` when not found on PATH), `exposedModels` (which variants the NTR seat menu lists), `idleTimeoutMs` (idle-release window for chat processes; `0` = never release, default 30 minutes). Plugin-level there are also `transcriptMaxEntries` (per-conversation event buffer cap, default 600) and `probeModelsOnBoot` (kept for compatibility; boot-time per-agent probing no longer happens — see the comment on `bootstrapCatalogs`). Reliability timeouts and retries (`startupTimeoutMs`, `cancelFallbackMs`, `staleCancelRetryDelayMs`, `staleCancelMaxRetries`, `permissionTimeoutMs`, `permissionFallbackTimeoutMs`) are covered by [AGENTS.md](./AGENTS.md) constraints 12 and 13; anything editable in the Settings page should be edited there.
234
+
235
+ ### Build & install
236
+
237
+ ```bash
238
+ npm install # dependencies
239
+ npm run build # host TS declarations + browser bundle
240
+ npm test # test suite
241
+ dsh plugin --profile web add ./ # mount into the Web profile
242
+ ```
243
+
244
+ ### Implementation notes
245
+
246
+ Each item below corresponds to a real past failure; read the linked doc before changing code. The browser half talks to the host half via `/acp` RPC and `/api/acp/stream` SSE; the host half runs one `AgentBridge` per conversation, driving the agent child process over stdio JSON-RPC.
247
+
248
+ Code map: `src/index.ts` (plugin entry) · `src/protocol.ts` (shared types) · `src/impl.ts` (pure host logic) · `src/bridge.ts` (child-process bridge) · `src/client/` (React extension) · `presets/acp/` (NTR mode declaration) · `~/.dsh/dsh-ntr/` (runtime state).
249
+
250
+ - **DSH renders, nothing more**: the plugin doesn't write DSH's model routing, doesn't take over the host's model catalog, and never lets ACP models appear in DSH's native selector.
251
+ - **Workspace = the side-panel path**: the directory opened in the side panel must become both the `session/new` cwd **and** the agent child's own cwd. One agent (codebuddy) pins its workspace at process startup.
252
+ - **NTR mode = an agent preset**: declared by `presets/acp/acp.patch.yml` shipped with the package — no tools, no injected project instructions, no compaction, so everything the model sees belongs to the remote agent.
253
+ - **Forwarding is strictly the user's text**: messages are picked by `source.kind === 'user'`; title generation, compaction, and other auxiliary calls aren't forwarded, and no boundary narration is ever spliced in.
254
+ - **Stepwise splitting = native interleaved tool cards**: an ACP turn is split into multiple host steps by tool activity — a segment that ends with a `tool-call` card closes its stream, the host settles the message and executes the card (real output answered through a guard virtual tool), then continues with the next step; continuation segments resume reading the same still-in-flight `session/prompt` and never send a new one. Cards thus sit between two assistant nodes, matching native multi-step conversations.
255
+ - **Binding must be wired into the request path**: the guard's `agent/request` waterfall (`prepend: true`) rewrites requests to the bound `acp/<agent>`, and the adapter double-checks.
256
+ - **Permission modes and approvals are two layers, both agent-owned**: the control in `composer-mode.tsx` renders only the `availableModes` the agent sends via `session/set_mode` — no visible label, no translated option names; `session/request_permission` maps straight to the user. The plugin has no permission switch of its own: if the agent asks, a human sees it. The earlier host-adjudicated semantics (`decidePermission`, the fs write fence) have been removed — see `docs/acp-convergence.md` §2.3.
257
+ - **Cross-platform paths**: `impl.ts` can't import `node:path`; absolute-path and containment checks are hand-written, with dedicated Windows and UNC cases.
258
+
259
+ ### Documentation index
260
+
261
+ - [AGENTS.md](./AGENTS.md) — **read its "what this plugin is" section first**: which constraints are retired, which target shapes have landed
262
+ - [NTR mode: handing the conversation to another agent](./docs/acp-mode.md) — design, field evidence, and hard boundaries
263
+ - [ACP convergence: display layer only](./docs/acp-convergence.md) — **the single authoritative reference for code convergence**
264
+ - [General guide to DSH plugin development](./docs/dsh-plugin-development-guide.md)
265
+ - [DSH official source reference & architecture index](./docs/README.md)
package/README.md CHANGED
@@ -1,3 +1,265 @@
1
- # Temporary Holding Version
1
+ # dsh-ntr
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [English](./README.en.md) | 简体中文
4
+
5
+ 在 [DSH(DeepSeek Harness)](https://github.com/deepseek-ai/deepseek-harness) 的网页/桌面端里,用你习惯的界面,直接和任意一个 **ACP agent**(CodeBuddy、Pi、Oh My Pi、Claude Code、OpenCode、Qoder 等)对话。
6
+
7
+ **DSH 提供界面,agent 提供能力。** 会话列表、输入框、侧栏、消息展示全部来自 DSH;推理、工具执行、文件读写、权限判断全部由你自己配置的那个 agent 完成,它跑在独立进程里,完全自治。这个插件只做一件事:把两边接通,并把 agent 的输出原样画到 DSH 的界面上。
8
+
9
+ > 模型选择、思考等级、工作模式都是 **agent 自己的概念**,不是 DSH 的模型目录。插件不替你换模型,也不会把 agent 的选择写进 DSH 的全局设置。
10
+
11
+ ---
12
+
13
+ ## NTR 是什么
14
+
15
+ **NTR = Negotiated Target Routing(协商式目标路由)**。
16
+
17
+ 一句话:你的宿主 Agent 作为 ACP 客户端,和目标 Agent 协商能力与权限,把这一轮任务交给它执行。
18
+
19
+ ---
20
+
21
+ ## 安装
22
+
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
+
25
+ ### 方式一:从 npm registry 安装
26
+
27
+ ```bash
28
+ dsh plugin --profile web add dsh-ntr@latest
29
+ ```
30
+
31
+ 升级重跑同一条命令即可。
32
+
33
+ > `add` 时**务必带 `@latest`**:不带的话包管理器会一直复用首次写入的版本号,之后连 `update` 也升不上去。
34
+
35
+ ### 方式二:经 [dsh-market](https://github.com/dsh-market/dsh-market) 插件市场安装
36
+
37
+ 先装市场本体:
38
+
39
+ ```bash
40
+ dsh plugin --profile web add dshmarket
41
+ ```
42
+
43
+ 重启 `dsh web`,打开**插件**页:dsh 0.2.1 起市场是「已安装」旁边的**插件市场**标签页;更早的版本在**设置 → 插件市场**。搜索 `dsh-ntr`,确认来源后一键安装,装完刷新页面即可使用;之后的更新检查与升级也在市场里完成。官方 DSH 桌面 App 自带这个市场,直接在 App 的**插件**页里搜索安装即可。
44
+
45
+ > 市场的插件清单来自 [awesome-dsh-plugin](https://awesome-dsh-plugin.com) 精选目录,每天自动刷新。若搜索结果里暂时没有 `dsh-ntr`,用方式一从 npm 安装,不影响使用。
46
+
47
+ ### 方式三:从源码安装
48
+
49
+ 需要 Node.js ≥ 22.12.0。
50
+
51
+ ```bash
52
+ git clone https://github.com/GDWhisper/dsh-ntr.git
53
+ cd dsh-ntr
54
+ npm install # 安装构建依赖(typescript 等)
55
+ npm run build # 编译宿主 TS 与浏览器 Bundle 到 lib/(构建产物不入库,此步必须执行)
56
+ dsh plugin --profile web add ./
57
+ ```
58
+
59
+ 之后升级:`git pull` → `npm run build` → 重启 `dsh web`。`add .` 以 `link:` 方式安装,重建后重启即生效,不需要重新 add。
60
+
61
+ > `dsh plugin` 是 pnpm 转发器,`--profile <name>` 必填;`add .` 会把当前目录以 `link:` 方式装进 profile。官方桌面 App 的 profile 由它自己的插件管理器掌管,不要对着它的 profile 跑命令行。
62
+
63
+ ---
64
+
65
+ ## 上手
66
+
67
+ ### 你需要什么
68
+
69
+ 1. 一个已经装好的 DSH(Web 或桌面版)。
70
+ 2. 至少一个 ACP agent 的命令行程序。插件内置了几个常见预设,也可以填任何支持 stdio 的 ACP agent。
71
+
72
+ ### 三步接入
73
+
74
+ 1. 打开 **设置 → NTR**。
75
+ 2. 在「Agent 列表」里选一个预设一键填入,或点「从零新建自定义 Agent」手填启动命令。
76
+ 3. 点「测试」。成功会显示 agent 的名字和握手耗时;失败会显示可操作的错误和 stderr 尾部。
77
+
78
+ 保存后,这个 agent 就出现在输入框行尾的 NTR 底座里。
79
+
80
+ ### 开始对话
81
+
82
+ 1. 新建一个会话,在模式选择里选「**NTR 模式**」。
83
+ 2. 在输入框行尾的 NTR 底座里选一个 agent。
84
+ 3. 直接说话。
85
+
86
+ agent 的回复和思考显示在主聊天区。工具活动按发生顺序插在它们中间:一行 `🛠️ **工具名** · 参数`,输出跟一个代码块。想看完整的工具输出、计划和 diff,切到对话头部的 **NTR** 标签页。
87
+
88
+ ---
89
+
90
+ ## 你会用到的界面
91
+
92
+ ### NTR 底座(输入框行尾)
93
+
94
+ 点一下,选哪个 agent 服务这条会话。这个菜单里只有 agent 列表——一个控件一件事。agent 自己的模型和思考等级是同一行里各自独立的芯片,各开各的菜单;如果 agent 还发布了额外配置项(温度、供应商开关等),模型芯片左边会多一个「高级」底座,点开逐项设置。首轮之前 agent 底座固定在行尾最右侧;锁定后它退场,模型和思考等级接管右端。那些清单由 agent 自己上报,不是你熟悉的 DSH 模型列表。
95
+
96
+ 底座只出现在 **NTR 模式**的会话里。普通会话里看到的是 DSH 原生的模型选择器。
97
+
98
+ **一个会话一个 agent。** 首条消息发出前,底座一直在,你可以先选好、也可以反悔改选。首轮**开始**时会话就**锁定**在这个 agent 上:底座粉碎,名字变成**输入框卡片上边缘的小挂牌**(`NTR · <agent>`,骑在顶边上),挂牌只显示、不能点击——换 agent 请新建会话。模型、思考等级和「高级」芯片留在原位,它们属于同一个 agent,不涉及上下文迁移。如果首轮还没产生任何可见输出就失败/取消,锁定会自动回滚,底座回来,可以直接重选。
99
+
100
+ ### `/` 命令属于 agent
101
+
102
+ 在 NTR 会话里输入 `/`,弹出的是 **agent 自己的命令清单**——它通过 ACP 声明的那些(例如 `compact`、`session`、`model`),不是 DSH 的命令。选中一项只是把命令名填进输入框;回车后整行原样发给 agent,由它执行。清单在**第一条消息之前**就能用:宿主记住了这个 agent 上次会话声明的命令(从没跑过的 agent 要等第一轮之后才有)。
103
+
104
+ DSH 自己的 `/model`、`/permission`、`/export` 等在这一模式下**不会**被宿主截走:同名命令也交给 agent,宿主命令菜单与技能补全不会出现。普通会话的斜杠行为完全不变。
105
+
106
+ ### 审批与 agent 自己的权限模式
107
+
108
+ agent 要动敏感的东西时会问你。审批卡直接出现在聊天输入框里,你点允许或拒绝,它照着做。
109
+
110
+ 每个 agent 问法不一样:有的给一组选项,有的只给允许/拒绝,有的会附上它要执行的具体命令。插件把这些原样呈现给你,不预先替你决定。
111
+
112
+ **权限模式由 agent 自己定义。** 每个 agent 在输入框工具行给出它自己的模式清单(例如 `Always Ask`、`Accept Edits`、`Plan`、`Bypass Permissions`),选项文字就是 agent 下发的原文。你切换模式,agent 就按新模式决定什么时候来问你。插件不另设一套权限档,也不替你翻译这些模式名。
113
+
114
+ > **注意**:DSH 自己的「访问模式」(只读 / 工作区写入 / 完全权限)管不到外部 agent。那个 agent 在自己的进程里,DSH 的文件沙箱对它没有作用力。真正生效的是 agent 自己的权限模式,以及它向你发起的那些审批。
115
+
116
+ ### NTR 标签页
117
+
118
+ 对话头部的 **NTR** 标签页显示 agent 的原始流:
119
+
120
+ - 计划和待办清单
121
+ - 每次工具调用的名称、状态、输出(默认折叠,点开看全部)
122
+ - 代码改动的 unified diff,带 `+N -M` 行数统计
123
+ - 用量读数
124
+
125
+ 这个标签页是只读的:它不发消息、不取消任务、不启动进程。实时跟随,不需要手动刷新。
126
+
127
+ **主聊天区和 NTR 标签页的分工**:人和 agent 的对话在主聊天区,工具活动也按时间顺序内联在那里;NTR 标签页放主聊天区装不下的东西——计划、完整输出、diff、用量。
128
+
129
+ ### 设置 → NTR
130
+
131
+ - **已接入的 NTR 智能体**:每个 agent 一行,标着进程状态。点开「展开配置与模型」看全部细节。
132
+ - **`★ 默认`**:新建 NTR 会话默认用哪个 agent。
133
+ - **每张 agent 卡片**:显示名、启动命令、npm 包名、环境变量、工作目录,全部直接摆在你面前,没有藏起来的默认值。
134
+ - **启动命令是一整行**:程序加参数,例如 `codebuddy --acp`。含空格的路径用引号包住。输入框下方会实时显示插件解析出的「程序 / 参数」。
135
+ - **测试按钮**:用当前表单里的值拉起一个一次性进程做握手,不保存、不注册。适合在改配置前先验证。
136
+ - **工作区目录**:agent 在哪个目录里干活。
137
+ - **对话中可见的模型**:agent 能报几百个模型时,这里挑你想在模型芯片菜单里看到的。没勾的仍然可用,只是不列出来。
138
+
139
+ ### 你的工作区
140
+
141
+ 在对话里起的 NTR 会话,工作目录就是你在这个 DSH 会话里选中的那个目录(侧栏点的那个)。对端 agent 的读文件、搜索、执行命令都在那里,它自己的进程也从那里启动。
142
+
143
+ 有一类 agent 在进程启动那一刻就把工作区定死了,只改会话参数它不听。插件在它没有活动会话时会把它搬到新目录;有会话在进行时就保持原进程,不打断你的对话。
144
+
145
+ ---
146
+
147
+ ## 常见问题
148
+
149
+ **会被平台风控吗?会封号吗?是反代吗?**
150
+
151
+ 不会,也不是反代。dsh-ntr 与 agent 之间的通信走 ACP——这是 agent 官方支持的协议入口。插件只负责启动你本地的 agent 进程并和它对话,全程不接触平台 API、不转发任何请求。模型调用由 agent 自己完成,跟你在终端里直接用它没有区别。在 NTR 提供的能力范围内合理使用,是官方允许的做法,不会因此封号。
152
+
153
+ **为什么选完 agent 之后,输入框里没看到 DSH 的模型选择器?**
154
+
155
+ 那是设计如此。NTR 会话里,模型是 agent 自己的概念,所以那个位置换成了 NTR 底座。切回普通模式的会话,原生选择器就回来了。
156
+
157
+ **为什么首轮开始后,输入框行尾不显示 agent 名字了?**
158
+
159
+ 一个会话一个 agent。首轮之前底座留着让你改选;首轮一开始,会话就锁在这个 agent 上,名字变成挂在输入框卡片上边缘的 `NTR · <agent>` 只读小挂牌。换 agent 会丢掉它这边的上下文,所以确认锁定后只能新建会话。模型、思考等级和高级配置芯片不受影响。首轮没有任何可见输出就失败/取消时,锁定会回滚,底座回来。
160
+
161
+ **锁定后想换 agent 怎么办?**
162
+
163
+ 新建一个会话重新选。已锁定的会话不会接受 `session/chat-bind` 的改绑(宿主返回 `agent_locked`),这是刻意的:新 agent 没有这段对话的上下文,假装切换只会得到一段失忆的对话。首轮没有产生任何可见输出就失败的情况例外:锁定会自动回滚,可以直接重选。
164
+
165
+ **顶部看不到 NTR 标签页?**
166
+
167
+ 每条会话都有这个标签页。这条会话还没绑过 agent 时,它是空的,会提示你去选一个。先在模式选择里选「NTR 模式」,再在 NTR 底座里选一个 agent。
168
+
169
+ **agent 卡住不动了?**
170
+
171
+ 可以放心点停止。插件会取消这一轮,并在 agent 不理会取消时强制收尾,会话不会永久卡住。如果 agent 进程崩了,下次发消息会自动把它拉起来并回放上下文,历史不丢。
172
+
173
+ **agent 只回了一个空气泡?**
174
+
175
+ 一部分 agent 会把上游失败(认证、计费、配额)当成「正常结束」。插件检测到这种情况时会补一条告警,不让失败静默。
176
+
177
+ **agent 没反应,也没报错?**
178
+
179
+ 看设置页的测试按钮。它会在宿主侧跑一次完整的握手,把真正的错误(包括 stderr 尾部)显示出来。
180
+
181
+ **模型列表太长或太短?**
182
+
183
+ 模型芯片的菜单没有搜索框,所以模型是**按需勾选**的。去设置页那张卡片里勾你想看的。不勾就没有那些条目,避免几百个模型刷屏。
184
+
185
+ **为什么 NTR 标签页里没有我发的消息?**
186
+
187
+ 那个标签页只显示 agent 的原始流(它的计划、完整工具输出、diff、用量)。你和它的对话在主聊天区,工具活动按发生顺序内联在对话里。
188
+
189
+ **为什么输入框里多了一个下拉,里面的选项我没见过?**
190
+
191
+ 那是 agent 自己的权限模式,选项文字由它下发。每个 agent 的叫法不一样,插件照原样列出。它管的是 agent 什么时候来问你,与 DSH 的「访问模式」无关。
192
+
193
+ **导入了 agent,但它的权限模式和别的 agent 不一样?**
194
+
195
+ 正常。模式清单由每个 agent 自己声明(`session/set_mode` 下发),插件照原样列出,不统一翻译、也不补默认值。想改叫法要在 agent 自己的配置里改。
196
+
197
+ ---
198
+
199
+ ## 它能做什么(能力清单)
200
+
201
+ - **任意 ACP agent**:内置常见预设,也可以接入任何支持 stdio 的 ACP agent。自定义 agent 带「自定义」徽标。
202
+ - **多会话隔离**:多个会话可以同时跟不同 agent 对话,各自独立。
203
+ - **流式展示**:用户消息、agent 回复、思考过程、工具执行、任务计划都是流式的。
204
+ - **工具活动内联**:工具调用按发生顺序插进主聊天区,思考和回答分列它两侧,不分层。
205
+ - **真实权限**:agent 的权限模式原样列出,它的每次权限询问也原样呈现,你答什么它收到什么。
206
+ - **斜杠命令归 agent**:NTR 会话里 `/` 弹出的是 agent 自己声明的命令;DSH 的同名命令不会截走你的输入。
207
+ - **断线重连**:浏览器刷新或断网后,历史按序补齐。agent 进程崩了,下次发消息自动恢复。
208
+ - **空闲释放**:没有活动的 agent 进程会在空闲一段时候后自动退出,不占内存。有会话的进程不会被释放。
209
+ - **Windows 可用**:npm 全局安装的 agent 在 Windows 上也能正常启动和回收,不会残留后台进程。
210
+
211
+ ---
212
+
213
+ ## 配置参考(开发者)
214
+
215
+ 以上是用户部分。以下写给要改这个插件、或要手工写配置文件的人。
216
+
217
+ ### 最小配置
218
+
219
+ 在 Profile 层的 `cordis.patch.yml` 里挂载。省略 `cwd` / `allowedCwdRoots` 时用插件自有工作区 `~/.dsh/dsh-ntr/workspaces`(保存时自动创建);`command` 是**一整条命令行**,含空格的路径用引号包住。
220
+
221
+ ```yaml
222
+ - id: dsh-ntr
223
+ name: dsh-ntr
224
+ config:
225
+ agents:
226
+ - id: pi
227
+ title: pi coding agent (ACP)
228
+ command: /path/to/bin/pi-acp # 例如 "codebuddy --acp"
229
+ cwd: ~/.dsh/dsh-ntr/workspaces
230
+ allowedCwdRoots: [~/.dsh/dsh-ntr/workspaces]
231
+ ```
232
+
233
+ 可选的 agent 字段:`npmPackage`(PATH 里找不到时自动 `npm install -g`)、`exposedModels`(NTR 座位菜单列哪些变体)、`idleTimeoutMs`(聊天进程的空闲释放窗口,`0` = 永不释放,默认 30 分钟)。插件级还有 `transcriptMaxEntries`(会话事件缓冲上限,默认 600)与 `probeModelsOnBoot`(保留字段,启动时已不再为每个 agent 探测,见 `bootstrapCatalogs` 的注释)。可靠性超时与重试的默认值(`startupTimeoutMs`、`cancelFallbackMs`、`staleCancelRetryDelayMs`、`staleCancelMaxRetries`、`permissionTimeoutMs` 与 `permissionFallbackTimeoutMs`)见 [AGENTS.md](./AGENTS.md) 约束 12 与 13;设置页能改的都在设置页改。
234
+
235
+ ### 构建与安装
236
+
237
+ ```bash
238
+ npm install # 依赖
239
+ npm run build # 宿主 TS 声明 + 浏览器 Bundle
240
+ npm test # 测试套件
241
+ dsh plugin --profile web add ./ # 挂载到 Web Profile
242
+ ```
243
+
244
+ ### 实现要点
245
+
246
+ 以下每条都对应一处真实故障,改代码前先读对应文档。浏览器半区经 `/acp` RPC 与 `/api/acp/stream` SSE 和宿主半区通信;宿主半区每会话一个 `AgentBridge`,经 stdio JSON-RPC 驱动 agent 子进程。
247
+
248
+ 代码位置:`src/index.ts`(插件入口)· `src/protocol.ts`(共享类型)· `src/impl.ts`(纯宿主逻辑)· `src/bridge.ts`(子进程通信桥)· `src/client/`(React 扩展)· `presets/acp/`(NTR 模式声明)· `~/.dsh/dsh-ntr/`(运行时落盘)。
249
+
250
+ - **DSH 只出显示层**:插件不写 DSH 的模型路由,不接管宿主的模型目录,也不让 ACP 模型出现在 DSH 原生选择器里。
251
+ - **工作区即侧栏路径**:侧栏点开的目录必须成为 `session/new` 的 cwd **和** agent 子进程自己的 cwd。有一条 agent(codebuddy)在进程启动那一刻就定死工作区。
252
+ - **NTR 模式 = agent preset**:由 `presets/acp/acp.patch.yml` 随包声明,不挂工具、不注入项目指令、不做 compaction,让模型可见的表面完全属于对端 agent。
253
+ - **转发严格等于用户文本**:按 `source.kind === 'user'` 挑消息,不转发标题生成、compaction 这类辅助调用,也不拼任何边界叙述。
254
+ - **逐步切分 = 原生交错的工具卡**:一个 ACP turn 按工具活动切成多个宿主 step——段以 `tool-call` 卡收尾即结束本次 stream,宿主落定消息、执行卡(真实输出经 guard 虚拟工具应答)、续跑下一步;续跑段续读同一个仍在飞的 `session/prompt`,绝不新发 prompt。卡片因此夹在两个 assistant 节点之间,与原生多步对话一致。
255
+ - **绑定必须接到请求路径上**:guard 的 `agent/request` waterfall(`prepend: true`)把请求改写成绑定指向的 `acp/<agent>`,adapter 再做一次复查。
256
+ - **权限模式与审批是两层,都以 agent 为准**:`composer-mode.tsx` 的控件只渲染 agent 经 `session/set_mode` 下发的 `availableModes`,不写可见标签、不翻译选项名;`session/request_permission` 原样映射给用户。插件没有自己的权限开关:agent 问了就一定到人面前。早先那套由宿主替 agent 裁定的端口语义(`decidePermission`、fs 写栅栏)已删除,见 `docs/acp-convergence.md` §2.3。
257
+ - **跨平台路径**:`impl.ts` 不能 import `node:path`,绝对路径判定与 containment 全部手写,Windows 与 UNC 有专门用例。
258
+
259
+ ### 文档索引
260
+
261
+ - [AGENTS.md](./AGENTS.md) —— **先读「本插件是什么」一节**:哪些约束已废弃、哪些目标形态已落地
262
+ - [NTR 模式:把会话交给另一个 agent](./docs/acp-mode.md) —— 设计、实测证据与硬边界
263
+ - [ACP 收敛方向:只借显示层](./docs/acp-convergence.md) —— **代码收敛的唯一权威依据**
264
+ - [DSH 插件开发通用指南](./docs/dsh-plugin-development-guide.md)
265
+ - [DSH 官方源码参考与架构索引](./docs/README.md)
@@ -0,0 +1,72 @@
1
+ # Mount line for the dsh-ntr bundle.
2
+ #
3
+ # The pure package-name row is REQUIRED for the ClientModuleRegistry to match
4
+ # the browser half (dsh.client in package.json); the host half is the same row
5
+ # (one plugin module carries both Node and browser entries).
6
+ #
7
+ # `agents` below SEEDS the agent registry at boot (BridgeManager's constructor
8
+ # `defs`), but it is not the only source and not a boundary. The settings page's
9
+ # launch-command box (`AgentForm.commandLine` in src/client/panel.tsx) writes
10
+ # through RPC `agent/save` → `validateAgentDef` → `BridgeManager.saveAgent` into
11
+ # `~/.dsh/dsh-ntr/agents.json`, and that is what spawns the child.
12
+ # `validateAgentDef` checks shape only (non-empty, splittable command line,
13
+ # length) — there is no program whitelist, so the browser CAN name any program.
14
+ # A child's env comes from the same two places, and `detectHostAgents` hands the
15
+ # stored `command` / `args` / `env` back to the browser UNSANITIZED as
16
+ # `currentConfig`, where `envRows` edits them.
17
+ - insert:
18
+ - id: dsh-ntr
19
+ name: dsh-ntr
20
+ config:
21
+ agents: []
22
+ # transcriptMaxEntries: 600
23
+ # probeModelsOnBoot: true # DEAD — validated and stored, never read.
24
+ # `bootstrapCatalogs` is empty, so boot
25
+ # probes nothing whatever this says; true
26
+ # and false behave identically. Real
27
+ # probes: RPC `agent/probe-models` (the
28
+ # settings button) and `probeAgentModels`
29
+ # after `saveAgent`.
30
+ # Reliability tuning (defaults shown):
31
+ # startupTimeoutMs: 60000 # budget for initialize / session/new / session/load
32
+ # permissionTimeoutMs: 0 # auto-cancel a pending approval after N ms. 0 waits
33
+ # for the human on the DESIGNED ledger path; on a
34
+ # DEGRADED native route the turn still ends, after
35
+ # `permissionFallbackTimeoutMs` (default 300000) —
36
+ # see `LedgerFallbackReason`.
37
+ # cancelFallbackMs: 15000 # force-finalize a turn the agent ignores after session/cancel (0 = off)
38
+ # staleCancelRetryDelayMs: 600 # resend a prompt answered cancelled with no cancel requested (0 = at once)
39
+ # staleCancelMaxRetries: 2 # cap on those resends before the cancelled stop is reported as-is
40
+ # ACP 模式 (agent preset) is declared by this package's own bundle
41
+ # patch list (package.json `dsh.bundle.patch` → presets/acp/acp.patch.yml),
42
+ # so it needs no install step and has no config knob. The settings card
43
+ # only reports the harness roster's verdict for the `preset-acp` row.
44
+ # The plugin's OWN default ACP agent (panel's ★ button) lives in the
45
+ # agent registry file at `~/.dsh/dsh-ntr/agents.json`, key
46
+ # `defaultAgentId` (`pluginStateDir` + `BridgeManager.getStoragePath`).
47
+ # `~/.dsh/acp-agents.json` is only a MIGRATION SOURCE (`legacyStatePaths`):
48
+ # `migrateLegacyState` moves it into the plugin directory on first boot,
49
+ # and nothing writes the old path afterwards.
50
+ # This plugin never reads or writes DSH's deployment default model: no
51
+ # ACP route is ever written there, so there is nothing to guard. (An
52
+ # older revision did write one and needed a host-side guard to put a
53
+ # native route back — both are gone; see AGENTS.md constraint 22.)
54
+ # Per-agent ACP-model filter — which of the agent's models its own
55
+ # conversation seats offer (panel checkboxes persist here). ACP models
56
+ # are never advertised in DSH's model catalog, so they cannot leak into
57
+ # a non-ACP conversation:
58
+ # agents:
59
+ # - id: pi
60
+ # command: pi-acp
61
+ # npmPackage: pi-acp # auto `npm install -g` when command is missing from PATH
62
+ # exposedModels: [model-a, model-b] # opt-in `agent@choice` variants; unset/[] = base entry only
63
+ # idleTimeoutMs: 1800000 # release a chat process once it has been idle
64
+ # # this long (ms) with NO live session
65
+ # # (0 = never; unset = 30 min). This window
66
+ # # applies ONLY to a process that has hosted a
67
+ # # session: a probe child (`everHostedSession`
68
+ # # false — what a model probe or a post-save
69
+ # # probe leaves behind, i.e. the row the panel
70
+ # # shows as connected) is released after
71
+ # # `probeIdleTimeoutMs` (5 min) whatever this
72
+ # # says, 0 included.