dsh-acp-enhanced 0.3.1 → 0.3.5
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-zh.md +22 -8
- package/README.md +28 -8
- package/lib/index.js +403 -72
- package/package.json +1 -1
package/README-zh.md
CHANGED
|
@@ -26,8 +26,12 @@ ACP 线上。
|
|
|
26
26
|
|
|
27
27
|
### Zed 深度集成
|
|
28
28
|
|
|
29
|
-
-
|
|
30
|
-
|
|
29
|
+
- **工具卡片**:折叠态即显示一行摘要——`Read <路径>`、shell 命令显示模型自己给出的意图描述
|
|
30
|
+
(`description`,Codex 风格,展开可见完整命令)、`Search: <模式>`、
|
|
31
|
+
`Fetch: <URL>` 等。卡片正文遵循 ACP 最佳实践:文件编辑渲染为真实 **diff 视图**、
|
|
32
|
+
shell 命令渲染为高亮代码块并在下方附输出、涉及文件以**可点击路径**呈现(点击直达);
|
|
33
|
+
`rawInput` / `rawOutput` 保留在展开区备查,按工具类型渲染图标,
|
|
34
|
+
状态机为进行中 → 完成/失败
|
|
31
35
|
- **Zed 文件与终端**:`zed_read_text_file` / `zed_write_text_file` / `zed_terminal` 把
|
|
32
36
|
文件编辑放进 Zed 的"编辑文件"区(diff + 接受/拒绝)、命令跑在 Zed 真实终端
|
|
33
37
|
- **原生表单提问**:`ask_user_question` → `elicitation/create` 表单,选项即点即答
|
|
@@ -37,6 +41,10 @@ ACP 线上。
|
|
|
37
41
|
|
|
38
42
|
- **恢复与归档**:`session/load` 恢复历史线程(完整回放);`session/list` /
|
|
39
43
|
`session/delete` 管理线程归档(带标题、按更新时间排序);标题实时推送
|
|
44
|
+
- **多根工作区**:`sessionCapabilities.additionalDirectories` 已声明,Zed 不再提示
|
|
45
|
+
"This agent doesn't currently support multi-root workspaces",而是把所有工作区根
|
|
46
|
+
通过 `session/new` / `session/load` 传入。所有根都会写进系统提示词并在
|
|
47
|
+
`session/list` 上回报;沙箱仍以主 `cwd` 为唯一可写根(见已知限制)
|
|
40
48
|
|
|
41
49
|
### 命令
|
|
42
50
|
|
|
@@ -188,7 +196,8 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
188
196
|
|---|---|
|
|
189
197
|
| `exec: dsh: not found`(status 127) | 用随附 `dsh-acp-zed.sh` 启动器(自定位 node/dsh) |
|
|
190
198
|
| `no API key for provider route "xxx"` | 写入 `~/.dsh/.credentials.yaml`,或在 agent_servers 里设 `env.DEEPSEEK_API_KEY` |
|
|
191
|
-
| 无法切换模型
|
|
199
|
+
| 无法切换模型 | 保存的 `reasoning_effort` 默认值(或会话当前 effort)被带到新模型上;0.3.3 起新模型不支持的 effort 会自动回退为该模型默认值,不再让整个切换失败。另检查:是否选到了不可路由的"幽灵 provider"——本桥默认过滤(只广播 `config.provider` 的模型),确认 profile 的 provider 指向真实路由 |
|
|
200
|
+
| 上下文用量不显示 | 选到了不可路由的"幽灵 provider";本桥默认过滤(只广播 `config.provider` 的模型),确认 profile 的 provider 指向真实路由 |
|
|
192
201
|
| 需要详细诊断 | `ACP_DEBUG=1 dsh --profile acp-enhanced`(stderr 生命周期 trace) |
|
|
193
202
|
|
|
194
203
|
## 开发
|
|
@@ -203,8 +212,13 @@ node scripts/acp-resume-test.mjs # 会话恢复测试
|
|
|
203
212
|
|
|
204
213
|
## 已知限制
|
|
205
214
|
|
|
206
|
-
仅 baseline prompt
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
仅 baseline prompt(无图片/音频附件)、文本按块粒度流式、每会话同时一个 in-flight
|
|
216
|
+
prompt。MCP 支持 stdio 与 streamable HTTP(不声明 legacy SSE / `acp` 传输)。
|
|
217
|
+
`session/close` / `session/fork` / `session/resume` 未实现(不声明能力,合规客户端
|
|
218
|
+
不会调用);`session/delete` 因 dsh 持久化无官方删除 API,采用直接删除后端目录的方式。
|
|
219
|
+
|
|
220
|
+
多根工作区已声明、模型可见所有根,但 dsh 沙箱策略每会话只解析**一个可写根**(主
|
|
221
|
+
`cwd`,即 `session.header.cwd`),本地沙箱也只为该根开放写权限。读操作在所有根均可
|
|
222
|
+
用;`workspace-write` 下对附加根的写入会先被拒绝、需升级/批准,`danger-full-access`
|
|
223
|
+
下所有根均可写。真正的多根写支持需改 dsh 核心(`dsh-sandbox-policy` /
|
|
224
|
+
`dsh-sandbox-local` 需要根列表而非单根)。
|
package/README.md
CHANGED
|
@@ -29,8 +29,14 @@ over the ACP wire.
|
|
|
29
29
|
|
|
30
30
|
### Zed deep integration
|
|
31
31
|
|
|
32
|
-
- **Tool cards**:
|
|
33
|
-
|
|
32
|
+
- **Tool cards**: one-line summary in the collapsed header — `Read <path>`, the
|
|
33
|
+
model's own intent line for shell commands (`description`, Codex-style — the
|
|
34
|
+
exact command stays one click away), `Search: <pattern>`, `Fetch: <url>`, etc.
|
|
35
|
+
The card body follows the ACP best practice: file edits render as a real
|
|
36
|
+
**diff**, shell commands as a syntax-highlighted code block with the output
|
|
37
|
+
beneath, and touched files as **clickable locations** that open the file —
|
|
38
|
+
with `rawInput` / `rawOutput` kept one click away for transparency, plus
|
|
39
|
+
per-kind icons and a proper in-progress → completed/failed status lifecycle
|
|
34
40
|
- **Zed files & terminal**: `zed_read_text_file` / `zed_write_text_file` / `zed_terminal`
|
|
35
41
|
put file edits into Zed's "edited files" area (diff + accept/reject) and commands into a
|
|
36
42
|
real Zed terminal
|
|
@@ -43,6 +49,11 @@ over the ACP wire.
|
|
|
43
49
|
- **Resume & archive**: `session/load` restores past threads (full replay);
|
|
44
50
|
`session/list` / `session/delete` manage the thread archive (titled, sorted by last
|
|
45
51
|
activity); live title updates
|
|
52
|
+
- **Multi-root workspaces**: `sessionCapabilities.additionalDirectories` is advertised,
|
|
53
|
+
so Zed no longer shows "this agent doesn't currently support multi-root workspaces"
|
|
54
|
+
and instead passes every workspace root on `session/new` / `session/load`. All roots
|
|
55
|
+
are described to the model in the system prompt and reported on `session/list`; the
|
|
56
|
+
sandbox keeps the primary `cwd` as its single writable root (see Known limitations)
|
|
46
57
|
|
|
47
58
|
### Commands
|
|
48
59
|
|
|
@@ -204,7 +215,8 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
204
215
|
|---|---|
|
|
205
216
|
| `exec: dsh: not found` (status 127) | Use the shipped `dsh-acp-zed.sh` launcher (locates node/dsh itself) |
|
|
206
217
|
| `no API key for provider route "xxx"` | Write `~/.dsh/.credentials.yaml`, or set `env.DEEPSEEK_API_KEY` on the agent_servers entry |
|
|
207
|
-
| Cannot switch models
|
|
218
|
+
| Cannot switch models | The saved `reasoning_effort` default (or the session's current effort) is carried onto the new model; since 0.3.3 an unsupported effort resets to the model's default instead of failing the switch. Also check: a "phantom provider" route was picked — this bridge filters them by default (only `config.provider`'s models are advertised), so point the profile's provider at a real route |
|
|
219
|
+
| Context usage missing | A "phantom provider" route was picked; this bridge filters them by default (only `config.provider`'s models are advertised) — point the profile's provider at a real route |
|
|
208
220
|
| Need detailed diagnostics | `ACP_DEBUG=1 dsh --profile acp-enhanced` (stderr lifecycle trace) |
|
|
209
221
|
|
|
210
222
|
## Development
|
|
@@ -219,9 +231,17 @@ node scripts/acp-resume-test.mjs # session resume test
|
|
|
219
231
|
|
|
220
232
|
## Known limitations
|
|
221
233
|
|
|
222
|
-
Baseline prompts only (no image/audio attachments),
|
|
223
|
-
|
|
224
|
-
|
|
234
|
+
Baseline prompts only (no image/audio attachments), text streams at block granularity,
|
|
235
|
+
one in-flight prompt per session. MCP supports stdio and streamable HTTP (legacy SSE /
|
|
236
|
+
`acp` transports are not advertised).
|
|
225
237
|
`session/close` / `session/fork` / `session/resume` are not implemented (capabilities
|
|
226
|
-
undeclared, compliant clients will not call them); `session/delete` removes the
|
|
227
|
-
directory directly because dsh persistence has no official delete API.
|
|
238
|
+
undeclared, compliant clients will not call them); `session/delete` removes the
|
|
239
|
+
persisted directory directly because dsh persistence has no official delete API.
|
|
240
|
+
|
|
241
|
+
Multi-root workspaces are advertised and all roots are visible to the model, but dsh's
|
|
242
|
+
sandbox policy resolves **one writable root per session** (the primary `cwd`, i.e.
|
|
243
|
+
`session.header.cwd`) and the local sandboxes bind exactly that root for writes. Reads
|
|
244
|
+
work in every root; under `workspace-write` a write under an additional root is denied
|
|
245
|
+
first and needs escalation/approval, while `danger-full-access` writes everywhere.
|
|
246
|
+
True multi-root write enforcement belongs in dsh core (`dsh-sandbox-policy` /
|
|
247
|
+
`dsh-sandbox-local` would need a root list instead of a single root).
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
* - **Session modes**: permission presets are exposed as ACP modes
|
|
24
24
|
* (`session/set_mode`), so the client's mode UI drives the sandbox/approval
|
|
25
25
|
* preset of the session.
|
|
26
|
+
* - **Multi-root workspaces**: `sessionCapabilities.additionalDirectories` is
|
|
27
|
+
* advertised, so Zed passes every workspace root on the session lifecycle
|
|
28
|
+
* requests instead of warning "this agent doesn't currently support
|
|
29
|
+
* multi-root workspaces". All roots are described to the model in the
|
|
30
|
+
* system prompt; the sandbox still enforces one writable root (the primary
|
|
31
|
+
* `cwd`) — writes under an additional root go through escalation.
|
|
26
32
|
*
|
|
27
33
|
* Stdout is reserved for ACP JSON-RPC; diagnostics go to stderr only.
|
|
28
34
|
*
|
|
@@ -32,7 +38,7 @@
|
|
|
32
38
|
import { randomUUID } from 'node:crypto'
|
|
33
39
|
import { rm } from 'node:fs/promises'
|
|
34
40
|
import { createRequire } from 'node:module'
|
|
35
|
-
import { isAbsolute, dirname } from 'node:path'
|
|
41
|
+
import { isAbsolute, dirname, resolve } from 'node:path'
|
|
36
42
|
import { Readable, Writable } from 'node:stream'
|
|
37
43
|
import Schema from '@deepseek-ai/schemastery'
|
|
38
44
|
import { AgentSideConnection, ndJsonStream, PROTOCOL_VERSION, RequestError } from '@agentclientprotocol/sdk'
|
|
@@ -48,7 +54,7 @@ const AGENT_VERSION = createRequire(import.meta.url)('../package.json').version
|
|
|
48
54
|
|
|
49
55
|
export const name = 'acp-enhanced'
|
|
50
56
|
/** The bridge creates and owns agents; every other concern is carried by the composition. */
|
|
51
|
-
export const inject = ['agents', 'llm', 'approval', 'tools', 'commands']
|
|
57
|
+
export const inject = ['agents', 'llm', 'approval', 'tools', 'commands', 'systemPrompt']
|
|
52
58
|
|
|
53
59
|
export const Config = Schema.object({
|
|
54
60
|
/** Initial provider route for every created agent. */
|
|
@@ -77,12 +83,13 @@ function internalError(detail) {
|
|
|
77
83
|
/**
|
|
78
84
|
* Map a dsh tool name to the ACP ToolKind used for icons and card UX.
|
|
79
85
|
*
|
|
80
|
-
* Zed treats kind == 'execute' as a
|
|
81
|
-
* diff tool, and for both it HIDES
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* (
|
|
85
|
-
*
|
|
86
|
+
* Kind and content must agree: Zed treats kind == 'execute' as a terminal tool
|
|
87
|
+
* and kind == 'edit' as a diff tool, and for both it HIDES the rawInput
|
|
88
|
+
* section. `zed_terminal` is genuinely terminal; write/edit tools map to
|
|
89
|
+
* 'edit' only because the bridge always pairs them with a `diff` content
|
|
90
|
+
* block (see toolCallContentFor) — the diff replaces the raw dump as the card
|
|
91
|
+
* body. Local executors like bash/run_code stay 'other' and carry the command
|
|
92
|
+
* as a markdown code block, keeping the raw sections available too.
|
|
86
93
|
*/
|
|
87
94
|
function toolKindFor(name) {
|
|
88
95
|
if (name === 'zed_terminal') return 'execute'
|
|
@@ -90,6 +97,7 @@ function toolKindFor(name) {
|
|
|
90
97
|
if (/search|find|grep/.test(name)) return 'search'
|
|
91
98
|
if (/fetch|http/.test(name)) return 'fetch'
|
|
92
99
|
if (/think/.test(name)) return 'think'
|
|
100
|
+
if (/write|edit|patch|apply/.test(name)) return 'edit'
|
|
93
101
|
return 'other'
|
|
94
102
|
}
|
|
95
103
|
|
|
@@ -103,6 +111,231 @@ function parseToolArguments(raw) {
|
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
|
|
114
|
+
/** Collapse whitespace and bound a string to one display line (Zed shows the
|
|
115
|
+
* tool-call header from the title, truncating overflow with an ellipsis). */
|
|
116
|
+
function clipOneLine(text, max) {
|
|
117
|
+
const oneLine = String(text).replace(/\s+/g, ' ').trim()
|
|
118
|
+
return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Escape markdown-significant characters so a dynamic title fragment (path,
|
|
122
|
+
* pattern, URL, query) renders literally inside Zed's markdown label. Zed
|
|
123
|
+
* applies no escaping to `other`/`read`/`search`/`fetch`/`think` kinds — it
|
|
124
|
+
* only escapes `edit` itself — so the bridge must keep emphasis/code/link
|
|
125
|
+
* syntax from being interpreted. Execute-kind titles render as plain text
|
|
126
|
+
* and are never escaped. */
|
|
127
|
+
function mdEscape(text) {
|
|
128
|
+
return String(text).replace(/([\\*_`[\]<>])/g, '\\$1')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Executor-class tool names (command runners) shared by title/content/kind. */
|
|
132
|
+
const EXECUTOR_NAME = /bash|pwsh|powershell|shell|exec|run_code|execute|terminal/
|
|
133
|
+
|
|
134
|
+
/** Fenced-code language hint per executor tool (best effort — highlight only). */
|
|
135
|
+
function executorLang(name) {
|
|
136
|
+
if (/pwsh|powershell/.test(name)) return 'powershell'
|
|
137
|
+
if (/run_code/.test(name)) return 'typescript'
|
|
138
|
+
return 'bash'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Shared file-path argument lookup (key order = display preference). */
|
|
142
|
+
function pathArg(obj) {
|
|
143
|
+
for (const key of ['path', 'file_path', 'filePath', 'file', 'target']) {
|
|
144
|
+
if (typeof obj[key] === 'string' && obj[key].trim() !== '') return obj[key].trim()
|
|
145
|
+
}
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Cap a long text body for a card, marking the cut so nothing looks lost. */
|
|
150
|
+
function boundBody(text, max) {
|
|
151
|
+
const body = String(text ?? '')
|
|
152
|
+
return body.length > max ? `${body.slice(0, max)}\n… (truncated)` : body
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A markdown fenced code block, widening the fence when the text itself
|
|
156
|
+
* contains ``` runs so the body always renders literally. */
|
|
157
|
+
function codeFence(text, lang) {
|
|
158
|
+
const body = String(text ?? '')
|
|
159
|
+
const ticks = body.includes('```') ? '````' : '```'
|
|
160
|
+
return `${ticks}${lang}\n${boundBody(body, 12000)}\n${ticks}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* ACP content blocks giving the card a friendly body — the protocol's
|
|
165
|
+
* best-practice surface (clients render these INSTEAD of the raw JSON dump,
|
|
166
|
+
* which stays available as rawInput/rawOutput for transparency):
|
|
167
|
+
* - write/edit tools → a `diff` block rendered as a real diff card;
|
|
168
|
+
* - executors → the command as a syntax-highlighted code block.
|
|
169
|
+
*/
|
|
170
|
+
function toolCallContentFor(name, argumentsValue, kind) {
|
|
171
|
+
const obj = argumentsValue !== null && typeof argumentsValue === 'object' && !Array.isArray(argumentsValue)
|
|
172
|
+
? argumentsValue
|
|
173
|
+
: {}
|
|
174
|
+
if (kind === 'edit') {
|
|
175
|
+
const path = pathArg(obj)
|
|
176
|
+
if (path === undefined) return undefined
|
|
177
|
+
const newText = ['content', 'new_string', 'new_str', 'newText'].find((key) => typeof obj[key] === 'string')
|
|
178
|
+
if (newText === undefined) return undefined
|
|
179
|
+
const oldKey = ['old_string', 'old_str', 'oldText'].find((key) => typeof obj[key] === 'string')
|
|
180
|
+
return [{
|
|
181
|
+
type: 'diff',
|
|
182
|
+
path,
|
|
183
|
+
newText: boundBody(obj[newText], 8000),
|
|
184
|
+
...oldKey === undefined ? {} : { oldText: boundBody(obj[oldKey], 8000) },
|
|
185
|
+
}]
|
|
186
|
+
}
|
|
187
|
+
if (name !== 'zed_terminal' && EXECUTOR_NAME.test(name)) {
|
|
188
|
+
const command = typeof obj.command === 'string' ? obj.command
|
|
189
|
+
: typeof obj.cmd === 'string' ? obj.cmd
|
|
190
|
+
: typeof obj.script === 'string' ? obj.script
|
|
191
|
+
: Array.isArray(obj.args) && obj.args.every((part) => typeof part === 'string')
|
|
192
|
+
? obj.args.join(' ')
|
|
193
|
+
: undefined
|
|
194
|
+
if (typeof command === 'string' && command.trim() !== '') {
|
|
195
|
+
return [{ type: 'content', content: { type: 'text', text: codeFence(command.trim(), executorLang(name)) } }]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return undefined
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Files the call touches, as ACP `locations` — the "follow the agent"
|
|
203
|
+
* surface. Editors render them as clickable chips that open the file (and
|
|
204
|
+
* scroll to `line`), so a read or an edit is one click from its target.
|
|
205
|
+
*/
|
|
206
|
+
function toolCallLocationsFor(kind, argumentsValue) {
|
|
207
|
+
if (kind !== 'read' && kind !== 'edit' && kind !== 'delete' && kind !== 'move') return undefined
|
|
208
|
+
const obj = argumentsValue !== null && typeof argumentsValue === 'object' && !Array.isArray(argumentsValue)
|
|
209
|
+
? argumentsValue
|
|
210
|
+
: {}
|
|
211
|
+
const path = pathArg(obj)
|
|
212
|
+
if (path === undefined) return undefined
|
|
213
|
+
const line = [obj.line, obj.offset].find((value) => (
|
|
214
|
+
typeof value === 'number' && Number.isFinite(value) && value >= 1
|
|
215
|
+
))
|
|
216
|
+
return [{ path, ...line === undefined ? {} : { line } }]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* One-line human-readable summary of a tool call for the ACP `title` field.
|
|
221
|
+
* Zed renders this as the collapsed tool-call header, so a bare tool name
|
|
222
|
+
* ("read", "bash") hides what actually happened until the card is expanded.
|
|
223
|
+
* Mirror Zed's own native-agent titles (`Read <path>`, `Fetch <url>`,
|
|
224
|
+
* `Search: <pattern>`, and the raw command for terminal tools) with the
|
|
225
|
+
* detail escaped and bounded to one line.
|
|
226
|
+
*/
|
|
227
|
+
function toolCallTitle(name, argumentsValue) {
|
|
228
|
+
const obj = argumentsValue !== null && typeof argumentsValue === 'object' && !Array.isArray(argumentsValue)
|
|
229
|
+
? argumentsValue
|
|
230
|
+
: {}
|
|
231
|
+
const str = (key) => (typeof obj[key] === 'string' ? obj[key].trim() : undefined)
|
|
232
|
+
const num = (key) => {
|
|
233
|
+
const value = obj[key]
|
|
234
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
235
|
+
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
|
|
236
|
+
return Number(value)
|
|
237
|
+
}
|
|
238
|
+
return undefined
|
|
239
|
+
}
|
|
240
|
+
const stringField = (value, key) => (
|
|
241
|
+
typeof value === 'object' && value !== null && typeof value[key] === 'string' ? value[key].trim() : undefined
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// Execute-kind tools (plain-text label): show the command itself.
|
|
245
|
+
if (name === 'zed_terminal' || EXECUTOR_NAME.test(name)) {
|
|
246
|
+
// Only zed_terminal maps to kind 'execute' (Zed renders its label as plain
|
|
247
|
+
// text); local executors like bash/run_code are kind 'other' and render as
|
|
248
|
+
// markdown, so their command must be escaped to stay literal.
|
|
249
|
+
const literal = (text) => (name === 'zed_terminal' ? text : mdEscape(text))
|
|
250
|
+
// Codex-style card: the collapsed title is the model's own intent line
|
|
251
|
+
// (bash/pwsh mark `description` required — "Show working tree status"),
|
|
252
|
+
// and the exact command stays visible in rawInput when the card expands.
|
|
253
|
+
const description = str('description')
|
|
254
|
+
if (description) return clipOneLine(literal(description), 100)
|
|
255
|
+
const command = str('command') ?? str('cmd') ?? str('script')
|
|
256
|
+
if (command) return clipOneLine(literal(command), 100)
|
|
257
|
+
const argv = obj.args
|
|
258
|
+
if (Array.isArray(argv) && argv.length > 0) return clipOneLine(literal(argv.map(String).join(' ')), 100)
|
|
259
|
+
const cwd = str('cwd')
|
|
260
|
+
if (cwd) return clipOneLine(literal(`Run in ${cwd}`), 100)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// File-content tools: `Read <path>` (incl. line range when present).
|
|
264
|
+
if (/read|cat|show|view/.test(name)) {
|
|
265
|
+
const path = pathArg(obj)
|
|
266
|
+
if (path) {
|
|
267
|
+
const range = [num('line') ?? num('offset'), num('limit')].filter((value) => value !== undefined)
|
|
268
|
+
const suffix = range.length === 2 && Number.isFinite(range[0]) && Number.isFinite(range[1]) && range[1] > 0
|
|
269
|
+
? ` (lines ${range[0]}-${range[0] + range[1] - 1})`
|
|
270
|
+
: range.length === 1 && Number.isFinite(range[0]) && range[0] > 1
|
|
271
|
+
? ` (from line ${range[0]})`
|
|
272
|
+
: ''
|
|
273
|
+
return clipOneLine(`Read ${mdEscape(path)}${suffix}`, 120)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// File-modifying tools: `Write <path>` / `Edit <path>`.
|
|
278
|
+
if (/write|edit|patch|apply/.test(name)) {
|
|
279
|
+
const path = pathArg(obj)
|
|
280
|
+
if (path) return clipOneLine(`Write ${mdEscape(path)}`, 120)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Content search: `Search: <pattern>`.
|
|
284
|
+
if (/grep|rg|content_search|search_text/.test(name)) {
|
|
285
|
+
const pattern = str('pattern') ?? str('regex') ?? str('query')
|
|
286
|
+
if (pattern) return clipOneLine(`Search: ${mdEscape(pattern)}`, 120)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Path search: `Find: <pattern>`.
|
|
290
|
+
if (/glob|find/.test(name)) {
|
|
291
|
+
const pattern = str('pattern') ?? str('query') ?? str('path')
|
|
292
|
+
if (pattern) return clipOneLine(`Find: ${mdEscape(pattern)}`, 120)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Network fetch: `Fetch: <url>`.
|
|
296
|
+
if (/fetch|http/.test(name)) {
|
|
297
|
+
const url = str('url') ?? str('uri') ?? str('endpoint')
|
|
298
|
+
if (url) return clipOneLine(`Fetch: ${mdEscape(url)}`, 120)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Generic web/data search: `Search: <query>`.
|
|
302
|
+
if (/search/.test(name)) {
|
|
303
|
+
const query = str('query') ?? str('q') ?? str('keyword') ?? str('keywords')
|
|
304
|
+
if (query) return clipOneLine(`Search: ${mdEscape(query)}`, 120)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// User questions: `Ask: <first question>`.
|
|
308
|
+
if (/ask|question|elicit/.test(name) && Array.isArray(obj.questions)) {
|
|
309
|
+
const first = obj.questions[0]
|
|
310
|
+
const text = typeof first === 'object' && first !== null
|
|
311
|
+
? stringField(first, 'question') ?? stringField(first, 'header') ?? ''
|
|
312
|
+
: String(first ?? '')
|
|
313
|
+
if (text.trim().length > 0) return clipOneLine(`Ask: ${mdEscape(text)}`, 120)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Subagents / delegated work: show the objective.
|
|
317
|
+
if (/spawn_agent|subagent|spawn|delegate|task /.test(name)) {
|
|
318
|
+
const description = str('description') ?? str('objective') ?? str('prompt')
|
|
319
|
+
if (description) return clipOneLine(mdEscape(description), 100)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// MCP tools: inline a single string-valued field (mirrors Zed's own MCP
|
|
323
|
+
// primary-argument heuristic); otherwise fall back to the tool name.
|
|
324
|
+
if (name.startsWith('mcp__')) {
|
|
325
|
+
const strings = Object.entries(obj)
|
|
326
|
+
.filter(([key, value]) => typeof value === 'string' && value.trim().length > 0)
|
|
327
|
+
if (strings.length === 1) {
|
|
328
|
+
return clipOneLine(mdEscape(strings[0][1].trim()), 120)
|
|
329
|
+
}
|
|
330
|
+
if (strings.length > 1) {
|
|
331
|
+
const [key, value] = strings[0]
|
|
332
|
+
return clipOneLine(`${key}=${mdEscape(value)}`, 120)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return name
|
|
337
|
+
}
|
|
338
|
+
|
|
106
339
|
/** Extract a bounded text preview of a dsh tool result for ACP rawOutput. */
|
|
107
340
|
function resultPreview(event) {
|
|
108
341
|
if (event.data.error !== undefined) {
|
|
@@ -143,6 +376,24 @@ export function apply(ctx, config) {
|
|
|
143
376
|
/** Resolve the permission-presets service, tolerating a lazy mount. */
|
|
144
377
|
const permissionPresets = () => ctx.get('permissionPresets')
|
|
145
378
|
|
|
379
|
+
// Multi-root workspaces: describe the session's roots to the model. Zed
|
|
380
|
+
// passes the primary cwd plus every additional workspace root on the
|
|
381
|
+
// session lifecycle requests; the sandbox policy still resolves a single
|
|
382
|
+
// writable root (the primary cwd), so the text tells the model how writes
|
|
383
|
+
// behave under each policy. Reads are unrestricted by the file sandbox.
|
|
384
|
+
ctx.systemPrompt?.context({
|
|
385
|
+
name: 'acp:workspace-roots',
|
|
386
|
+
order: 115,
|
|
387
|
+
text: (context) => {
|
|
388
|
+
const session = context.agent?.session
|
|
389
|
+
if (session === undefined) return ''
|
|
390
|
+
return renderWorkspaceRoots(
|
|
391
|
+
session.header.cwd,
|
|
392
|
+
sessions.get(session.id)?.additionalDirectories ?? [],
|
|
393
|
+
)
|
|
394
|
+
},
|
|
395
|
+
})
|
|
396
|
+
|
|
146
397
|
/** Return the bridge-owned record for an agent, rejecting same-id impostors. */
|
|
147
398
|
const ownedRecord = (agent) => {
|
|
148
399
|
const record = sessions.get(agent.session.id)
|
|
@@ -177,6 +428,70 @@ export function apply(ctx, config) {
|
|
|
177
428
|
inflight.reject(internalError(`turn failed: ${reason.error.message}`))
|
|
178
429
|
}
|
|
179
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Wire update for a harness `tool/call`: the one-line title header plus the
|
|
433
|
+
* protocol's friendly-body surfaces — `content` (diff for file edits, the
|
|
434
|
+
* command as a code block for executors), `locations` (clickable file chips),
|
|
435
|
+
* and `rawInput`/`rawOutput`-class transparency fields.
|
|
436
|
+
*/
|
|
437
|
+
function toolCallUpdateFor(record, event) {
|
|
438
|
+
const parsedArgs = parseToolArguments(event.data.arguments)
|
|
439
|
+
const kind = toolKindFor(event.data.name)
|
|
440
|
+
const content = toolCallContentFor(event.data.name, parsedArgs, kind)
|
|
441
|
+
const locations = toolCallLocationsFor(kind, parsedArgs)
|
|
442
|
+
// Result-side card bodies need the call's own args (the executor output
|
|
443
|
+
// fence); remember them per callId with a small ceiling against
|
|
444
|
+
// aborted-call leaks.
|
|
445
|
+
if (record.callArgs.size >= 64) record.callArgs.delete(record.callArgs.keys().next().value)
|
|
446
|
+
record.callArgs.set(event.data.callId, { name: event.data.name, parsedArgs })
|
|
447
|
+
return {
|
|
448
|
+
sessionUpdate: 'tool_call',
|
|
449
|
+
toolCallId: event.data.callId,
|
|
450
|
+
name: event.data.name,
|
|
451
|
+
title: toolCallTitle(event.data.name, parsedArgs),
|
|
452
|
+
kind,
|
|
453
|
+
status: 'in_progress',
|
|
454
|
+
...content === undefined ? {} : { content },
|
|
455
|
+
...locations === undefined ? {} : { locations },
|
|
456
|
+
rawInput: parsedArgs,
|
|
457
|
+
_meta: {
|
|
458
|
+
turn: event.data.turn,
|
|
459
|
+
step: event.data.step,
|
|
460
|
+
name: event.data.name,
|
|
461
|
+
argumentsPreview: event.data.arguments.slice(0, 200),
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Wire update for a harness `tool/result`: terminal status plus, for
|
|
467
|
+
* executors, the output as a friendly code block under the command. */
|
|
468
|
+
function toolResultUpdateFor(record, event, callId, elapsed) {
|
|
469
|
+
const preview = resultPreview(event)
|
|
470
|
+
const call = record.callArgs.get(callId)
|
|
471
|
+
record.callArgs.delete(callId)
|
|
472
|
+
const isError = event.data.error !== undefined
|
|
473
|
+
let content
|
|
474
|
+
if (!isError && preview !== undefined && call !== undefined
|
|
475
|
+
&& call.name !== 'zed_terminal' && EXECUTOR_NAME.test(call.name)) {
|
|
476
|
+
content = [{ type: 'content', content: { type: 'text', text: codeFence(preview, executorLang(call.name)) } }]
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
sessionUpdate: 'tool_call_update',
|
|
480
|
+
toolCallId: callId,
|
|
481
|
+
...call === undefined ? {} : { name: call.name },
|
|
482
|
+
status: isError ? 'failed' : 'completed',
|
|
483
|
+
...content === undefined ? {} : { content },
|
|
484
|
+
...preview === undefined ? {} : { rawOutput: preview },
|
|
485
|
+
_meta: {
|
|
486
|
+
turn: event.data.turn,
|
|
487
|
+
step: event.data.step,
|
|
488
|
+
elapsedMs: elapsed,
|
|
489
|
+
count: record.toolStats.count,
|
|
490
|
+
totalMs: record.toolStats.totalMs,
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
180
495
|
// ── session/event → ACP notifications ─────────────────────────────────────
|
|
181
496
|
|
|
182
497
|
ctx.on('session/event', (session, event) => {
|
|
@@ -239,19 +554,7 @@ export function apply(ctx, config) {
|
|
|
239
554
|
record.toolStats.lastName = event.data.name
|
|
240
555
|
notify({
|
|
241
556
|
sessionId: session.header.id,
|
|
242
|
-
update:
|
|
243
|
-
sessionUpdate: 'tool_call',
|
|
244
|
-
toolCallId: event.data.callId,
|
|
245
|
-
title: event.data.name,
|
|
246
|
-
kind: toolKindFor(event.data.name),
|
|
247
|
-
rawInput: parseToolArguments(event.data.arguments),
|
|
248
|
-
_meta: {
|
|
249
|
-
turn: event.data.turn,
|
|
250
|
-
step: event.data.step,
|
|
251
|
-
name: event.data.name,
|
|
252
|
-
argumentsPreview: event.data.arguments.slice(0, 200),
|
|
253
|
-
},
|
|
254
|
-
},
|
|
557
|
+
update: toolCallUpdateFor(record, event),
|
|
255
558
|
})
|
|
256
559
|
break
|
|
257
560
|
}
|
|
@@ -264,22 +567,9 @@ export function apply(ctx, config) {
|
|
|
264
567
|
record.toolStats.lastCallAt = undefined
|
|
265
568
|
// The tool call id lives on the ToolResultBlock, not on the event root.
|
|
266
569
|
const callId = event.data.message?.content?.[0]?.toolCallId ?? event.data.callId
|
|
267
|
-
const preview = resultPreview(event)
|
|
268
570
|
notify({
|
|
269
571
|
sessionId: session.header.id,
|
|
270
|
-
update:
|
|
271
|
-
sessionUpdate: 'tool_call_update',
|
|
272
|
-
toolCallId: callId,
|
|
273
|
-
status: event.data.error === undefined ? 'completed' : 'error',
|
|
274
|
-
...preview === undefined ? {} : { rawOutput: preview },
|
|
275
|
-
_meta: {
|
|
276
|
-
turn: event.data.turn,
|
|
277
|
-
step: event.data.step,
|
|
278
|
-
elapsedMs: elapsed,
|
|
279
|
-
count: record.toolStats.count,
|
|
280
|
-
totalMs: record.toolStats.totalMs,
|
|
281
|
-
},
|
|
282
|
-
},
|
|
572
|
+
update: toolResultUpdateFor(record, event, callId, elapsed),
|
|
283
573
|
})
|
|
284
574
|
break
|
|
285
575
|
}
|
|
@@ -611,7 +901,22 @@ export function apply(ctx, config) {
|
|
|
611
901
|
|
|
612
902
|
/** Resolve and apply a new model/effort selection to the session. */
|
|
613
903
|
async function applySelection(record, next) {
|
|
614
|
-
|
|
904
|
+
let resolved
|
|
905
|
+
try {
|
|
906
|
+
resolved = await llm.resolveCallConfig(next)
|
|
907
|
+
} catch (error) {
|
|
908
|
+
// A reasoning effort can outlive the model that supported it: Zed
|
|
909
|
+
// re-applies its saved `default_config_options` on every session, and
|
|
910
|
+
// switching models carries the session's current effort onto the newly
|
|
911
|
+
// picked model, which may not offer it (glm-* and gpt-5.6-luna reject
|
|
912
|
+
// "max"). Rejecting the whole switch snapped Zed's dropdown back and
|
|
913
|
+
// made model switching itself appear broken, so drop the stale effort
|
|
914
|
+
// and resolve against the target model's own default instead. Any other
|
|
915
|
+
// failure (unknown route, missing credential) still surfaces.
|
|
916
|
+
if (error?.code !== 'UNSUPPORTED_REASONING_EFFORT' || next.reasoningEffort === undefined) throw error
|
|
917
|
+
logger.warn(`acp-enhanced: dropped reasoning effort "${next.reasoningEffort}" for ${next.provider}/${next.model} (unsupported); using the model default`)
|
|
918
|
+
resolved = await llm.resolveCallConfig({ ...next, reasoningEffort: undefined })
|
|
919
|
+
}
|
|
615
920
|
const selected = {
|
|
616
921
|
provider: resolved.provider,
|
|
617
922
|
model: resolved.model,
|
|
@@ -1142,6 +1447,8 @@ export function apply(ctx, config) {
|
|
|
1142
1447
|
return {
|
|
1143
1448
|
agent: handle.agent,
|
|
1144
1449
|
dispose: () => handle.dispose(),
|
|
1450
|
+
/** Additional workspace roots beyond the primary cwd (multi-root). */
|
|
1451
|
+
additionalDirectories: [],
|
|
1145
1452
|
inflight: undefined,
|
|
1146
1453
|
messageId: undefined,
|
|
1147
1454
|
stepStartedAt: undefined,
|
|
@@ -1150,6 +1457,8 @@ export function apply(ctx, config) {
|
|
|
1150
1457
|
title: undefined,
|
|
1151
1458
|
lastActivityAt: undefined,
|
|
1152
1459
|
toolStats: { count: 0, totalMs: 0, lastCallAt: undefined, lastName: undefined },
|
|
1460
|
+
/** callId → { name, parsedArgs } for result-time card bodies (bounded). */
|
|
1461
|
+
callArgs: new Map(),
|
|
1153
1462
|
buffer: {},
|
|
1154
1463
|
thoughtBuffer: {},
|
|
1155
1464
|
contextWindow: undefined,
|
|
@@ -1260,35 +1569,15 @@ export function apply(ctx, config) {
|
|
|
1260
1569
|
}
|
|
1261
1570
|
break
|
|
1262
1571
|
}
|
|
1263
|
-
case 'tool/call':
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
title: event.data.name,
|
|
1268
|
-
kind: toolKindFor(event.data.name),
|
|
1269
|
-
rawInput: parseToolArguments(event.data.arguments),
|
|
1270
|
-
_meta: {
|
|
1271
|
-
turn: event.data.turn,
|
|
1272
|
-
step: event.data.step,
|
|
1273
|
-
name: event.data.name,
|
|
1274
|
-
argumentsPreview: String(event.data.arguments ?? '').slice(0, 200),
|
|
1275
|
-
},
|
|
1276
|
-
})
|
|
1572
|
+
case 'tool/call': {
|
|
1573
|
+
// Same card shape as the live path — replayed threads render the
|
|
1574
|
+
// friendly body (diff / command block) and clickable locations too.
|
|
1575
|
+
await notifyNow(record, toolCallUpdateFor(record, event))
|
|
1277
1576
|
break
|
|
1577
|
+
}
|
|
1278
1578
|
case 'tool/result': {
|
|
1279
|
-
const preview = resultPreview(event)
|
|
1280
1579
|
const callId = event.data.message?.content?.[0]?.toolCallId ?? event.data.callId
|
|
1281
|
-
await notifyNow(record,
|
|
1282
|
-
sessionUpdate: 'tool_call_update',
|
|
1283
|
-
toolCallId: callId,
|
|
1284
|
-
status: event.data.error === undefined ? 'completed' : 'error',
|
|
1285
|
-
...preview === undefined ? {} : { rawOutput: preview },
|
|
1286
|
-
_meta: {
|
|
1287
|
-
turn: event.data.turn,
|
|
1288
|
-
step: event.data.step,
|
|
1289
|
-
name: event.data.name,
|
|
1290
|
-
},
|
|
1291
|
-
})
|
|
1580
|
+
await notifyNow(record, toolResultUpdateFor(record, event, callId, 0))
|
|
1292
1581
|
break
|
|
1293
1582
|
}
|
|
1294
1583
|
}
|
|
@@ -1321,7 +1610,10 @@ export function apply(ctx, config) {
|
|
|
1321
1610
|
agentInfo: { name: 'deepseek-harness-acp-enhanced', version: AGENT_VERSION },
|
|
1322
1611
|
agentCapabilities: {
|
|
1323
1612
|
loadSession: true,
|
|
1324
|
-
|
|
1613
|
+
// additionalDirectories: multi-root workspaces (Zed passes every
|
|
1614
|
+
// workspace root on session/new / session/load instead of showing
|
|
1615
|
+
// the "doesn't currently support multi-root workspaces" callout).
|
|
1616
|
+
sessionCapabilities: { list: {}, delete: {}, additionalDirectories: {} },
|
|
1325
1617
|
promptCapabilities: { image: false, audio: false, embeddedContext: false },
|
|
1326
1618
|
// Stdio MCP servers always work; streamable HTTP maps onto
|
|
1327
1619
|
// dsh-mcp-client's second transport. Legacy SSE does not.
|
|
@@ -1337,7 +1629,7 @@ export function apply(ctx, config) {
|
|
|
1337
1629
|
|
|
1338
1630
|
async newSession(params) {
|
|
1339
1631
|
assertOpen()
|
|
1340
|
-
|
|
1632
|
+
const additionalDirectories = normalizeSessionParams(params)
|
|
1341
1633
|
const sessionId = SessionId(randomUUID())
|
|
1342
1634
|
const handle = await agents.create({
|
|
1343
1635
|
sessionId,
|
|
@@ -1352,6 +1644,7 @@ export function apply(ctx, config) {
|
|
|
1352
1644
|
throw internalError('connection closed during session/new')
|
|
1353
1645
|
}
|
|
1354
1646
|
const record = makeRecord(handle)
|
|
1647
|
+
record.additionalDirectories = additionalDirectories
|
|
1355
1648
|
sessions.set(sessionId, record)
|
|
1356
1649
|
await syncMcpServers(params.mcpServers, params.cwd)
|
|
1357
1650
|
publishCommandsAfterResponse(record)
|
|
@@ -1377,15 +1670,14 @@ export function apply(ctx, config) {
|
|
|
1377
1670
|
|
|
1378
1671
|
async loadSession(params) {
|
|
1379
1672
|
assertOpen()
|
|
1380
|
-
|
|
1381
|
-
if (params.additionalDirectories !== undefined && params.additionalDirectories.length > 0) {
|
|
1382
|
-
throw invalidParams('additionalDirectories is not supported')
|
|
1383
|
-
}
|
|
1673
|
+
const additionalDirectories = normalizeSessionParams(params)
|
|
1384
1674
|
await syncMcpServers(params.mcpServers, params.cwd)
|
|
1385
1675
|
const sessionId = SessionId(params.sessionId)
|
|
1386
1676
|
const live = sessions.get(sessionId)
|
|
1387
1677
|
if (live !== undefined) {
|
|
1388
1678
|
// Already live on this connection: return its state without replay.
|
|
1679
|
+
// The client's root list is authoritative for the loaded workspace.
|
|
1680
|
+
live.additionalDirectories = additionalDirectories
|
|
1389
1681
|
const permission = permissionPresets()
|
|
1390
1682
|
return {
|
|
1391
1683
|
...permission === undefined ? {} : {
|
|
@@ -1412,6 +1704,7 @@ export function apply(ctx, config) {
|
|
|
1412
1704
|
throw internalError('connection closed during session/load')
|
|
1413
1705
|
}
|
|
1414
1706
|
const record = makeRecord(handle)
|
|
1707
|
+
record.additionalDirectories = additionalDirectories
|
|
1415
1708
|
sessions.set(sessionId, record)
|
|
1416
1709
|
// Zed inserts the thread before the load RPC completes; replay the
|
|
1417
1710
|
// conversation history as notifications so the thread renders.
|
|
@@ -1622,9 +1915,11 @@ export function apply(ctx, config) {
|
|
|
1622
1915
|
const live = sessions.get(header.id)
|
|
1623
1916
|
const title = live?.title ?? await readStoredTitle(persistence, header.id)
|
|
1624
1917
|
const updatedAtMs = live?.lastActivityAt ?? header.createdAt
|
|
1918
|
+
const liveDirs = live?.additionalDirectories ?? []
|
|
1625
1919
|
out.push({
|
|
1626
1920
|
sessionId: header.id,
|
|
1627
1921
|
cwd: header.cwd ?? cwd ?? process.cwd(),
|
|
1922
|
+
...liveDirs.length === 0 ? {} : { additionalDirectories: liveDirs },
|
|
1628
1923
|
...title === undefined ? {} : { title },
|
|
1629
1924
|
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
1630
1925
|
})
|
|
@@ -1727,10 +2022,46 @@ export function apply(ctx, config) {
|
|
|
1727
2022
|
ctx.effect(() => quiesce, 'acp-enhanced.connection')
|
|
1728
2023
|
}
|
|
1729
2024
|
|
|
1730
|
-
/**
|
|
1731
|
-
|
|
2025
|
+
/**
|
|
2026
|
+
* Validate `session/new`-style params and normalize the optional
|
|
2027
|
+
* `additionalDirectories` list. Entries must be absolute paths; they are
|
|
2028
|
+
* lexically resolved, deduplicated, and the primary cwd itself is dropped
|
|
2029
|
+
* (the schema treats the cwd as the first root — Zed never repeats it, but a
|
|
2030
|
+
* hand-rolled client may).
|
|
2031
|
+
*/
|
|
2032
|
+
function normalizeSessionParams(params) {
|
|
1732
2033
|
if (!isAbsolute(params.cwd)) throw invalidParams(`cwd must be an absolute path: ${params.cwd}`)
|
|
1733
|
-
|
|
1734
|
-
|
|
2034
|
+
const raw = params.additionalDirectories
|
|
2035
|
+
if (raw === undefined || raw === null) return []
|
|
2036
|
+
if (!Array.isArray(raw)) throw invalidParams('additionalDirectories must be an array of absolute paths')
|
|
2037
|
+
const primary = resolve(params.cwd)
|
|
2038
|
+
const seen = new Set()
|
|
2039
|
+
const dirs = []
|
|
2040
|
+
for (const entry of raw) {
|
|
2041
|
+
if (typeof entry !== 'string' || !isAbsolute(entry)) {
|
|
2042
|
+
throw invalidParams(`additionalDirectories entries must be absolute paths: ${String(entry)}`)
|
|
2043
|
+
}
|
|
2044
|
+
const dir = resolve(entry)
|
|
2045
|
+
if (dir === primary || seen.has(dir)) continue
|
|
2046
|
+
seen.add(dir)
|
|
2047
|
+
dirs.push(dir)
|
|
1735
2048
|
}
|
|
2049
|
+
return dirs
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/**
|
|
2053
|
+
* System-prompt context for a multi-root session (empty for single-root).
|
|
2054
|
+
* Reads are unrestricted by the file sandbox; writes follow the session
|
|
2055
|
+
* policy, whose single writable root is the primary cwd (dsh-sandbox-policy
|
|
2056
|
+
* resolves one `workspaceRoot` per session), so additional roots need
|
|
2057
|
+
* escalation under workspace-write and are unrestricted under
|
|
2058
|
+
* danger-full-access.
|
|
2059
|
+
*/
|
|
2060
|
+
function renderWorkspaceRoots(cwd, dirs) {
|
|
2061
|
+
if (dirs.length === 0 || cwd === undefined) return ''
|
|
2062
|
+
return [
|
|
2063
|
+
`Multi-root workspace: relative paths resolve against the primary root ${JSON.stringify(cwd)}. The session also spans these additional workspace roots:`,
|
|
2064
|
+
...dirs.map((dir) => `- ${dir}`),
|
|
2065
|
+
'File reads work in every root. File writes follow the sandbox policy: in workspace-write mode only the primary root (plus temp areas) is writable without approval, so a write under an additional root must obtain wider access first; in danger-full-access mode every root is writable.',
|
|
2066
|
+
].join('\n')
|
|
1736
2067
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-acp-enhanced",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Enhanced ACP server for DeepSeek Harness: block-level streaming, usage/stat telemetry (cache hit rate, token speed, input/output tokens, context length, turns, tool timing), model & reasoning-effort switching, and permission-preset control over the ACP wire (Zed-friendly)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|