dsh-acp-enhanced 0.5.2 → 0.6.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.
- package/README-zh.md +47 -1
- package/README.md +70 -1
- package/lib/index.js +152 -20
- package/lib/terminal-codec.js +6 -1
- package/package.json +1 -1
package/README-zh.md
CHANGED
|
@@ -51,7 +51,9 @@ ACP 线上。
|
|
|
51
51
|
状态机为进行中 → 完成/失败
|
|
52
52
|
- **Zed 文件与终端**:`zed_read_text_file` / `zed_write_text_file` / `zed_terminal` 把
|
|
53
53
|
文件编辑放进 Zed 的"编辑文件"区(diff + 接受/拒绝)、命令跑在 Zed 真实终端
|
|
54
|
-
- **原生表单提问**:`ask_user_question` → `elicitation/create`
|
|
54
|
+
- **原生表单提问**:`ask_user_question` → `elicitation/create` 表单,选项即点即答;
|
|
55
|
+
选项带描述展示,每个带选项的问题附一个"自定义答案"输入框——选项都不合适时可自由输入,
|
|
56
|
+
单选时自定义答案覆盖所选、多选时与所选并存(与 dsh 原生提问卡片语义一致)
|
|
55
57
|
- **Plan 面板**:plan mode 开关 → Zed 底部"规划中"状态条
|
|
56
58
|
|
|
57
59
|
### 会话
|
|
@@ -212,6 +214,50 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
212
214
|
> LLM provider id(即上面 `DSH_ACP_PROVIDER` 填的那个)。web 插件按 id 精确匹配,
|
|
213
215
|
> 填错时配置期不会报错,直到首次搜索才抛 `WEB_PROVIDER_CONFIGURED_MISSING`。
|
|
214
216
|
|
|
217
|
+
### 管理 profile 的插件
|
|
218
|
+
|
|
219
|
+
dsh-acp-enhanced 跑在**独立的 profile** 里——`acp-enhanced`(由上面的安装命令创建于
|
|
220
|
+
`~/.dsh/profiles/acp-enhanced/`),与 `dsh web` 背后的 `web` profile 完全隔离,
|
|
221
|
+
在这里增删改插件不会影响 web 侧的任何配置。
|
|
222
|
+
|
|
223
|
+
profile 的插件树由三层组合而成,后层修补前层:
|
|
224
|
+
|
|
225
|
+
1. **bundle 层**:profile `package.json` 的 `dsh.profile.bundles`——模板自带的
|
|
226
|
+
`@deepseek-ai/dsh-base` 在前,随后是每个声明了 `dsh.bundle` 的已安装包(如
|
|
227
|
+
`dsh-acp-enhanced`),按数组顺序排列。
|
|
228
|
+
2. **用户层**:`~/.dsh/profiles/acp-enhanced/cordis.patch.yml`——按 id 定位的行配置
|
|
229
|
+
覆写、`disabled: true` 行禁用,以及 `insert` 挂载(无 `dsh.bundle` 的包——如上面
|
|
230
|
+
的 `dsh-web-search-openrouter`——就靠它装配)。
|
|
231
|
+
3. **临时覆盖**:`dsh --profile acp-enhanced --patch extra.yml`。
|
|
232
|
+
|
|
233
|
+
调整插件集:
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
dsh plugin --profile acp-enhanced add <package> # 安装;声明 dsh.bundle 的包自动加入层栈
|
|
237
|
+
dsh plugin --profile acp-enhanced remove <package> # 卸载;自动退出层栈
|
|
238
|
+
dsh plugin --profile acp-enhanced update [package] # 更新一个/全部并 reconcile
|
|
239
|
+
dsh --profile acp-enhanced --dump-config # 查看组合后的完整树(标注每行来自哪一层)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`dsh plugin` 本质是在 profile 目录里转发 pnpm,并在每次运行后按安装状态 reconcile
|
|
243
|
+
`dsh.profile.bundles`。两个值得知道的推论:
|
|
244
|
+
|
|
245
|
+
- **靠从 `bundles` 里删条目来禁用 bundle 是禁不住的**——包仍是已安装依赖,下一次
|
|
246
|
+
`dsh plugin` 运行会原样加回来。想不禁载地禁用某一行,请在用户层按**行 id**(不是
|
|
247
|
+
包名,id 可在 `--dump-config` 输出里查)定位:
|
|
248
|
+
|
|
249
|
+
```yaml
|
|
250
|
+
- id: mnemon
|
|
251
|
+
disabled: true
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
- **无 `dsh.bundle` 的包自身不会装配**——它只作为普通依赖安装(带一次性警告),需要
|
|
255
|
+
像上面的 `web-search-openrouter` 行那样在用户层 `insert` 挂载;要改已有行的配置,
|
|
256
|
+
用 `- id: <行>` + `config:` 覆写——patch 条目是整行替换、不做合并。
|
|
257
|
+
|
|
258
|
+
改动在**下一个**进程生效:Zed 为每个 agent 线程拉起一个全新的
|
|
259
|
+
`dsh --profile acp-enhanced`,编辑 profile 后新开 agent 线程(或重启 Zed)即可。
|
|
260
|
+
|
|
215
261
|
## 故障排查
|
|
216
262
|
|
|
217
263
|
| 症状 | 处理 |
|
package/README.md
CHANGED
|
@@ -59,7 +59,10 @@ over the ACP wire.
|
|
|
59
59
|
put file edits into Zed's "edited files" area (diff + accept/reject) and commands into a
|
|
60
60
|
real Zed terminal
|
|
61
61
|
- **Native form questions**: `ask_user_question` → `elicitation/create` form, click an
|
|
62
|
-
option
|
|
62
|
+
option — or type a custom answer when none of them fit: options render with their
|
|
63
|
+
descriptions, each option-backed question gets a free-text "Custom answer" field, and a
|
|
64
|
+
custom answer replaces the single selection / accompanies a multi-select (same semantics
|
|
65
|
+
as dsh's native question card)
|
|
63
66
|
- **Plan panel**: plan mode toggle → "planning" status bar in Zed
|
|
64
67
|
|
|
65
68
|
### Sessions
|
|
@@ -151,6 +154,21 @@ Zed spawns agents with a minimal PATH, so use the shipped launcher
|
|
|
151
154
|
> store it in `~/.dsh/.credentials.yaml` (`DEEPSEEK_API_KEY`) and the dsh credentials
|
|
152
155
|
> service resolves it; the launcher also falls back to a running `dsh web` process's key.
|
|
153
156
|
|
|
157
|
+
Debugging a stalled turn (is it the model request or the tool?):
|
|
158
|
+
|
|
159
|
+
```jsonc
|
|
160
|
+
"env": {
|
|
161
|
+
// ...existing vars...
|
|
162
|
+
"ACP_LOG": "/Users/you/.dsh/dsh-acp-enhanced.trace.jsonl" // append-only JSONL event trace
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Each line is one session event with wall-clock `time` (ms epoch); a turn that appears to
|
|
167
|
+
hang is attributable afterwards: a **model request stall** shows a long gap between
|
|
168
|
+
`step/start` and the first `assistant/chunk`, while a **tool-execution stall** shows a
|
|
169
|
+
long gap between `tool/call` and `tool/result` (the result line carries `elapsedMs`).
|
|
170
|
+
`prompt/settled` lines cover the full user-message round trip (stopReason + elapsed).
|
|
171
|
+
|
|
154
172
|
Optional: pin the panel's default config options (all still changeable in the panel):
|
|
155
173
|
|
|
156
174
|
```jsonc
|
|
@@ -233,6 +251,57 @@ dsh plugin --profile acp-enhanced add dsh-web-search-openrouter
|
|
|
233
251
|
> exactly, so a wrong value produces no error at config time and only fails at the first
|
|
234
252
|
> search with `WEB_PROVIDER_CONFIGURED_MISSING`.
|
|
235
253
|
|
|
254
|
+
### Managing the profile's plugins
|
|
255
|
+
|
|
256
|
+
dsh-acp-enhanced runs in its **own profile** — `acp-enhanced`, created at
|
|
257
|
+
`~/.dsh/profiles/acp-enhanced/` by the install command above — fully separate from the
|
|
258
|
+
`web` profile behind `dsh web`, so plugin changes here never affect your web setup.
|
|
259
|
+
|
|
260
|
+
The profile composes its plugin tree from three sources, each layer patching the ones
|
|
261
|
+
before it:
|
|
262
|
+
|
|
263
|
+
1. **Bundle layers** — `dsh.profile.bundles` in the profile's `package.json`: the
|
|
264
|
+
template's `@deepseek-ai/dsh-base` first, then every installed package that declares
|
|
265
|
+
`dsh.bundle` (like `dsh-acp-enhanced`), in array order.
|
|
266
|
+
2. **Your user layer** — `~/.dsh/profiles/acp-enhanced/cordis.patch.yml`: id-targeted
|
|
267
|
+
row config overrides, `disabled: true` row disables, and `insert` lists (how a
|
|
268
|
+
package without `dsh.bundle` — like `dsh-web-search-openrouter` above — gets
|
|
269
|
+
mounted).
|
|
270
|
+
3. **Per-run overlays** — `dsh --profile acp-enhanced --patch extra.yml`.
|
|
271
|
+
|
|
272
|
+
Adjust the set with:
|
|
273
|
+
|
|
274
|
+
```sh
|
|
275
|
+
dsh plugin --profile acp-enhanced add <package> # install; a dsh.bundle package auto-joins the layer stack
|
|
276
|
+
dsh plugin --profile acp-enhanced remove <package> # uninstall; auto-leaves the stack
|
|
277
|
+
dsh plugin --profile acp-enhanced update [package] # update one/all, then reconcile
|
|
278
|
+
dsh --profile acp-enhanced --dump-config # inspect the composed tree (per-layer provenance)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`dsh plugin` is a thin pnpm forwarder (run inside the profile directory) that
|
|
282
|
+
reconciles `dsh.profile.bundles` against the installed state after every run. Two
|
|
283
|
+
consequences worth knowing:
|
|
284
|
+
|
|
285
|
+
- **Disabling a bundle by deleting it from `bundles` does not stick** — the package is
|
|
286
|
+
still an installed dependency, and the next `dsh plugin` run appends it right back.
|
|
287
|
+
To disable a single row without uninstalling, target it in the user layer by its
|
|
288
|
+
**row id** (not the package name — find ids in the `--dump-config` output):
|
|
289
|
+
|
|
290
|
+
```yaml
|
|
291
|
+
- id: mnemon
|
|
292
|
+
disabled: true
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
- **A package without `dsh.bundle` loads nothing by itself** — it installs as a plain
|
|
296
|
+
dependency (with a one-time warning) and needs your own `insert` entry in the user
|
|
297
|
+
layer, like the `web-search-openrouter` row above. To change an existing row's
|
|
298
|
+
config, override it with `- id: <row>` + `config:` — patch entries replace the
|
|
299
|
+
whole row config, they do not merge.
|
|
300
|
+
|
|
301
|
+
Changes take effect in the **next** process: Zed spawns a fresh
|
|
302
|
+
`dsh --profile acp-enhanced` for every agent thread, so open a new agent thread (or
|
|
303
|
+
restart Zed) after editing the profile.
|
|
304
|
+
|
|
236
305
|
## Troubleshooting
|
|
237
306
|
|
|
238
307
|
| Symptom | Fix |
|
package/lib/index.js
CHANGED
|
@@ -126,6 +126,13 @@ export const Config = Schema.object({
|
|
|
126
126
|
* default. The `DSH_ACP_PRESET` environment variable overrides this value
|
|
127
127
|
* when set. */
|
|
128
128
|
preset: Schema.string().default(undefined),
|
|
129
|
+
/** Optional JSONL trace of session events with wall-clock timestamps and
|
|
130
|
+
* tool call durations. After a stalled turn, the log discriminates a hung
|
|
131
|
+
* model request (long gap between `step/start` and the first
|
|
132
|
+
* `assistant/chunk`) from a hung tool call (long `tool/call` →
|
|
133
|
+
* `tool/result` gap). The `ACP_LOG` environment variable overrides this
|
|
134
|
+
* value; unset disables tracing. */
|
|
135
|
+
logFile: Schema.string().default(undefined),
|
|
129
136
|
})
|
|
130
137
|
|
|
131
138
|
/** Preserve invalid-parameter detail in the SDK wire error message. */
|
|
@@ -399,6 +406,17 @@ export function apply(ctx, config) {
|
|
|
399
406
|
const commands = ctx.commands
|
|
400
407
|
const skills = ctx.skills
|
|
401
408
|
const logger = ctx.logger
|
|
409
|
+
/** Optional JSONL trace target: config option, overridable by `ACP_LOG`
|
|
410
|
+
* (mirrors `ACP_DEBUG`, which remains stderr-only). Every event carries a
|
|
411
|
+
* wall-clock `time` so a stalled turn can be attributed to the model
|
|
412
|
+
* request vs the tool execution afterwards. */
|
|
413
|
+
const tracePath = process.env.ACP_LOG || config.logFile
|
|
414
|
+
const trace = (entry) => {
|
|
415
|
+
if (tracePath === undefined) return
|
|
416
|
+
writeFile(tracePath, `${JSON.stringify({ time: Date.now(), ...entry })}\n`, { flag: 'a' }).catch((error) => {
|
|
417
|
+
logger.warn(`acp-enhanced: trace write failed: ${String(error)}`)
|
|
418
|
+
})
|
|
419
|
+
}
|
|
402
420
|
/** The user-questions service (mounted by dsh-base); absent in minimal deployments. */
|
|
403
421
|
const userQuestions = ctx.get('userQuestions')
|
|
404
422
|
|
|
@@ -691,6 +709,17 @@ export function apply(ctx, config) {
|
|
|
691
709
|
const record = sessions.get(session.header.id)
|
|
692
710
|
if (record === undefined || record.agent.session !== session) return
|
|
693
711
|
try {
|
|
712
|
+
if (tracePath !== undefined) {
|
|
713
|
+
const extra = event.type === 'tool/call'
|
|
714
|
+
? { name: event.data.name }
|
|
715
|
+
: event.type === 'tool/result'
|
|
716
|
+
? { name: event.data.name ?? event.data.message?.content?.[0]?.name,
|
|
717
|
+
elapsedMs: record.toolStats.lastCallAt === undefined ? undefined : Date.now() - record.toolStats.lastCallAt }
|
|
718
|
+
: event.type === 'turn/end'
|
|
719
|
+
? { elapsedMs: record.turnStartedAt === undefined ? undefined : Date.now() - record.turnStartedAt }
|
|
720
|
+
: {}
|
|
721
|
+
trace({ event: event.type, turn: event.data?.turn, step: event.data?.step, ...extra })
|
|
722
|
+
}
|
|
694
723
|
if (process.env.ACP_DEBUG) {
|
|
695
724
|
const extra = event.type === 'turn/end' ? ` reason=${JSON.stringify(event.data.reason)}` : event.type === 'assistant/chunk' ? ` chunkType=${event.data.chunk.type}` : event.type === 'agent/inbox/spliced' ? ` hasPending=${record.agent.inbox?.hasPending}` : event.type === 'tool/call' ? ` name=${event.data.name}` : ''
|
|
696
725
|
process.stderr.write(`[acp-debug] ${event.type} turn=${event.data?.turn} step=${event.data?.step}${extra}\n`)
|
|
@@ -1038,6 +1067,13 @@ export function apply(ctx, config) {
|
|
|
1038
1067
|
|
|
1039
1068
|
/** Build the full config-option set for one session. */
|
|
1040
1069
|
async function buildConfigOptions(record) {
|
|
1070
|
+
// Test hook (unset in production): artificial latency proving the
|
|
1071
|
+
// response-vs-broadcast ordering holds even when config-option assembly
|
|
1072
|
+
// spans real event-loop turns (the cold-runner condition that raced the
|
|
1073
|
+
// command broadcast past the session/new response on CI).
|
|
1074
|
+
if (process.env.DSH_TEST_SLOW_CATALOG_MS !== undefined) {
|
|
1075
|
+
await new Promise((resolve) => setTimeout(resolve, Number(process.env.DSH_TEST_SLOW_CATALOG_MS) || 0))
|
|
1076
|
+
}
|
|
1041
1077
|
const selected = record.selection.current
|
|
1042
1078
|
const options = []
|
|
1043
1079
|
const groups = await modelCatalog()
|
|
@@ -1451,21 +1487,48 @@ export function apply(ctx, config) {
|
|
|
1451
1487
|
if (record === undefined) {
|
|
1452
1488
|
throw new Error('ask_user_question is only usable inside a bridge-owned session')
|
|
1453
1489
|
}
|
|
1490
|
+
// Editor forms cannot mix a fixed option list with free text in one
|
|
1491
|
+
// field, so every option-backed question gains a companion
|
|
1492
|
+
// `<id>__custom` text field: the form equivalent of dsh's native
|
|
1493
|
+
// "type your own answer" row, for when none of the offered options
|
|
1494
|
+
// fit. Option-free questions are already free-text fields.
|
|
1495
|
+
const usedKeys = new Set(request.questions.map((question) => question.id))
|
|
1496
|
+
const customKeys = new Map()
|
|
1454
1497
|
const properties = {}
|
|
1455
1498
|
const required = []
|
|
1456
1499
|
for (const question of request.questions) {
|
|
1457
1500
|
required.push(question.id)
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1501
|
+
const options = question.options ?? []
|
|
1502
|
+
// Titled options (const/title/description) instead of a bare enum
|
|
1503
|
+
// so Zed renders each option's description under its label, like
|
|
1504
|
+
// the native question card. A question without options becomes a
|
|
1505
|
+
// plain text field: an optionless multi-select would otherwise
|
|
1506
|
+
// render as an empty, unanswerable checkbox list.
|
|
1507
|
+
const titled = options.map((option) => ({
|
|
1508
|
+
const: option.label,
|
|
1509
|
+
title: option.label,
|
|
1510
|
+
...option.description === undefined ? {} : { description: option.description },
|
|
1511
|
+
}))
|
|
1512
|
+
const heading = question.header === undefined ? {} : { title: question.header }
|
|
1513
|
+
if (options.length === 0) {
|
|
1514
|
+
properties[question.id] = { type: 'string', description: question.question, ...heading }
|
|
1515
|
+
} else if (question.multiSelect === true) {
|
|
1516
|
+
properties[question.id] = { type: 'array', items: { anyOf: titled }, description: question.question, ...heading }
|
|
1467
1517
|
} else {
|
|
1468
|
-
properties[question.id] = { type: 'string', description: question.question }
|
|
1518
|
+
properties[question.id] = { type: 'string', oneOf: titled, description: question.question, ...heading }
|
|
1519
|
+
}
|
|
1520
|
+
if (options.length > 0) {
|
|
1521
|
+
let customKey = `${question.id}__custom`
|
|
1522
|
+
while (usedKeys.has(customKey)) customKey = `${customKey}_`
|
|
1523
|
+
usedKeys.add(customKey)
|
|
1524
|
+
customKeys.set(question.id, customKey)
|
|
1525
|
+
properties[customKey] = {
|
|
1526
|
+
type: 'string',
|
|
1527
|
+
title: 'Custom answer',
|
|
1528
|
+
description: question.multiSelect === true
|
|
1529
|
+
? 'None of the options fit? Type your own answer; it is returned alongside your selections.'
|
|
1530
|
+
: 'None of the options fit? Type your own answer; it replaces the selection.',
|
|
1531
|
+
}
|
|
1469
1532
|
}
|
|
1470
1533
|
}
|
|
1471
1534
|
const response = await conn.unstable_createElicitation({
|
|
@@ -1481,10 +1544,28 @@ export function apply(ctx, config) {
|
|
|
1481
1544
|
return {
|
|
1482
1545
|
answers: request.questions.map((question) => {
|
|
1483
1546
|
const value = content[question.id]
|
|
1547
|
+
const customKey = customKeys.get(question.id)
|
|
1548
|
+
if (customKey === undefined) {
|
|
1549
|
+
// Option-free question: the typed text IS the answer, reported
|
|
1550
|
+
// as the custom ("other") answer the way the native UI does.
|
|
1551
|
+
const text = typeof value === 'string' ? value.trim() : ''
|
|
1552
|
+
return { id: question.id, selected: [], ...text === '' ? {} : { custom: text } }
|
|
1553
|
+
}
|
|
1484
1554
|
const selected = Array.isArray(value)
|
|
1485
1555
|
? value.filter((entry) => typeof entry === 'string')
|
|
1486
1556
|
: typeof value === 'string' ? [value] : []
|
|
1487
|
-
|
|
1557
|
+
const customRaw = content[customKey]
|
|
1558
|
+
const custom = typeof customRaw === 'string' && customRaw.trim() !== '' ? customRaw.trim() : undefined
|
|
1559
|
+
if (custom === undefined) {
|
|
1560
|
+
return { id: question.id, selected }
|
|
1561
|
+
}
|
|
1562
|
+
// A custom answer replaces a single selection and accompanies a
|
|
1563
|
+
// multi-select, matching the native question card.
|
|
1564
|
+
return {
|
|
1565
|
+
id: question.id,
|
|
1566
|
+
selected: question.multiSelect === true ? selected : [],
|
|
1567
|
+
custom,
|
|
1568
|
+
}
|
|
1488
1569
|
}),
|
|
1489
1570
|
}
|
|
1490
1571
|
},
|
|
@@ -1647,6 +1728,16 @@ export function apply(ctx, config) {
|
|
|
1647
1728
|
* cannot route session notifications for it until the response arrives; an
|
|
1648
1729
|
* `available_commands_update` queued before that response is dropped and
|
|
1649
1730
|
* the slash menu stays empty ("Available commands: none").
|
|
1731
|
+
*
|
|
1732
|
+
* Ordering contract: arm this only as the handler's LAST statement, after
|
|
1733
|
+
* every await. The SDK serializes all outgoing messages through one FIFO
|
|
1734
|
+
* write queue and enqueues the handler's response synchronously in the
|
|
1735
|
+
* microtask continuation of the handler's promise, which strictly
|
|
1736
|
+
* precedes this setImmediate (check phase). Arming before a remaining
|
|
1737
|
+
* macrotask await (e.g. the config-option catalog build) lets the
|
|
1738
|
+
* immediate fire first, the broadcast overtakes the response, and
|
|
1739
|
+
* clients drop it — observed on CI as the broadcast landing before the
|
|
1740
|
+
* session/new response.
|
|
1650
1741
|
*/
|
|
1651
1742
|
function publishCommandsAfterResponse(record) {
|
|
1652
1743
|
setImmediate(() => {
|
|
@@ -1955,7 +2046,7 @@ export function apply(ctx, config) {
|
|
|
1955
2046
|
// additionalDirectories: multi-root workspaces (Zed passes every
|
|
1956
2047
|
// workspace root on session/new / session/load instead of showing
|
|
1957
2048
|
// the "doesn't currently support multi-root workspaces" callout).
|
|
1958
|
-
sessionCapabilities: { list: {}, delete: {}, additionalDirectories: {} },
|
|
2049
|
+
sessionCapabilities: { list: {}, delete: {}, additionalDirectories: {}, close: {} },
|
|
1959
2050
|
// Image support is a live capability: the harness advertises
|
|
1960
2051
|
// `image: true` only when the composition mounted a working
|
|
1961
2052
|
// attachment store (duck-typed, so dsh 0.1.1-rc.2+ with
|
|
@@ -2014,8 +2105,14 @@ export function apply(ctx, config) {
|
|
|
2014
2105
|
record.additionalDirectories = additionalDirectories
|
|
2015
2106
|
sessions.set(sessionId, record)
|
|
2016
2107
|
await syncMcpServers(params.mcpServers, params.cwd)
|
|
2017
|
-
publishCommandsAfterResponse(record)
|
|
2018
2108
|
const permission = permissionPresets()
|
|
2109
|
+
const configOptions = await buildConfigOptions(record)
|
|
2110
|
+
// Arm strictly after the handler's last await (see
|
|
2111
|
+
// publishCommandsAfterResponse): the SDK enqueues the response into
|
|
2112
|
+
// its FIFO write queue in the microtask continuation of this
|
|
2113
|
+
// handler's promise, which always precedes the immediate — so the
|
|
2114
|
+
// broadcast can never overtake the response.
|
|
2115
|
+
publishCommandsAfterResponse(record)
|
|
2019
2116
|
return {
|
|
2020
2117
|
sessionId,
|
|
2021
2118
|
...permission === undefined ? {} : {
|
|
@@ -2031,7 +2128,7 @@ export function apply(ctx, config) {
|
|
|
2031
2128
|
}),
|
|
2032
2129
|
},
|
|
2033
2130
|
},
|
|
2034
|
-
configOptions
|
|
2131
|
+
configOptions,
|
|
2035
2132
|
}
|
|
2036
2133
|
},
|
|
2037
2134
|
|
|
@@ -2042,9 +2139,15 @@ export function apply(ctx, config) {
|
|
|
2042
2139
|
const sessionId = SessionId(params.sessionId)
|
|
2043
2140
|
const live = sessions.get(sessionId)
|
|
2044
2141
|
if (live !== undefined) {
|
|
2045
|
-
// Already live on this connection
|
|
2046
|
-
//
|
|
2142
|
+
// Already live on this connection. The client is re-loading the
|
|
2143
|
+
// session because it dropped its local thread (that is the only
|
|
2144
|
+
// reason a load arrives for a session the client already knows), so
|
|
2145
|
+
// replay the history — without it the client renders a blank thread
|
|
2146
|
+
// and Zed misreads the empty thread as a draft, permanently losing
|
|
2147
|
+
// the session linkage in its sidebar. The client's root list is
|
|
2148
|
+
// authoritative for the loaded workspace.
|
|
2047
2149
|
live.additionalDirectories = additionalDirectories
|
|
2150
|
+
await replayHistory(live)
|
|
2048
2151
|
const permission = permissionPresets()
|
|
2049
2152
|
return {
|
|
2050
2153
|
...permission === undefined ? {} : {
|
|
@@ -2091,8 +2194,12 @@ export function apply(ctx, config) {
|
|
|
2091
2194
|
// Zed inserts the thread before the load RPC completes; replay the
|
|
2092
2195
|
// conversation history as notifications so the thread renders.
|
|
2093
2196
|
await replayHistory(record)
|
|
2094
|
-
publishCommandsAfterResponse(record)
|
|
2095
2197
|
const permission = permissionPresets()
|
|
2198
|
+
const configOptions = await buildConfigOptions(record)
|
|
2199
|
+
// Arm strictly after the handler's last await (see
|
|
2200
|
+
// publishCommandsAfterResponse): the broadcast must land after the
|
|
2201
|
+
// load response, never before it.
|
|
2202
|
+
publishCommandsAfterResponse(record)
|
|
2096
2203
|
return {
|
|
2097
2204
|
...permission === undefined ? {} : {
|
|
2098
2205
|
modes: {
|
|
@@ -2107,7 +2214,7 @@ export function apply(ctx, config) {
|
|
|
2107
2214
|
}),
|
|
2108
2215
|
},
|
|
2109
2216
|
},
|
|
2110
|
-
configOptions
|
|
2217
|
+
configOptions,
|
|
2111
2218
|
}
|
|
2112
2219
|
},
|
|
2113
2220
|
|
|
@@ -2216,6 +2323,7 @@ export function apply(ctx, config) {
|
|
|
2216
2323
|
}
|
|
2217
2324
|
const message = createUserMessage({ content: blocks, source: { kind: 'user' } })
|
|
2218
2325
|
if (process.env.ACP_DEBUG) process.stderr.write(`[acp-debug] followup queued, agent phase=${record.agent.phase?.kind} inboxPending=${record.agent.inbox?.hasPending}\n`)
|
|
2326
|
+
const promptAt = Date.now()
|
|
2219
2327
|
const stopReason = await new Promise((resolve, reject) => {
|
|
2220
2328
|
const inflight = {
|
|
2221
2329
|
resolve,
|
|
@@ -2243,13 +2351,19 @@ export function apply(ctx, config) {
|
|
|
2243
2351
|
}
|
|
2244
2352
|
})
|
|
2245
2353
|
})
|
|
2354
|
+
trace({ event: 'prompt/settled', stopReason, elapsedMs: Date.now() - promptAt })
|
|
2246
2355
|
return { stopReason }
|
|
2247
2356
|
},
|
|
2248
2357
|
|
|
2249
2358
|
cancel(params) {
|
|
2250
2359
|
const record = sessions.get(SessionId(params.sessionId))
|
|
2251
2360
|
if (record === undefined) return Promise.resolve()
|
|
2252
|
-
|
|
2361
|
+
// An Error reason, not a bare object: the harness surfaces an aborted
|
|
2362
|
+
// tool's signal reason verbatim into the tool result the model reads,
|
|
2363
|
+
// so a plain object would render as the useless "Error: [object
|
|
2364
|
+
// Object]" and push the model off the tool that was merely
|
|
2365
|
+
// interrupted by the user's stop.
|
|
2366
|
+
record.agent.cancel(new Error('cancelled by user'))
|
|
2253
2367
|
settlePrompt(record, 'cancelled')
|
|
2254
2368
|
return Promise.resolve()
|
|
2255
2369
|
},
|
|
@@ -2383,6 +2497,24 @@ export function apply(ctx, config) {
|
|
|
2383
2497
|
return { sessions: out }
|
|
2384
2498
|
},
|
|
2385
2499
|
|
|
2500
|
+
async closeSession(params) {
|
|
2501
|
+
assertOpen()
|
|
2502
|
+
const sessionId = SessionId(params.sessionId)
|
|
2503
|
+
const live = sessions.get(sessionId)
|
|
2504
|
+
if (live === undefined) return {}
|
|
2505
|
+
sessions.delete(sessionId)
|
|
2506
|
+
// The client released its last handle on this session (its thread was
|
|
2507
|
+
// dropped). Dispose the in-memory record so a later session/load fully
|
|
2508
|
+
// resumes from the persisted session; keeping it "live" would serve an
|
|
2509
|
+
// empty thread with no history replay on the next load.
|
|
2510
|
+
try {
|
|
2511
|
+
await live.dispose()
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
logger.warn(`acp-enhanced: failed to dispose closed session ${sessionId}: ${String(error)}`)
|
|
2514
|
+
}
|
|
2515
|
+
return {}
|
|
2516
|
+
},
|
|
2517
|
+
|
|
2386
2518
|
async deleteSession(params) {
|
|
2387
2519
|
assertOpen()
|
|
2388
2520
|
const sessionId = SessionId(params.sessionId)
|
|
@@ -2432,7 +2564,7 @@ export function apply(ctx, config) {
|
|
|
2432
2564
|
const records = [...sessions.values()]
|
|
2433
2565
|
sessions.clear()
|
|
2434
2566
|
for (const record of records) {
|
|
2435
|
-
record.agent.cancel(
|
|
2567
|
+
record.agent.cancel(new Error('cancelled because the session closed'))
|
|
2436
2568
|
settlePrompt(record, 'cancelled')
|
|
2437
2569
|
}
|
|
2438
2570
|
for (const { fiber } of mcpMounts.values()) {
|
package/lib/terminal-codec.js
CHANGED
|
@@ -23,10 +23,15 @@ import { isAbsolute, resolve } from 'node:path'
|
|
|
23
23
|
* diff replaces the raw dump as the card body. Remaining local executors like
|
|
24
24
|
* run_code stay 'other' and carry the command as a markdown code block,
|
|
25
25
|
* keeping the raw sections available too.
|
|
26
|
+
*
|
|
27
|
+
* Read tools map to 'read' so the bridge can pair the kind with ACP
|
|
28
|
+
* `locations` (the clickable file chips via `toolCallLocationsFor`): the bare
|
|
29
|
+
* dsh tool name is exactly `read`, so it must match by itself — `read_text`
|
|
30
|
+
* and `fs_*read` only cover the long-form names.
|
|
26
31
|
*/
|
|
27
32
|
export function toolKindFor(name) {
|
|
28
33
|
if (name === 'bash' || name === 'pwsh' || name === 'zed_terminal') return 'execute'
|
|
29
|
-
if (/^fs_.*read
|
|
34
|
+
if (/^fs_.*read|^read$|read_text|cat|show/.test(name)) return 'read'
|
|
30
35
|
if (/search|find|grep/.test(name)) return 'search'
|
|
31
36
|
if (/fetch|http/.test(name)) return 'fetch'
|
|
32
37
|
if (/think/.test(name)) return 'think'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-acp-enhanced",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|