dsh-lsp-actions 0.2.0 → 0.3.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/CHANGELOG.md +51 -0
- package/README.es.md +11 -0
- package/README.hi.md +11 -0
- package/README.md +108 -6
- package/README.pt.md +11 -0
- package/README.zh-CN.md +110 -6
- package/cordis.patch.yml +6 -0
- package/docs/editor-protocol.md +159 -0
- package/docs/editor-protocol.zh-CN.md +159 -0
- package/docs/seam-extension-notes.md +76 -0
- package/examples/vscode/README.md +72 -0
- package/examples/vscode/README.zh-CN.md +72 -0
- package/examples/vscode/backend/bin.mjs +44 -0
- package/examples/vscode/backend/cordis.yml +53 -0
- package/examples/vscode/backend/package.json +22 -0
- package/examples/vscode/extension/package.json +96 -0
- package/examples/vscode/extension/src/client.js +100 -0
- package/examples/vscode/extension/src/extension.js +187 -0
- package/examples/vscode/extension/src/views.js +127 -0
- package/lib/editor/actions.d.ts +44 -0
- package/lib/editor/actions.d.ts.map +1 -0
- package/lib/editor/actions.js +370 -0
- package/lib/editor/actions.js.map +1 -0
- package/lib/editor/cache.d.ts +51 -0
- package/lib/editor/cache.d.ts.map +1 -0
- package/lib/editor/cache.js +75 -0
- package/lib/editor/cache.js.map +1 -0
- package/lib/editor/server.d.ts +61 -0
- package/lib/editor/server.d.ts.map +1 -0
- package/lib/editor/server.js +97 -0
- package/lib/editor/server.js.map +1 -0
- package/lib/editor/service.d.ts +63 -0
- package/lib/editor/service.d.ts.map +1 -0
- package/lib/editor/service.js +181 -0
- package/lib/editor/service.js.map +1 -0
- package/lib/editor/transport.d.ts +53 -0
- package/lib/editor/transport.d.ts.map +1 -0
- package/lib/editor/transport.js +143 -0
- package/lib/editor/transport.js.map +1 -0
- package/lib/editor/types.d.ts +185 -0
- package/lib/editor/types.d.ts.map +1 -0
- package/lib/editor/types.js +13 -0
- package/lib/editor/types.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +26 -1
- package/lib/index.js.map +1 -1
- package/lib/servers.d.ts +20 -1
- package/lib/servers.d.ts.map +1 -1
- package/lib/servers.js +6 -0
- package/lib/servers.js.map +1 -1
- package/lib/tools.d.ts +13 -0
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +2 -2
- package/lib/tools.js.map +1 -1
- package/lib/vocabulary.d.ts +3 -1
- package/lib/vocabulary.d.ts.map +1 -1
- package/lib/vocabulary.js.map +1 -1
- package/package.json +51 -3
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Editor Action Protocol v1
|
|
2
|
+
|
|
3
|
+
English | [简体中文](editor-protocol.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
The **editor action protocol** is the stable, editor-facing surface of `dsh-lsp-actions`: any editor (VS Code first) consumes the plugin's LSP capabilities over **newline-delimited JSON-RPC 2.0** without driving an agent and without implementing LSP itself. The wire framing is identical to the official DeepSeek Harness SDK/ACP transports (one JSON object per line); only the method vocabulary differs.
|
|
6
|
+
|
|
7
|
+
| Method | Direction | Purpose |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `lsp.actions.list` | client → server | The protocol version, the action catalog, and the addressable DSH sessions. |
|
|
10
|
+
| `lsp.actions.run` | client → server | Execute one action (`diagnostics.get`, `quickfix.apply`, `format`, `completion.get`) with a structured result envelope. |
|
|
11
|
+
| `lsp.events` | client → server (notification) | `{subscribe: true/false}` turns the event stream on/off. |
|
|
12
|
+
| `lsp.event` | server → client (notification) | Streaming events: diagnostics updates, action status, file changes, session changes. |
|
|
13
|
+
|
|
14
|
+
Transport: one UTF-8 JSON object per line (`\n`-terminated). Requests carry `id` + `method` + `params`; responses carry `id` + `result` (or `error`); notifications carry only `method` + `params`. Malformed lines are ignored. Unknown methods answer `-32601`; handler failures answer `-32603`.
|
|
15
|
+
|
|
16
|
+
## Versioning and the backward-compatibility promise
|
|
17
|
+
|
|
18
|
+
- The protocol is versioned: `lsp.actions.list` returns `protocol: "lsp-actions/v1"` and `version: 1`. `lsp.actions.run` may carry `protocol`; a server that does not support it answers a failed envelope with `LSP_PROTOCOL_VERSION_UNSUPPORTED`.
|
|
19
|
+
- **v1 is frozen.** Every v1 field name, action id, event kind, and error code stays stable. Evolution is additive only:
|
|
20
|
+
- new actions, new fields, and new event kinds may be added without a version bump;
|
|
21
|
+
- existing field semantics never change in place;
|
|
22
|
+
- a breaking change is published under a new `protocol` version, and servers may serve several versions side by side.
|
|
23
|
+
- **Clients must** ignore unknown fields, unknown event kinds, and unknown actions advertised in `lsp.actions.list`; route on the stable error `code`, never on message text.
|
|
24
|
+
|
|
25
|
+
## `lsp.actions.list`
|
|
26
|
+
|
|
27
|
+
Params: `{}`.
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"protocol": "lsp-actions/v1",
|
|
32
|
+
"version": 1,
|
|
33
|
+
"actions": [
|
|
34
|
+
{ "action": "diagnostics.get", "writes": false, "description": "…" },
|
|
35
|
+
{ "action": "completion.get", "writes": false, "description": "…" },
|
|
36
|
+
{ "action": "quickfix.apply", "writes": true, "description": "…" },
|
|
37
|
+
{ "action": "format", "writes": true, "description": "…" }
|
|
38
|
+
],
|
|
39
|
+
"sessions": [
|
|
40
|
+
{ "sessionId": "…", "cwd": "/path/to/workspace", "live": true }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`actions` is the closed v1 catalog. `sessions` lists the live DSH sessions of the backend runtime; a run may pass `sessionId` to bind its permission preset and approval to that session.
|
|
46
|
+
|
|
47
|
+
## `lsp.actions.run`
|
|
48
|
+
|
|
49
|
+
Params:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
{
|
|
53
|
+
action: string // an id from lsp.actions.list
|
|
54
|
+
params?: object // per-action parameters (below)
|
|
55
|
+
sessionId?: string // bind permission preset + approval to this DSH session
|
|
56
|
+
requestId?: string // echoed on the result and every action.status event
|
|
57
|
+
protocol?: string // optional; "lsp-actions/v1"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The result is **always** the same envelope — success and failure are structured, machine-routable:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{ "requestId": "q1", "action": "quickfix.apply", "status": "succeeded", "result": { … } }
|
|
65
|
+
{ "requestId": "q1", "action": "quickfix.apply", "status": "failed",
|
|
66
|
+
"error": { "code": "LSP_ACTION_READ_ONLY", "message": "…" } }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Positions and ranges on this wire use the **LSP convention**: zero-based, half-open UTF-16.
|
|
70
|
+
|
|
71
|
+
### `diagnostics.get` (read-only)
|
|
72
|
+
|
|
73
|
+
Params: `{ filePath, workspaceRoot, includeSource? }`.
|
|
74
|
+
|
|
75
|
+
Result: `{ kind: "diagnostics", filePath, diagnostics: [{ severity, range, message, source?, code? }], truncated, total, source? }`. The snapshot is cached in the backend's bounded LRU diagnostics cache (config `editor.diagnosticsCacheMaxFiles`) and announced to every subscriber as a `diagnostics.updated` event.
|
|
76
|
+
|
|
77
|
+
### `completion.get` (read-only)
|
|
78
|
+
|
|
79
|
+
Params: `{ filePath, workspaceRoot, position: { line, character } }` (zero-based cursor).
|
|
80
|
+
|
|
81
|
+
Result: `{ kind: "completion", filePath, position, items: [{ label, kind?, detail?, insertText?, sortText?, textEdit? }], truncated, total }`. Reference-only: the editor owns insertion.
|
|
82
|
+
|
|
83
|
+
### `quickfix.apply` (writes)
|
|
84
|
+
|
|
85
|
+
Params: `{ filePath, workspaceRoot, range?, title?, index?, only?, sandbox_permissions?, justification? }`.
|
|
86
|
+
|
|
87
|
+
- `title` (exact match) wins over `index` (zero-based, default `0`). Without `range`, the action targets the first cached **error** diagnostic of the file, when the cache holds a snapshot of the same source version.
|
|
88
|
+
- The backend runs `textDocument/codeAction` through the same seam-first runner the model tools use, selects the action, and applies its edits through the official write path (`fs/write-intent` + the session's official permission preset). Command-only actions are never executed. All targets are pre-flighted before the first write; out-of-workspace edits fail as `LSP_ACTION_CONFLICT`.
|
|
89
|
+
|
|
90
|
+
Result: `{ kind: "quickfixApplied", filePath, title, filesChanged, appliedEdits, diffs: [{ filePath, before, after }] }` or `{ kind: "unchanged", filePath }`.
|
|
91
|
+
|
|
92
|
+
### `format` (writes)
|
|
93
|
+
|
|
94
|
+
Params: `{ filePath, workspaceRoot, range?, sandbox_permissions?, justification? }` (`range` zero-based; omit for the whole file).
|
|
95
|
+
|
|
96
|
+
Result: `{ kind: "formatted", filePath, appliedEdits, linesChanged, before, after }` or `{ kind: "unchanged", filePath }`.
|
|
97
|
+
|
|
98
|
+
## `lsp.events`
|
|
99
|
+
|
|
100
|
+
Client notification `{ "method": "lsp.events", "params": { "subscribe": true|false } }`. While subscribed, the server pushes `lsp.event` notifications:
|
|
101
|
+
|
|
102
|
+
| Kind | Payload | Meaning |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `diagnostics.updated` | `{ filePath, diagnostics, truncated, total, source? }` | A fresh `diagnostics.get` snapshot (also emitted on demand, not only on subscription). |
|
|
105
|
+
| `action.status` | `{ requestId, action, status: started\|succeeded\|failed, error? }` | One event per run lifecycle transition. |
|
|
106
|
+
| `file.changed` | `{ filePath }` | A write action edited the file (or the filesystem observed a new version); cached diagnostics for it are invalidated. |
|
|
107
|
+
| `sessions.changed` | `{ sessions }` | The live-session snapshot changed. |
|
|
108
|
+
|
|
109
|
+
## Error codes
|
|
110
|
+
|
|
111
|
+
Every failed envelope carries a stable `code`. Route on it, never on message text.
|
|
112
|
+
|
|
113
|
+
| Code | Meaning |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `LSP_ACTION_UNAVAILABLE` | No server entry and no seam provider handles the file; also the run-timeout failure. |
|
|
116
|
+
| `LSP_ACTION_UNSUPPORTED` | The server does not advertise the operation. |
|
|
117
|
+
| `LSP_ACTION_SERVER_FAILED` | The server failed (stderr tail in the message); startup failures retry once. |
|
|
118
|
+
| `LSP_ACTION_MALFORMED_RESPONSE` | The server sent a structurally invalid payload. |
|
|
119
|
+
| `LSP_ACTION_CONFLICT` | The file changed since it was read, or edits overlap / leave the workspace. |
|
|
120
|
+
| `LSP_ACTION_READ_ONLY` | The session's official permission preset is `read-only`; refused before any server round-trip. |
|
|
121
|
+
| `LSP_ACTION_WORKSPACE_REQUIRED` | The request carries no usable workspace root. |
|
|
122
|
+
| `LSP_ACTION_UNKNOWN` | Unknown action id, or no code action matched `title`/`index`. |
|
|
123
|
+
| `LSP_ACTION_INVALID_ARGS` | Malformed action parameters. |
|
|
124
|
+
| `LSP_ACTION_APPROVAL_UNAVAILABLE` | The official approval path could not grant a wider sandbox mode (no approval service, no live agent, no open turn, rejection, cancellation). |
|
|
125
|
+
| `LSP_PROTOCOL_VERSION_UNSUPPORTED` | The `protocol` a run declared is not served by this server. |
|
|
126
|
+
|
|
127
|
+
## Permission presets and approval (write actions)
|
|
128
|
+
|
|
129
|
+
Write actions (`quickfix.apply`, `format`) never bypass the official permission machinery:
|
|
130
|
+
|
|
131
|
+
1. **Permission preset gate.** The backend resolves the addressed session's official sandbox policy (`ctx.sandboxPolicy`). A `read-only` session refuses the write with `LSP_ACTION_READ_ONLY` before any server round-trip; `workspace-write` confines every edit to the session workspace (the official filesystem fence enforces it); `danger-full-access` passes through.
|
|
132
|
+
2. **Official write path.** Applied edits go through the `fs/write-intent` waterfall and guarded writes, exactly like the `lsp_format` tool — a file that changed on disk fails as `LSP_ACTION_CONFLICT`, never a silent clobber.
|
|
133
|
+
3. **Approval (`ask`).** The editor may carry the official escalation pair `sandbox_permissions` + `justification`; it is resolved through `approveEscalation` → `ctx.approval.request`, i.e. the same choreography as `write`/`edit` escalation. Because the official audit pair requires an open agent turn, escalation asks bind to the addressed session's live agent: with a live agent in an open turn, the composed answerers decide (Web UI dialog, CLI, or an ACP client's `session/request_permission`); without one, the run fails closed with `LSP_ACTION_APPROVAL_UNAVAILABLE`.
|
|
134
|
+
|
|
135
|
+
## How this composes with the official ACP server and the Python SDK
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
┌─────────────────────────────────────────────┐
|
|
139
|
+
│ one DSH runtime (headless backend) │
|
|
140
|
+
VS Code extension ────┤ editor action protocol (this plugin) │
|
|
141
|
+
(UI only) │ dsh-lsp-actions, editor.enabled: true │
|
|
142
|
+
├─────────────────────────────────────────────┤
|
|
143
|
+
official ACP clients ─┤ @deepseek-ai/dsh-acp (agent chat sessions) │
|
|
144
|
+
(separate spawn) │ session/new · session/prompt · permissions │
|
|
145
|
+
├─────────────────────────────────────────────┤
|
|
146
|
+
Python SDK ───────────┤ @deepseek-ai/dsh-sdk-jsonrpc-server │
|
|
147
|
+
(separate spawn) │ session/prompt · session.event notifications│
|
|
148
|
+
└─────────────────────────────────────────────┘
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- **Official ACP server** ([`@deepseek-ai/dsh-acp`](../../../packages/acp/acp/README.md) in the harness repo) is the *agent-conversation* transport: `session/new`, `session/prompt`, `session/cancel`, `session/request_permission`. Editors that want to chat with DSH agents use it — and the agents those ACP sessions run can call the plugin's `lsp_*` model tools. The editor action protocol is the *direct-capability* transport: no model round-trip, structured results. Each transport owns one stdio connection, so a runtime serves one owner per process; the same profile/persistence root can back both processes.
|
|
152
|
+
- **Approval interop.** When an editor write action escalates inside an ACP-owned session's open turn, the official approval ask flows to the ACP client as `session/request_permission` — the ACP client (or a policy) answers it, and the editor observes the outcome via `action.status`.
|
|
153
|
+
- **Python SDK** (`pip install deepseek-harness-sdk`) drives the same runtime over the official SDK wire (`session/prompt` + notifications). SDK users do not need the editor protocol: their sessions' agents call the `lsp_*` tools. Because this protocol uses the same newline-delimited JSON-RPC framing, an SDK-grade client can speak it unchanged — call `lsp.actions.list`, `lsp.actions.run`, and read `lsp.event` notifications with the same transport code.
|
|
154
|
+
|
|
155
|
+
## Engineering guarantees
|
|
156
|
+
|
|
157
|
+
- **Reversible registration.** The transport, event listeners, and cache all live in the plugin's effect scope; stopping or updating the plugin tears all of them down.
|
|
158
|
+
- **Bounded diagnostics cache.** A Map-backed LRU capped by `editor.diagnosticsCacheMaxFiles` (default 64), freshness-stamped and invalidated by filesystem observations and by the plugin's own writes.
|
|
159
|
+
- **Schema configuration.** `editor.enabled` (default `false` — only headless backends may claim stdio), `editor.requestTimeoutMs` (default 60000, enforced per run), `editor.diagnosticsCacheMaxFiles` (default 64). Misconfiguration fails at load.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# 编辑器 Action 协议 v1
|
|
2
|
+
|
|
3
|
+
[English](editor-protocol.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
**编辑器 action 协议**是 `dsh-lsp-actions` 面向编辑器的稳定接口:任何编辑器(先行 VS Code)通过**换行分帧的 JSON-RPC 2.0** 消费插件的 LSP 能力——无需驱动 agent,也无需自己实现 LSP。线上帧格式与官方 DeepSeek Harness SDK/ACP 传输完全一致(每行一个 JSON 对象);区别仅在方法词汇。
|
|
6
|
+
|
|
7
|
+
| 方法 | 方向 | 用途 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `lsp.actions.list` | 客户端 → 服务端 | 协议版本、动作目录、可寻址的 DSH 会话。 |
|
|
10
|
+
| `lsp.actions.run` | 客户端 → 服务端 | 执行一个动作(`diagnostics.get`、`quickfix.apply`、`format`、`completion.get`),返回结构化结果信封。 |
|
|
11
|
+
| `lsp.events` | 客户端 → 服务端(通知) | `{subscribe: true/false}` 开关事件流。 |
|
|
12
|
+
| `lsp.event` | 服务端 → 客户端(通知) | 流式事件:诊断更新、动作状态、文件变更、会话变更。 |
|
|
13
|
+
|
|
14
|
+
传输:每行一个 UTF-8 JSON 对象(以 `\n` 结尾)。请求携带 `id` + `method` + `params`;响应携带 `id` + `result`(或 `error`);通知只携带 `method` + `params`。格式错误的行被忽略。未知方法应答 `-32601`;处理失败应答 `-32603`。
|
|
15
|
+
|
|
16
|
+
## 版本化与向后兼容承诺
|
|
17
|
+
|
|
18
|
+
- 协议带版本:`lsp.actions.list` 返回 `protocol: "lsp-actions/v1"` 与 `version: 1`。`lsp.actions.run` 可携带 `protocol`;服务端不支持时以 `LSP_PROTOCOL_VERSION_UNSUPPORTED` 的失败信封应答。
|
|
19
|
+
- **v1 已冻结。** v1 的所有字段名、动作 id、事件类型与错误码保持稳定。演进只做加法:
|
|
20
|
+
- 新增动作、新字段、新事件类型无需升版本;
|
|
21
|
+
- 既有字段语义不得原地变更;
|
|
22
|
+
- 破坏性变更以新的 `protocol` 版本发布,服务端可同时服务多个版本。
|
|
23
|
+
- **客户端必须**忽略未知字段、未知事件类型与目录中未宣告的动作;按稳定错误 `code` 路由,绝不解析消息文本。
|
|
24
|
+
|
|
25
|
+
## `lsp.actions.list`
|
|
26
|
+
|
|
27
|
+
参数:`{}`。
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"protocol": "lsp-actions/v1",
|
|
32
|
+
"version": 1,
|
|
33
|
+
"actions": [
|
|
34
|
+
{ "action": "diagnostics.get", "writes": false, "description": "…" },
|
|
35
|
+
{ "action": "completion.get", "writes": false, "description": "…" },
|
|
36
|
+
{ "action": "quickfix.apply", "writes": true, "description": "…" },
|
|
37
|
+
{ "action": "format", "writes": true, "description": "…" }
|
|
38
|
+
],
|
|
39
|
+
"sessions": [
|
|
40
|
+
{ "sessionId": "…", "cwd": "/path/to/workspace", "live": true }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`actions` 是闭合的 v1 目录。`sessions` 列出后端运行时的活跃 DSH 会话;`run` 可传 `sessionId` 把权限预设与审批绑定到该会话。
|
|
46
|
+
|
|
47
|
+
## `lsp.actions.run`
|
|
48
|
+
|
|
49
|
+
参数:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
{
|
|
53
|
+
action: string // lsp.actions.list 中的动作 id
|
|
54
|
+
params?: object // 各动作参数(见下)
|
|
55
|
+
sessionId?: string // 将权限预设与审批绑定到该 DSH 会话
|
|
56
|
+
requestId?: string // 在结果与每个 action.status 事件中回显
|
|
57
|
+
protocol?: string // 可选;"lsp-actions/v1"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
结果**始终**是同一信封——成功与失败均为结构化、可机器路由:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{ "requestId": "q1", "action": "quickfix.apply", "status": "succeeded", "result": { … } }
|
|
65
|
+
{ "requestId": "q1", "action": "quickfix.apply", "status": "failed",
|
|
66
|
+
"error": { "code": "LSP_ACTION_READ_ONLY", "message": "…" } }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
线上位置与范围采用 **LSP 约定**:零基、半开区间、UTF-16。
|
|
70
|
+
|
|
71
|
+
### `diagnostics.get`(只读)
|
|
72
|
+
|
|
73
|
+
参数:`{ filePath, workspaceRoot, includeSource? }`。
|
|
74
|
+
|
|
75
|
+
结果:`{ kind: "diagnostics", filePath, diagnostics: [{ severity, range, message, source?, code? }], truncated, total, source? }`。快照存入后端有界的 LRU 诊断缓存(配置 `editor.diagnosticsCacheMaxFiles`),并以 `diagnostics.updated` 事件推送给所有订阅者。
|
|
76
|
+
|
|
77
|
+
### `completion.get`(只读)
|
|
78
|
+
|
|
79
|
+
参数:`{ filePath, workspaceRoot, position: { line, character } }`(零基光标)。
|
|
80
|
+
|
|
81
|
+
结果:`{ kind: "completion", filePath, position, items: [{ label, kind?, detail?, insertText?, sortText?, textEdit? }], truncated, total }`。仅供参考:插入由编辑器自己完成。
|
|
82
|
+
|
|
83
|
+
### `quickfix.apply`(写)
|
|
84
|
+
|
|
85
|
+
参数:`{ filePath, workspaceRoot, range?, title?, index?, only?, sandbox_permissions?, justification? }`。
|
|
86
|
+
|
|
87
|
+
- `title`(精确匹配)优先于 `index`(零基,默认 `0`)。未给 `range` 时,若缓存中持有同源文件版本快照,则以该文件第一条**错误**诊断的范围为目标。
|
|
88
|
+
- 后端通过模型工具同款 seam-first runner 执行 `textDocument/codeAction`,选中动作后经官方写入路径(`fs/write-intent` + 会话官方权限预设)应用其编辑。仅命令型动作永不执行。所有目标在首个字节写入前完成预检;越出工作区的编辑以 `LSP_ACTION_CONFLICT` 失败。
|
|
89
|
+
|
|
90
|
+
结果:`{ kind: "quickfixApplied", filePath, title, filesChanged, appliedEdits, diffs: [{ filePath, before, after }] }` 或 `{ kind: "unchanged", filePath }`。
|
|
91
|
+
|
|
92
|
+
### `format`(写)
|
|
93
|
+
|
|
94
|
+
参数:`{ filePath, workspaceRoot, range?, sandbox_permissions?, justification? }`(`range` 零基;省略即全文件)。
|
|
95
|
+
|
|
96
|
+
结果:`{ kind: "formatted", filePath, appliedEdits, linesChanged, before, after }` 或 `{ kind: "unchanged", filePath }`。
|
|
97
|
+
|
|
98
|
+
## `lsp.events`
|
|
99
|
+
|
|
100
|
+
客户端通知 `{ "method": "lsp.events", "params": { "subscribe": true|false } }`。订阅期间,服务端推送 `lsp.event` 通知:
|
|
101
|
+
|
|
102
|
+
| 类型 | 载荷 | 含义 |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `diagnostics.updated` | `{ filePath, diagnostics, truncated, total, source? }` | 一次新的 `diagnostics.get` 快照(按需产生,非仅订阅时)。 |
|
|
105
|
+
| `action.status` | `{ requestId, action, status: started\|succeeded\|failed, error? }` | 每次运行的生命周期转换。 |
|
|
106
|
+
| `file.changed` | `{ filePath }` | 写动作编辑了文件(或文件系统观察到新版本);该文件的缓存诊断已失效。 |
|
|
107
|
+
| `sessions.changed` | `{ sessions }` | 活跃会话快照变化。 |
|
|
108
|
+
|
|
109
|
+
## 错误码
|
|
110
|
+
|
|
111
|
+
每个失败信封携带稳定 `code`。请按 code 路由,切勿解析消息文本。
|
|
112
|
+
|
|
113
|
+
| 码 | 含义 |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `LSP_ACTION_UNAVAILABLE` | 无服务器条目且无 seam 提供方处理该文件;也用于运行超时。 |
|
|
116
|
+
| `LSP_ACTION_UNSUPPORTED` | 服务器未宣告该操作。 |
|
|
117
|
+
| `LSP_ACTION_SERVER_FAILED` | 服务器失败(消息带 stderr 尾部);启动失败重试一次。 |
|
|
118
|
+
| `LSP_ACTION_MALFORMED_RESPONSE` | 服务器返回结构非法的载荷。 |
|
|
119
|
+
| `LSP_ACTION_CONFLICT` | 文件在读取后变更,或编辑重叠/越出工作区。 |
|
|
120
|
+
| `LSP_ACTION_READ_ONLY` | 会话官方权限预设为 `read-only`;在任何服务器往返前拒绝。 |
|
|
121
|
+
| `LSP_ACTION_WORKSPACE_REQUIRED` | 请求没有可用的工作区根。 |
|
|
122
|
+
| `LSP_ACTION_UNKNOWN` | 未知动作 id,或没有 code action 匹配 `title`/`index`。 |
|
|
123
|
+
| `LSP_ACTION_INVALID_ARGS` | 动作参数不合法。 |
|
|
124
|
+
| `LSP_ACTION_APPROVAL_UNAVAILABLE` | 官方审批路径未能授予更宽沙箱模式(无审批服务、无活跃 agent、无开放 turn、被拒或取消)。 |
|
|
125
|
+
| `LSP_PROTOCOL_VERSION_UNSUPPORTED` | 该运行所声明的 `protocol` 不被本服务端支持。 |
|
|
126
|
+
|
|
127
|
+
## 权限预设与审批(写动作)
|
|
128
|
+
|
|
129
|
+
写动作(`quickfix.apply`、`format`)绝不绕过官方权限机制:
|
|
130
|
+
|
|
131
|
+
1. **权限预设门禁。** 后端解析所寻址会话的官方沙箱策略(`ctx.sandboxPolicy`)。`read-only` 会话在任何服务器往返前以 `LSP_ACTION_READ_ONLY` 拒绝写操作;`workspace-write` 把每次编辑限制在会话工作区内(由官方文件系统栅栏执行);`danger-full-access` 放行。
|
|
132
|
+
2. **官方写入路径。** 应用的编辑走 `fs/write-intent` 瀑布与受守卫写入,与 `lsp_format` 工具完全一致——磁盘上文件已变化时以 `LSP_ACTION_CONFLICT` 失败,绝不静默覆盖。
|
|
133
|
+
3. **审批(`ask`)。** 编辑器可携带官方升级对 `sandbox_permissions` + `justification`;经 `approveEscalation` → `ctx.approval.request` 解析,即与 `write`/`edit` 升级相同的编排。因官方审计对要求开放 agent turn,升级请求绑定所寻址会话的活跃 agent:有活跃 agent 且处于开放 turn 时,由已组合的应答方决定(Web UI 对话框、CLI,或 ACP 客户端的 `session/request_permission`);否则以 `LSP_ACTION_APPROVAL_UNAVAILABLE` 失败关闭。
|
|
134
|
+
|
|
135
|
+
## 与官方 ACP server、Python SDK 的配合
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
┌─────────────────────────────────────────────┐
|
|
139
|
+
│ 一个 DSH 运行时(headless 后端) │
|
|
140
|
+
VS Code 扩展 ─────────┤ 编辑器 action 协议(本插件) │
|
|
141
|
+
(纯 UI) │ dsh-lsp-actions, editor.enabled: true │
|
|
142
|
+
├─────────────────────────────────────────────┤
|
|
143
|
+
官方 ACP 客户端 ──────┤ @deepseek-ai/dsh-acp(agent 对话会话) │
|
|
144
|
+
(独立进程) │ session/new · session/prompt · permissions │
|
|
145
|
+
├─────────────────────────────────────────────┤
|
|
146
|
+
Python SDK ───────────┤ @deepseek-ai/dsh-sdk-jsonrpc-server │
|
|
147
|
+
(独立进程) │ session/prompt · session.event 通知 │
|
|
148
|
+
└─────────────────────────────────────────────┘
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- **官方 ACP server**(harness 仓库中的 [`@deepseek-ai/dsh-acp`](../../../packages/acp/acp/README.md))是 *agent 对话*传输:`session/new`、`session/prompt`、`session/cancel`、`session/request_permission`。想与 DSH agent 聊天的编辑器使用它——这些 ACP 会话中的 agent 可以调用本插件的 `lsp_*` 模型工具。编辑器 action 协议是*直接能力*传输:无模型往返、结构化结果。每个传输独占一条 stdio 连接,因此每个进程只服务一个归属方;同一 profile/持久化根可支撑多个进程。
|
|
152
|
+
- **审批互操作。** 当编辑器写动作在 ACP 拥有的会话的开放 turn 内升级时,官方审批请求以 `session/request_permission` 流向 ACP 客户端——由 ACP 客户端(或策略)作答,编辑器通过 `action.status` 观察结果。
|
|
153
|
+
- **Python SDK**(`pip install deepseek-harness-sdk`)经官方 SDK 线(`session/prompt` + 通知)驱动同一运行时。SDK 用户无需编辑器协议:其会话 agent 直接调用 `lsp_*` 工具。由于本协议使用相同的换行分帧 JSON-RPC,SDK 级客户端可用同一套传输代码直接调用 `lsp.actions.list`、`lsp.actions.run` 并读取 `lsp.event` 通知。
|
|
154
|
+
|
|
155
|
+
## 工程保证
|
|
156
|
+
|
|
157
|
+
- **可逆注册。** 传输、事件监听与缓存全部处于插件 effect 作用域内;停止或更新插件即全部拆除。
|
|
158
|
+
- **有界诊断缓存。** Map 实现 LRU,上限 `editor.diagnosticsCacheMaxFiles`(默认 64),带新鲜度戳,并随文件系统观察与本插件自身写入而失效。
|
|
159
|
+
- **Schema 配置。** `editor.enabled`(默认 `false`——只有 headless 后端才可占用 stdio)、`editor.requestTimeoutMs`(默认 60000,逐次运行强制)、`editor.diagnosticsCacheMaxFiles`(默认 64)。配置错误在加载时即失败。
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# LSP 动作面 seam 扩展调研笔记
|
|
2
|
+
|
|
3
|
+
状态:2026-08-14。本文档是交付物第 1 条:结论分叉的调研记录与决策依据。
|
|
4
|
+
|
|
5
|
+
> 2026-08-15 增补:对当前 harness 树(`a0ab396`)执行 `git apply --check upstream/lsp-action-seam.patch` 干净通过(34 文件,+1201/−201),补丁仍可重放;跟踪表见 `upstream/README.md`。插件的 seam 探测已改为按调用惰性解析(不再依赖加载顺序)。
|
|
6
|
+
|
|
7
|
+
## 1. 现状盘点(调研结论)
|
|
8
|
+
|
|
9
|
+
### 官方 seam 词汇(`packages/lsp/`)
|
|
10
|
+
|
|
11
|
+
- `@deepseek-ai/dsh-lsp`(Service Definition)在单一 `ctx.lsp` 服务上只暴露 **4 个导航操作**:`goToDefinition` / `findReferences` / `goToImplementation` / `hover`。`LspOperation` 是**闭合联合**,文档明言"新增一项查询会通过编译强制要求同步修改 seam、提供方和工具";`LspQueryResult` 是闭合可辨识联合(`locations` / `hover`),消费方 `switch` 穷尽。
|
|
12
|
+
- 请求 `LspQueryRequest` 的四个字段全部必填:`operation` / `filePath` / `position` / `workspaceRoot`;位置与范围是**从零开始的 UTF-16** 坐标,模型面向的工具做从 1 开始的光标换算。
|
|
13
|
+
- `@deepseek-ai/dsh-lsp-stdio`(Provider)是通用 stdio 语言服务器宿主:每服务器一个隔离 provider、按工作区惰性单飞、瞬态 `didOpen`→请求→`didClose` 生命周期、坏 transport 换进程重试一次、进程树级 teardown。它是本插件自带 client 的直接范本。
|
|
14
|
+
- `@deepseek-ai/dsh-tool-lsp`(Consumer)是 `lsp` 工具:schema、位置归一化、结果上限(`maxLocations`/`maxResultChars`)、`timeoutMs` 交给官方 `dsh-tool-call-timeout-policy` 执行。
|
|
15
|
+
- seam 无协议类型、进程或文档控制、无 JSON-RPC 逃生口;错误按稳定码(`LSP_*`)路由。
|
|
16
|
+
|
|
17
|
+
### 门禁理解(实现前已跑)
|
|
18
|
+
|
|
19
|
+
- `pnpm run verify-cordis-api`(= `scripts/gen-cordis-api.ts --check`)在干净树上通过(EXIT=0)。它校验的是生成式 Cordis 核心 API 文档;**seam 词汇改动真正牵动的门禁是 `gen-cordis-catalog`(docs/subsystems/lsp.* 的 cordis-surface 区)与 `verify-type-equiv`(文档 `type-equiv` 块必须与 `types.ts` 逐字节一致)**,还有 `verify-export-jsdoc`(新导出需 JSDoc)。上游补丁按此方向实现并逐项跑绿。
|
|
20
|
+
- 工具契约(`docs/cookbook/adding-a-tool.zh.md`):`execute` 返回 `output.schema` 声明的规范 JSON;尊重 `exec.signal`;`timeoutMs` 由 `dsh-tool-call-timeout-policy` 通过替换 `exec.signal` 执行,插件不得自实现超时;UI 卡片 presenter 是 `args`(+ result)的纯函数;diff 卡片走 `presentationMeta` 持久化投影,保证回放可重现。
|
|
21
|
+
|
|
22
|
+
### 冲突排查(GitHub)
|
|
23
|
+
|
|
24
|
+
- GitHub topic `dsh-plugin`(约 550 仓)+ topic `lsp` 检索:**无任何 DSH 插件实现 LSP 动作面**;"diagnostics" 命中的全是健康检查类插件;OpenCode bridge 只桥接 skills/config,不包装 OpenCode 的 LSP。
|
|
25
|
+
- `deepseek-ai/deepseek-harness` 的 issue/PR 检索(lsp/diagnostics/formatting/completion/formatDocument/publishDiagnostics):**0 条**,无人提过 seam 扩展提案。
|
|
26
|
+
- 结论:`lsp_diagnostics` / `lsp_format` / `lsp_completion` 三个工具名在 DSH 生态内无冲突,是生态首例。
|
|
27
|
+
|
|
28
|
+
## 2. 结论分叉
|
|
29
|
+
|
|
30
|
+
**路径一(推荐,已执行)**:向 `ctx.lsp` seam 词汇提上游 PR——新增 `diagnostics` / `formatDocument` / `completion` 操作、对应 provider 契约与位置归一化;本插件作为官方 seam 的消费方实现三个工具与结果卡片。补丁文件在本仓库 `upstream/lsp-action-seam.patch`(PR 描述见 `upstream/PR-description.md`)。
|
|
31
|
+
|
|
32
|
+
**路径二(兜底,已实现为插件的内置 client)**:PR 合入周期不可控,插件自带最小 LSP 客户端(`src/client.ts` 等:`ctx.subprocess.spawn` + stdio JSON-RPC,可执行路径走 Config `servers` 表,不硬编码),实现同样的三个工具。README 明确"待 seam 扩展 PR 合入后迁移到 ctx.lsp"。
|
|
33
|
+
|
|
34
|
+
**提案提交流程(2026-08-14 实测)**:上游仓库关闭了 issues(`has_issues=false`),GitHub 因此**禁用 PR 功能**(pulls 端点整体 404)。提案已改道提交到官方 **GitHub Discussions #781**(Ideas 分类):https://github.com/deepseek-ai/deepseek-harness/discussions/781 —— 文中附了 fork 分支(`PerryLink/deepseek-harness:agent/lsp-action-seam`,提交 7f10651,门禁全绿)、补丁文件与本插件的链接;上游开启 PR 后可直接把该分支开成 PR。
|
|
35
|
+
|
|
36
|
+
**混合架构(本插件实际形态)**:每次动作调用 **seam 优先、自带 client 兜底**(`src/runner.ts`):
|
|
37
|
+
|
|
38
|
+
1. `ctx.lsp` 已挂载 → 尝试 seam.query;成功即返回。
|
|
39
|
+
2. 失败分类(按错误 `code`,不用 instanceof 跨副本判断):
|
|
40
|
+
- `LSP_UNAVAILABLE`(该扩展名无 provider 路由)/ **无 code 的普通错误**(旧 seam 的 provider 对未知操作 assertNever)→ 旧 seam 或无路由 → 落到自带 client。
|
|
41
|
+
- `LSP_UNSUPPORTED_OPERATION`(新 seam、provider 存在但服务器无该能力)→ **响亮失败**,不悄悄起第二台服务器(两处都配了服务器是配置错误)。
|
|
42
|
+
- 其他带 code 的 LSP 错误 → 原样上抛。
|
|
43
|
+
3. seam 未挂载 → 直接自带 client;`servers` 表按 fileGlob → 扩展名映射路由。
|
|
44
|
+
|
|
45
|
+
## 3. 上游 PR 提案要点(`upstream/lsp-action-seam.patch`)
|
|
46
|
+
|
|
47
|
+
- `LspOperation` 闭合联合 + 3:`diagnostics` / `formatDocument` / `completion`。
|
|
48
|
+
- 请求改为闭合联合(不同操作 arity 不同,无法再"全字段必填"):位置型(导航+completion 带 `position`)、`diagnostics`(无位置)、`formatDocument`(可选 `range`)。
|
|
49
|
+
- 结果联合 + 3 分支:`diagnostics`(带 `resolvedWorkspaceUri`)、`edits`(格式化返回 TextEdit[],**seam/provider 永不落盘**——应用由消费方走 fs write-intent)、`completion`。
|
|
50
|
+
- provider 侧:`requestMethod`/`supportsOperation` 请求感知(range → `textDocument/rangeFormatting`);诊断双路径——服务器有 `diagnosticProvider` 走 pull `textDocument/diagnostic`,否则 didOpen 后收 `publishDiagnostics` 推送并受 `diagnosticsSettleMs` 限时;`LspConnection` 增加通知观察钩子。
|
|
51
|
+
- 范围:不动 `tool-lsp` 的 4 操作与输出 schema(动作工具在插件里),只补 `assertNever` 不可达分支;双语文档(lsp.zh.md/lsp.md + type-equiv 逐字节同步 + gen-cordis-catalog 重生成);Agent Note + 测试 + keyless snapshot 判定(无仓库内模型可见行为变化,故不新增 snapshot,判定写进 Agent Note)。
|
|
52
|
+
|
|
53
|
+
## 4. 本插件的硬性契约落实情况
|
|
54
|
+
|
|
55
|
+
| 契约 | 落实位置 |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| 三工具尊重 `exec.signal` 与工具超时 | 每个 `defineTool` 声明 `timeoutMs`(Config 默认 60000),官方 timeout-policy 替换信号;全链路(读文件、握手、请求、取消竞速)走 `abortable` + `$/cancelRequest` + `killGraceMs` 终止实例 |
|
|
58
|
+
| `lsp_format` 写入过 fs 意图事件与权限检查 | 读后 `fs/observed`(present+version)→ `fs/write-intent` waterfall 取意图 → `ctx.fs.writeText(..., intent, signal, sandboxPolicy)` → 写后 `fs/observed` |
|
|
59
|
+
| read-only sandbox 拒绝写入(fail loud 结构化) | 见下 §4.1 |
|
|
60
|
+
| 诊断/补全不落持久层、不跨会话缓存 | 工具结果只进会话日志(模型历史本身);无任何磁盘/内存缓存;不提供 cache 开关(更严格地满足"除非 Config 显式开启") |
|
|
61
|
+
| `servers` 配置形态与官方对齐 | `command`/`args`/`env`/`extensionToLanguage`/`initializationOptions`/`configuration`/字节上限/计时器字段与 `lsp-stdio` 同名同默认;另加 `fileGlobs`(glob 优先于扩展名)与 `formattingOptions`/`diagnosticsSettleMs`;`maxDocumentBytes` 因读取发生在上具层而提升为插件级 Config(文档已注明偏差) |
|
|
62
|
+
|
|
63
|
+
### 4.1 权限矩阵(`lsp_format` / `lsp_rename`)
|
|
64
|
+
|
|
65
|
+
- 裸 fs(无 sandboxMode)→ 无 policy,走无条件写(与官方 `write` 工具语义一致)。
|
|
66
|
+
- 约束型 fs 后端 + 会话 `read-only` → **在任何服务器往返之前**抛 `LspActionError('LSP_ACTION_READ_ONLY')`,文本为共享的 `[sandbox: file access denied under read-only mode]` + 同轮升级提示。
|
|
67
|
+
- 升级:`sandbox_permissions`/`justification` 参数对(仅约束后端下广告),经 `@deepseek-ai/dsh-sandbox` 的 `approveEscalation` 严格放宽(read-only → workspace-write → danger-full-access),与 `dsh-tool-fs` 的 write/edit 完全同源。
|
|
68
|
+
- 写时拒绝(FS_SANDBOX_DENIED)→ 映射为共享 `[sandbox: …]` 标记(保留结构化 code)。
|
|
69
|
+
- 格式冲突:重叠 edits 或越界 edits → `LSP_ACTION_CONFLICT`;磁盘内容在读后被改(`replaceIfVersion` → `FS_STALE_VERSION` / `FS_NOT_OBSERVED`)→ `LSP_ACTION_CONFLICT`,错误文本提示模型**选择**:重读后重跑,或用 edit/write 手工应用 diff。
|
|
70
|
+
- `lsp_rename` 在同一矩阵上再叠加一道**写入前预检**:每个目标文档先做工作区包含性校验(越出工作区 → `LSP_ACTION_CONFLICT`,任何字节写出前失败)、重叠检查与字节上限读取,无实际变化的文件不进入写入计划;非 utf-16 服务器的跨文件位置按文档解码(需读取每个被编辑文件,不可读 → 结构化冲突)。
|
|
71
|
+
|
|
72
|
+
## 5. 已知取舍
|
|
73
|
+
|
|
74
|
+
- seam 路径下工具也先读一遍源文件(为了 format 的 diff 基线与观测),seam provider 内部会再读一份;代价是双读,换来单一基线与统一 prepare 路径,PR 合入后可优化。
|
|
75
|
+
- 坏 server 启动:命令在 **load 期**经 `subprocess.resolveExecutable` 解析(与 lsp-stdio 对齐,坏命令 fail loud at load);运行期握手/进程失败 → `LSP_ACTION_SERVER_FAILED` + stderr tail,且不重试(调用失败即 isError,模型重跑会因死实例被逐出而重新拉起)。
|
|
76
|
+
- 诊断卡片的"严重级颜色"是中性的卡片词汇没有的字段;卡片只带 `[Error]` 等标签文本与行号,颜色映射是 UI 桥接层的能力(README 已注明)。
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# dsh-lsp-actions VS Code example
|
|
2
|
+
|
|
3
|
+
A minimal VS Code extension that consumes the [`dsh-lsp-actions`](../../) **editor action protocol** (`lsp.actions.list` / `lsp.actions.run` / `lsp.events`) over ACP-style JSON-RPC. The extension is **UI-only**: it never implements LSP logic, never computes diagnostics, and never edits buffers — every capability and every byte written belongs to the plugin backend.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌──────────────────┐ stdio JSON-RPC (newline-delimited) ┌───────────────────────────────┐
|
|
7
|
+
│ VS Code │ lsp.actions.list / lsp.actions.run │ editor backend (headless DSH)│
|
|
8
|
+
│ extension (UI) │ ────────────────────────────────────► │ dsh-lsp-actions │
|
|
9
|
+
│ sidebar + cmds │ ◄──────────────────────────────────── │ (editor.enabled: true) │
|
|
10
|
+
│ │ lsp.event (diagnostics, status, …) │ fs-local · subprocess-local │
|
|
11
|
+
└──────────────────┘ │ LSP servers (tsls, …) │
|
|
12
|
+
└───────────────────────────────┘
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What the sidebar shows
|
|
16
|
+
|
|
17
|
+
- **DSH Sessions** — the sessions living in the backend runtime (the `sessionId` a run can bind its permission preset to).
|
|
18
|
+
- **LSP Diagnostics** — per-file diagnostics pushed by the backend (`diagnostics.updated` events) and refreshed on demand. Each diagnostic row:
|
|
19
|
+
- click → opens the file at the range (`openDiagnostic`, pure VS Code UI);
|
|
20
|
+
- **Apply quickfix** button → the backend selects the server's preferred code action for that range and applies it through the official write policy (`quickfix.apply`). The extension never touches the buffer.
|
|
21
|
+
- **DSH: Format the active document** → `format` through the backend (whole file, server-verified edits).
|
|
22
|
+
|
|
23
|
+
## Install & run
|
|
24
|
+
|
|
25
|
+
### 1. Install the backend
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
cd examples/vscode/backend
|
|
29
|
+
npm install
|
|
30
|
+
# make sure a language server is on PATH, e.g.:
|
|
31
|
+
npm install -g typescript-language-server typescript
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Launch the extension
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
cd examples/vscode/extension
|
|
38
|
+
code --new-window path/to/your/typescript/project .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- In VS Code: **Run and Debug → Run Extension** (`F5`), or `Extensions: Install from VSIX…` after `npx @vscode/vsce package`.
|
|
42
|
+
- Open the **DSH LSP Actions** activity-bar container (server-process icon).
|
|
43
|
+
|
|
44
|
+
### 3. Use it
|
|
45
|
+
|
|
46
|
+
1. **DSH: Connect to the editor backend** (command palette) — spawns `node backend/bin.mjs backend/cordis.yml` and reads `lsp.actions.list`.
|
|
47
|
+
2. Open a `.ts` file with an error, then press the **↻ refresh** button in the LSP Diagnostics view.
|
|
48
|
+
3. Expand the diagnostic and click **Apply quickfix** — the backend applies the server's fix and pushes `file.changed`; refresh shows the error gone.
|
|
49
|
+
4. Run **DSH: Format the active document** — indentation is rewritten by `typescript-language-server` through the backend.
|
|
50
|
+
|
|
51
|
+
## Settings
|
|
52
|
+
|
|
53
|
+
| Setting | Default | Meaning |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `dshLspActions.backend.node` | `node` | Node executable for the backend. |
|
|
56
|
+
| `dshLspActions.backend.bin` | (bundled path) | Absolute path to `backend/bin.mjs`. |
|
|
57
|
+
| `dshLspActions.backend.config` | (bundled path) | Absolute path to `backend/cordis.yml`. |
|
|
58
|
+
|
|
59
|
+
## Recording the demo gif
|
|
60
|
+
|
|
61
|
+
The README ships a demo gif recorded with this flow on a clean profile:
|
|
62
|
+
|
|
63
|
+
1. `examples/vscode/backend` freshly `npm install`ed; a new TypeScript project with one bad file (e.g. an undeclared variable and four-space indentation).
|
|
64
|
+
2. Connect → refresh diagnostics (errors appear) → apply quickfix (fix applied) → format document (indentation rewritten).
|
|
65
|
+
3. Record with your favorite gif tool (e.g. `peek` on Linux, ScreenToGif on Windows, Kap on macOS) and place it as `docs/editor-demo.gif`; the main README references it.
|
|
66
|
+
|
|
67
|
+
## Boundaries (read this)
|
|
68
|
+
|
|
69
|
+
- **UI only.** All LSP requests, quickfix selection, edit computation, and file writes happen in the backend. The extension sends `lsp.actions.run` and renders the structured results/events.
|
|
70
|
+
- **Official write policy.** `quickfix.apply` and `format` ride the backend plugin's `fs/write-intent` waterfall and the session's official permission preset. In this unsandboxed example composition they write directly; compose `@deepseek-ai/dsh-fs-sandbox` + `@deepseek-ai/dsh-sandbox-policy` + `@deepseek-ai/dsh-user-approval` to enforce presets (read-only sessions then refuse writes with `LSP_ACTION_READ_ONLY`, and escalations ask through the official approval path).
|
|
71
|
+
- **One stdio owner.** The backend's stdout carries only protocol frames — never add a stdout logger to `backend/cordis.yml`.
|
|
72
|
+
- **Protocol stability.** The wire is `lsp-actions/v1` (see [`docs/editor-protocol.md`](../../docs/editor-protocol.md)); clients must ignore unknown fields and event kinds.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# dsh-lsp-actions VS Code 示例
|
|
2
|
+
|
|
3
|
+
一个最小 VS Code 扩展,通过 ACP 风格的 JSON-RPC 消费 [`dsh-lsp-actions`](../../) 的**编辑器 action 协议**(`lsp.actions.list` / `lsp.actions.run` / `lsp.events`)。扩展**只做 UI**:不实现任何 LSP 逻辑、不计算诊断、不编辑缓冲区——所有能力与每一次写入都由插件后端完成。
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌──────────────────┐ stdio JSON-RPC(换行分帧) ┌───────────────────────────────┐
|
|
7
|
+
│ VS Code │ lsp.actions.list / lsp.actions.run │ 编辑器后端(headless DSH) │
|
|
8
|
+
│ 扩展(纯 UI) │ ────────────────────────────────────► │ dsh-lsp-actions │
|
|
9
|
+
│ 侧栏 + 命令 │ ◄──────────────────────────────────── │ (editor.enabled: true) │
|
|
10
|
+
│ │ lsp.event(诊断、动作状态…) │ fs-local · subprocess-local │
|
|
11
|
+
└──────────────────┘ │ LSP 服务器(tsls 等) │
|
|
12
|
+
└───────────────────────────────┘
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 侧栏提供什么
|
|
16
|
+
|
|
17
|
+
- **DSH Sessions** —— 后端运行时内的会话(`run` 可绑定权限预设的 `sessionId`)。
|
|
18
|
+
- **LSP Diagnostics** —— 后端推送的逐文件诊断(`diagnostics.updated` 事件),可按需刷新。每条诊断:
|
|
19
|
+
- 点击 → 在范围内打开文件(`openDiagnostic`,纯 VS Code UI);
|
|
20
|
+
- **Apply quickfix** 按钮 → 后端为该范围选择服务器首选 code action,并走官方写入策略应用(`quickfix.apply`)。扩展不碰缓冲区。
|
|
21
|
+
- **DSH: Format the active document** → 经后端执行 `format`(全文件、服务器验证过的编辑)。
|
|
22
|
+
|
|
23
|
+
## 安装与运行
|
|
24
|
+
|
|
25
|
+
### 1. 安装后端
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
cd examples/vscode/backend
|
|
29
|
+
npm install
|
|
30
|
+
# 确保 PATH 上有语言服务器,例如:
|
|
31
|
+
npm install -g typescript-language-server typescript
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. 启动扩展
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
cd examples/vscode/extension
|
|
38
|
+
code --new-window 你的/TypeScript/项目路径 .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- 在 VS Code 中:**Run and Debug → Run Extension**(`F5`),或先 `npx @vscode/vsce package` 再 **Extensions: Install from VSIX…**。
|
|
42
|
+
- 打开 **DSH LSP Actions** 活动栏容器(server-process 图标)。
|
|
43
|
+
|
|
44
|
+
### 3. 使用
|
|
45
|
+
|
|
46
|
+
1. 命令面板执行 **DSH: Connect to the editor backend** —— 启动 `node backend/bin.mjs backend/cordis.yml` 并读取 `lsp.actions.list`。
|
|
47
|
+
2. 打开一个带错误的 `.ts` 文件,点击 LSP Diagnostics 视图的 **↻ 刷新**按钮。
|
|
48
|
+
3. 展开诊断,点击 **Apply quickfix** —— 后端应用服务器修复并推送 `file.changed`;刷新后错误消失。
|
|
49
|
+
4. 执行 **DSH: Format the active document** —— 缩进由 `typescript-language-server` 经后端重写。
|
|
50
|
+
|
|
51
|
+
## 设置项
|
|
52
|
+
|
|
53
|
+
| 设置 | 默认值 | 含义 |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `dshLspActions.backend.node` | `node` | 启动后端的 Node 可执行文件。 |
|
|
56
|
+
| `dshLspActions.backend.bin` | (内置路径) | `backend/bin.mjs` 的绝对路径。 |
|
|
57
|
+
| `dshLspActions.backend.config` | (内置路径) | `backend/cordis.yml` 的绝对路径。 |
|
|
58
|
+
|
|
59
|
+
## 录制动图
|
|
60
|
+
|
|
61
|
+
主 README 中的演示动图按以下流程在干净 profile 上录制:
|
|
62
|
+
|
|
63
|
+
1. `examples/vscode/backend` 全新 `npm install`;新建一个 TypeScript 项目,放入一个坏文件(如未声明变量 + 四空格缩进)。
|
|
64
|
+
2. 连接 → 刷新诊断(错误出现)→ 应用 quickfix(修复生效)→ 格式化文档(缩进被重写)。
|
|
65
|
+
3. 用你顺手的动图工具(Linux `peek`、Windows ScreenToGif、macOS Kap)录制,命名为 `docs/editor-demo.gif`,主 README 引用它。
|
|
66
|
+
|
|
67
|
+
## 边界(请先阅读)
|
|
68
|
+
|
|
69
|
+
- **只做 UI。** 所有 LSP 请求、quickfix 选择、编辑计算与文件写入都发生在后端。扩展只发送 `lsp.actions.run` 并渲染结构化结果/事件。
|
|
70
|
+
- **官方写入策略。** `quickfix.apply` 与 `format` 走后端插件的 `fs/write-intent` 瀑布与会话的官方权限预设。本示例(未沙箱)组合中为直接写入;如需预设管控,组合 `@deepseek-ai/dsh-fs-sandbox` + `@deepseek-ai/dsh-sandbox-policy` + `@deepseek-ai/dsh-user-approval`(read-only 会话将以 `LSP_ACTION_READ_ONLY` 拒绝写操作,越级升级走官方审批)。
|
|
71
|
+
- **stdio 单一归属。** 后端 stdout 只承载协议帧——切勿给 `backend/cordis.yml` 添加 stdout 日志器。
|
|
72
|
+
- **协议稳定性。** 线上协议为 `lsp-actions/v1`(见 [`docs/editor-protocol.md`](../../docs/editor-protocol.md));客户端必须忽略未知字段与未知事件类型。
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The editor-backend bin: boots `cordis.yml` and keeps the process alive until
|
|
4
|
+
* stdin closes or a signal arrives. Stdout belongs to the editor action
|
|
5
|
+
* protocol — nothing in this composition may log to stdout.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node bin.mjs <path/to/cordis.yml>
|
|
8
|
+
* (or set DSH_CORDIS_CONFIG=<path>, which wins)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { boot, installFailLoud, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
|
13
|
+
|
|
14
|
+
const NAME = 'dsh-lsp-editor-backend'
|
|
15
|
+
installFailLoud(NAME)
|
|
16
|
+
|
|
17
|
+
const fromEnv = process.env.DSH_CORDIS_CONFIG
|
|
18
|
+
const fromArgv = process.argv[2]
|
|
19
|
+
const requested = fromEnv !== undefined && fromEnv !== '' ? fromEnv : fromArgv
|
|
20
|
+
if (requested === undefined || requested === '') {
|
|
21
|
+
process.stderr.write(`usage: ${NAME} <path/to/cordis.yml> (or set DSH_CORDIS_CONFIG=<path>, which wins); the config is required\n`)
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
const configPath = resolveConfigPath(requested, undefined)
|
|
25
|
+
if (!existsSync(configPath)) {
|
|
26
|
+
process.stderr.write(`${NAME}: configuration not found: ${configPath}\n`)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ctx = await boot(NAME, configPath)
|
|
31
|
+
let exiting = false
|
|
32
|
+
async function disposeAndExit(code) {
|
|
33
|
+
if (exiting) return
|
|
34
|
+
exiting = true
|
|
35
|
+
try {
|
|
36
|
+
await ctx.fiber.dispose()
|
|
37
|
+
} finally {
|
|
38
|
+
process.exit(code)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.stdin.on('end', () => { void disposeAndExit(0) })
|
|
43
|
+
process.on('SIGTERM', () => { void disposeAndExit(0) })
|
|
44
|
+
process.on('SIGINT', () => { void disposeAndExit(130) })
|