dsh-lsp-actions 0.2.0 → 0.3.1
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 +57 -0
- package/README.es.md +176 -129
- package/README.hi.md +176 -129
- package/README.md +170 -129
- package/README.pt.md +176 -129
- package/README.zh.md +224 -0
- 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/client.d.ts +3 -3
- 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 +67 -0
- package/lib/editor/service.d.ts.map +1 -0
- package/lib/editor/service.js +182 -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/edits.d.ts +1 -1
- package/lib/extra-tools.d.ts +3 -3
- package/lib/index.d.ts +17 -11
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +26 -1
- package/lib/index.js.map +1 -1
- package/lib/runner.d.ts +5 -5
- package/lib/seam.d.ts +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 +18 -5
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +2 -2
- package/lib/tools.js.map +1 -1
- package/lib/translate.d.ts +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 +61 -8
- package/README.zh-CN.md +0 -181
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# The DSH IDE integration backend for dsh-lsp-actions.
|
|
2
|
+
#
|
|
3
|
+
# A headless runtime whose stdout carries ONLY the editor action protocol
|
|
4
|
+
# (`lsp.actions.list` / `lsp.actions.run` / `lsp.events`, newline-delimited
|
|
5
|
+
# JSON-RPC 2.0). Do not add a stdout logger or any other stdout writer to this
|
|
6
|
+
# composition; diagnostics belong on stderr.
|
|
7
|
+
#
|
|
8
|
+
# This is the minimal spine the plugin needs: the tool registry, the system
|
|
9
|
+
# prompt service it requires, a filesystem backend, the fs observation policy
|
|
10
|
+
# (the `fs/write-intent` waterfall the write actions ride), and a local
|
|
11
|
+
# subprocess backend for language servers. No LLM adapter, no agent loop, no
|
|
12
|
+
# terminal UI — editors drive the LSP capabilities directly, with no model
|
|
13
|
+
# round-trip.
|
|
14
|
+
|
|
15
|
+
# The system prompt service the official tool registry injects.
|
|
16
|
+
- id: system-prompt
|
|
17
|
+
name: '@deepseek-ai/dsh-system-prompt'
|
|
18
|
+
|
|
19
|
+
# The official tool registry (provides ctx.tools; lsp-actions injects it).
|
|
20
|
+
- id: tools
|
|
21
|
+
name: '@deepseek-ai/dsh-tools'
|
|
22
|
+
|
|
23
|
+
# The local filesystem backend (provides ctx.fs).
|
|
24
|
+
- id: fs
|
|
25
|
+
name: '@deepseek-ai/dsh-fs-local'
|
|
26
|
+
|
|
27
|
+
# Read-before-edit observation: supplies the `fs/write-intent` waterfall the
|
|
28
|
+
# plugin's write actions (quickfix.apply / format) go through.
|
|
29
|
+
- id: fs-observation-policy
|
|
30
|
+
name: '@deepseek-ai/dsh-fs-observation-policy'
|
|
31
|
+
|
|
32
|
+
# Local child processes (provides ctx.subprocess) for language servers.
|
|
33
|
+
- id: subprocess
|
|
34
|
+
name: '@deepseek-ai/dsh-subprocess-local'
|
|
35
|
+
|
|
36
|
+
# The plugin: the eight model tools (unused here but always registered) plus
|
|
37
|
+
# the editor action protocol served over stdio. Add one server entry per
|
|
38
|
+
# language you want diagnostics/quickfixes/formatting for.
|
|
39
|
+
- id: lsp-actions
|
|
40
|
+
name: dsh-lsp-actions
|
|
41
|
+
inject: [tools, fs, subprocess]
|
|
42
|
+
config:
|
|
43
|
+
editor:
|
|
44
|
+
enabled: true
|
|
45
|
+
requestTimeoutMs: 60000
|
|
46
|
+
diagnosticsCacheMaxFiles: 64
|
|
47
|
+
servers:
|
|
48
|
+
ts:
|
|
49
|
+
command: typescript-language-server
|
|
50
|
+
args: [--stdio]
|
|
51
|
+
extensionToLanguage:
|
|
52
|
+
".ts": typescript
|
|
53
|
+
formattingOptions: { tabSize: 2, insertSpaces: true }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-lsp-editor-backend-example",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "The DSH IDE integration backend for dsh-lsp-actions: a headless runtime whose stdout carries only the editor action protocol (newline-delimited JSON-RPC).",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dsh-lsp-editor-backend": "./bin.mjs"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@deepseek-ai/dsh-app-boot": "^0.1.0-rc.5",
|
|
12
|
+
"@deepseek-ai/dsh-fs-local": "^0.1.0-rc.5",
|
|
13
|
+
"@deepseek-ai/dsh-fs-observation-policy": "^0.1.0-rc.5",
|
|
14
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.1.0-rc.5",
|
|
15
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.5",
|
|
16
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.5",
|
|
17
|
+
"dsh-lsp-actions": "^0.3.0"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-lsp-actions-vscode",
|
|
3
|
+
"displayName": "DSH LSP Actions (IDE integration example)",
|
|
4
|
+
"description": "Minimal VS Code extension consuming the dsh-lsp-actions editor action protocol over ACP-style JSON-RPC. UI only — every LSP capability is served by the plugin backend.",
|
|
5
|
+
"version": "0.3.0",
|
|
6
|
+
"publisher": "dsh",
|
|
7
|
+
"private": true,
|
|
8
|
+
"engines": {
|
|
9
|
+
"vscode": "^1.90.0"
|
|
10
|
+
},
|
|
11
|
+
"categories": [
|
|
12
|
+
"Linters",
|
|
13
|
+
"Other"
|
|
14
|
+
],
|
|
15
|
+
"main": "./src/extension.js",
|
|
16
|
+
"activationEvents": [],
|
|
17
|
+
"contributes": {
|
|
18
|
+
"commands": [
|
|
19
|
+
{
|
|
20
|
+
"command": "dshLspActions.connect",
|
|
21
|
+
"title": "DSH: Connect to the editor backend"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"command": "dshLspActions.disconnect",
|
|
25
|
+
"title": "DSH: Disconnect from the editor backend"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"command": "dshLspActions.refreshDiagnostics",
|
|
29
|
+
"title": "DSH: Refresh diagnostics for the active file",
|
|
30
|
+
"icon": "$(refresh)"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"command": "dshLspActions.applyQuickfix",
|
|
34
|
+
"title": "DSH: Apply this quickfix"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"command": "dshLspActions.openDiagnostic",
|
|
38
|
+
"title": "DSH: Open the file at this diagnostic"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"command": "dshLspActions.formatDocument",
|
|
42
|
+
"title": "DSH: Format the active document (through the backend)"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"viewsContainers": {
|
|
46
|
+
"activitybar": [
|
|
47
|
+
{
|
|
48
|
+
"id": "dsh-lsp-actions",
|
|
49
|
+
"title": "DSH LSP Actions",
|
|
50
|
+
"icon": "$(server-process)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"views": {
|
|
55
|
+
"dsh-lsp-actions": [
|
|
56
|
+
{
|
|
57
|
+
"id": "dshLspActions.sessions",
|
|
58
|
+
"name": "DSH Sessions"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "dshLspActions.diagnostics",
|
|
62
|
+
"name": "LSP Diagnostics"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"menus": {
|
|
67
|
+
"view/title": [
|
|
68
|
+
{
|
|
69
|
+
"command": "dshLspActions.refreshDiagnostics",
|
|
70
|
+
"when": "view == dshLspActions.diagnostics",
|
|
71
|
+
"group": "navigation"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"configuration": {
|
|
76
|
+
"title": "DSH LSP Actions",
|
|
77
|
+
"properties": {
|
|
78
|
+
"dshLspActions.backend.node": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"default": "node",
|
|
81
|
+
"description": "The Node.js executable used to launch the editor backend."
|
|
82
|
+
},
|
|
83
|
+
"dshLspActions.backend.bin": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"default": "",
|
|
86
|
+
"description": "Absolute path to the backend bin.mjs (examples/vscode/backend/bin.mjs). Empty falls back to the bundled example path resolution."
|
|
87
|
+
},
|
|
88
|
+
"dshLspActions.backend.config": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"default": "",
|
|
91
|
+
"description": "Absolute path to the backend cordis.yml. Empty falls back to the bin's default lookup."
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal JSON-RPC client over the editor backend's stdio: spawns the
|
|
3
|
+
* backend, frames newline-delimited JSON-RPC 2.0 requests, and re-emits
|
|
4
|
+
* `lsp.event` notifications as events. Pure transport — no LSP logic lives
|
|
5
|
+
* here; every capability is served by the dsh-lsp-actions backend.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict'
|
|
9
|
+
|
|
10
|
+
const { spawn } = require('node:child_process')
|
|
11
|
+
|
|
12
|
+
class BackendClient {
|
|
13
|
+
/**
|
|
14
|
+
* @param {{node: string, bin: string, config: string}} options
|
|
15
|
+
* @param {(line: string) => void} onStderr backend diagnostics land here
|
|
16
|
+
*/
|
|
17
|
+
constructor(options, onStderr) {
|
|
18
|
+
this.closed = false
|
|
19
|
+
this.nextId = 1
|
|
20
|
+
this.pending = new Map()
|
|
21
|
+
this.listeners = new Set()
|
|
22
|
+
this.buffer = ''
|
|
23
|
+
|
|
24
|
+
const args = [options.bin]
|
|
25
|
+
if (options.config) args.push(options.config)
|
|
26
|
+
const child = spawn(options.node, args, {
|
|
27
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
})
|
|
30
|
+
this.child = child
|
|
31
|
+
child.on('error', (error) => { this.failAll(error) })
|
|
32
|
+
child.on('close', () => { this.failAll(new Error('the editor backend exited')) })
|
|
33
|
+
child.stderr.on('data', (chunk) => { onStderr(String(chunk)) })
|
|
34
|
+
child.stdout.on('data', (chunk) => {
|
|
35
|
+
this.buffer += String(chunk)
|
|
36
|
+
this.drain()
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Send one request and resolve with its JSON-RPC result. */
|
|
41
|
+
request(method, params) {
|
|
42
|
+
if (this.closed) return Promise.reject(new Error('not connected'))
|
|
43
|
+
const id = String(this.nextId++)
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
this.pending.set(id, { resolve, reject })
|
|
46
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Send one notification (no response expected). */
|
|
51
|
+
notify(method, params) {
|
|
52
|
+
if (this.closed) return
|
|
53
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Subscribe to `lsp.event` payloads. */
|
|
57
|
+
onEvent(listener) {
|
|
58
|
+
this.listeners.add(listener)
|
|
59
|
+
return () => { this.listeners.delete(listener) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Terminate the backend. */
|
|
63
|
+
close() {
|
|
64
|
+
this.closed = true
|
|
65
|
+
this.failAll(new Error('connection closed'))
|
|
66
|
+
this.child.stdin.end()
|
|
67
|
+
this.child.kill()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
drain() {
|
|
71
|
+
for (;;) {
|
|
72
|
+
const newline = this.buffer.indexOf('\n')
|
|
73
|
+
if (newline < 0) break
|
|
74
|
+
const line = this.buffer.slice(0, newline).trim()
|
|
75
|
+
this.buffer = this.buffer.slice(newline + 1)
|
|
76
|
+
if (!line) continue
|
|
77
|
+
let frame
|
|
78
|
+
try {
|
|
79
|
+
frame = JSON.parse(line)
|
|
80
|
+
} catch {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if (typeof frame.id === 'string' && this.pending.has(frame.id)) {
|
|
84
|
+
const { resolve, reject } = this.pending.get(frame.id)
|
|
85
|
+
this.pending.delete(frame.id)
|
|
86
|
+
if (frame.error) reject(Object.assign(new Error(frame.error.message || 'JSON-RPC error'), { code: frame.error.code }))
|
|
87
|
+
else resolve(frame.result)
|
|
88
|
+
} else if (frame.method === 'lsp.event') {
|
|
89
|
+
for (const listener of this.listeners) listener(frame.params)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
failAll(error) {
|
|
95
|
+
for (const { reject } of this.pending.values()) reject(error)
|
|
96
|
+
this.pending.clear()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { BackendClient }
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH LSP Actions — minimal VS Code extension over the dsh-lsp-actions editor
|
|
3
|
+
* action protocol. The extension is UI-only: it connects to the backend over
|
|
4
|
+
* ACP-style JSON-RPC, lists sessions, shows diagnostics, and forwards
|
|
5
|
+
* quickfix/format intents. Every LSP capability — and every byte written —
|
|
6
|
+
* belongs to the backend plugin.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict'
|
|
10
|
+
|
|
11
|
+
const vscode = require('vscode')
|
|
12
|
+
const path = require('node:path')
|
|
13
|
+
const { BackendClient } = require('./client')
|
|
14
|
+
const { DiagnosticTreeProvider, SessionTreeProvider } = require('./views')
|
|
15
|
+
|
|
16
|
+
/** @type {BackendClient | undefined} */
|
|
17
|
+
let client
|
|
18
|
+
let sessions = []
|
|
19
|
+
let workspaceRoot = ''
|
|
20
|
+
const output = vscode.window.createOutputChannel('DSH LSP Actions')
|
|
21
|
+
const sessionTree = new SessionTreeProvider()
|
|
22
|
+
const diagnosticTree = new DiagnosticTreeProvider()
|
|
23
|
+
|
|
24
|
+
/** One action-status line per run, on the output channel (never stdout of the backend). */
|
|
25
|
+
function logStatus(event) {
|
|
26
|
+
const error = event.error ? ` — ${event.error.code}: ${event.error.message}` : ''
|
|
27
|
+
output.appendLine(`[${event.action}] ${event.status}${error}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Route one `lsp.event` payload to the UI. */
|
|
31
|
+
function handleEvent(event) {
|
|
32
|
+
switch (event.kind) {
|
|
33
|
+
case 'diagnostics.updated':
|
|
34
|
+
diagnosticTree.update(event.filePath, workspaceRoot, event.diagnostics)
|
|
35
|
+
break
|
|
36
|
+
case 'file.changed':
|
|
37
|
+
output.appendLine(`[file.changed] ${event.filePath} (diagnostics cache invalidated)`)
|
|
38
|
+
break
|
|
39
|
+
case 'action.status':
|
|
40
|
+
logStatus(event)
|
|
41
|
+
break
|
|
42
|
+
case 'sessions.changed':
|
|
43
|
+
sessions = event.sessions
|
|
44
|
+
sessionTree.setSessions(sessions)
|
|
45
|
+
break
|
|
46
|
+
default:
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The backend launch spec from settings. */
|
|
52
|
+
function backendOptions() {
|
|
53
|
+
const config = vscode.workspace.getConfiguration('dshLspActions.backend')
|
|
54
|
+
return {
|
|
55
|
+
node: config.get('node', 'node'),
|
|
56
|
+
bin: config.get('bin', '') || path.join(__dirname, '..', '..', 'backend', 'bin.mjs'),
|
|
57
|
+
config: config.get('config', '') || path.join(__dirname, '..', '..', 'backend', 'cordis.yml'),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Resolve the first workspace folder as the backend workspace root. */
|
|
62
|
+
function resolveWorkspaceRoot() {
|
|
63
|
+
const folder = vscode.workspace.workspaceFolders?.[0]
|
|
64
|
+
if (!folder) throw new Error('open a workspace folder first — the backend needs a workspace root')
|
|
65
|
+
return folder.uri.fsPath
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The filePath + workspaceRoot pair for the active editor. */
|
|
69
|
+
function activeFile() {
|
|
70
|
+
const editor = vscode.window.activeTextEditor
|
|
71
|
+
if (!editor) throw new Error('open a file in the editor first')
|
|
72
|
+
const root = resolveWorkspaceRoot()
|
|
73
|
+
const filePath = editor.document.uri.fsPath
|
|
74
|
+
if (!filePath.startsWith(root)) throw new Error(`the active file must live under the workspace root (${root})`)
|
|
75
|
+
return { filePath: path.relative(root, filePath), workspaceRoot: root }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function connect() {
|
|
79
|
+
if (client) {
|
|
80
|
+
vscode.window.showInformationMessage('DSH: already connected')
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
const options = backendOptions()
|
|
84
|
+
output.appendLine(`[connect] ${options.node} ${options.bin} ${options.config}`)
|
|
85
|
+
client = new BackendClient(options, (line) => { output.append(line) })
|
|
86
|
+
client.onEvent(handleEvent)
|
|
87
|
+
const list = await client.request('lsp.actions.list', {})
|
|
88
|
+
output.appendLine(`[list] protocol=${list.protocol} actions=${list.actions.map(entry => entry.action).join(',')}`)
|
|
89
|
+
sessions = list.sessions
|
|
90
|
+
sessionTree.setSessions(sessions)
|
|
91
|
+
client.notify('lsp.events', { subscribe: true })
|
|
92
|
+
vscode.window.showInformationMessage(`DSH: connected (${list.protocol})`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function disconnect() {
|
|
96
|
+
client?.close()
|
|
97
|
+
client = undefined
|
|
98
|
+
sessions = []
|
|
99
|
+
sessionTree.setSessions([])
|
|
100
|
+
diagnosticTree.clear()
|
|
101
|
+
output.appendLine('[disconnect]')
|
|
102
|
+
vscode.window.showInformationMessage('DSH: disconnected')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function run(action, params, title) {
|
|
106
|
+
if (!client) {
|
|
107
|
+
vscode.window.showWarningMessage('DSH: connect to the editor backend first')
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
const result = await client.request('lsp.actions.run', { action, params, requestId: `${action}:${Date.now()}` })
|
|
111
|
+
if (result.status === 'failed') {
|
|
112
|
+
vscode.window.showErrorMessage(`DSH ${title} failed: ${result.error.code} — ${result.error.message}`)
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
return result.result
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function refreshDiagnostics() {
|
|
119
|
+
try {
|
|
120
|
+
const { filePath, workspaceRoot: root } = activeFile()
|
|
121
|
+
workspaceRoot = root
|
|
122
|
+
const result = await run('diagnostics.get', { filePath, workspaceRoot: root }, 'diagnostics')
|
|
123
|
+
if (result) diagnosticTree.update(filePath, root, result.diagnostics)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
vscode.window.showErrorMessage(`DSH diagnostics: ${error.message}`)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function applyQuickfix(filePath, root, diagnostic) {
|
|
130
|
+
try {
|
|
131
|
+
const result = await run('quickfix.apply', {
|
|
132
|
+
filePath,
|
|
133
|
+
workspaceRoot: root,
|
|
134
|
+
range: diagnostic.range,
|
|
135
|
+
index: 0,
|
|
136
|
+
}, 'quickfix')
|
|
137
|
+
if (result && result.kind === 'quickfixApplied') {
|
|
138
|
+
vscode.window.showInformationMessage(`DSH: applied "${result.title}" to ${result.filesChanged} file(s)`)
|
|
139
|
+
void refreshDiagnostics()
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
vscode.window.showErrorMessage(`DSH quickfix: ${error.message}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function openDiagnostic(filePath, root, range) {
|
|
147
|
+
const uri = vscode.Uri.file(path.join(root, filePath))
|
|
148
|
+
const document = await vscode.workspace.openTextDocument(uri)
|
|
149
|
+
const editor = await vscode.window.showTextDocument(document, { preview: true })
|
|
150
|
+
const start = new vscode.Position(range.start.line, range.start.character)
|
|
151
|
+
const end = new vscode.Position(range.end.line, range.end.character)
|
|
152
|
+
editor.selection = new vscode.Selection(start, end)
|
|
153
|
+
editor.revealRange(new vscode.Range(start, end), vscode.TextEditorRevealType.InCenter)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function formatDocument() {
|
|
157
|
+
try {
|
|
158
|
+
const { filePath, workspaceRoot: root } = activeFile()
|
|
159
|
+
workspaceRoot = root
|
|
160
|
+
const result = await run('format', { filePath, workspaceRoot: root }, 'format')
|
|
161
|
+
if (result && result.kind === 'formatted') {
|
|
162
|
+
vscode.window.showInformationMessage(`DSH: formatted ${filePath} (${result.appliedEdits} edit(s))`)
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
vscode.window.showErrorMessage(`DSH format: ${error.message}`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function activate(context) {
|
|
170
|
+
vscode.window.registerTreeDataProvider('dshLspActions.sessions', sessionTree)
|
|
171
|
+
vscode.window.registerTreeDataProvider('dshLspActions.diagnostics', diagnosticTree)
|
|
172
|
+
context.subscriptions.push(
|
|
173
|
+
vscode.commands.registerCommand('dshLspActions.connect', () => { void connect() }),
|
|
174
|
+
vscode.commands.registerCommand('dshLspActions.disconnect', disconnect),
|
|
175
|
+
vscode.commands.registerCommand('dshLspActions.refreshDiagnostics', () => { void refreshDiagnostics() }),
|
|
176
|
+
vscode.commands.registerCommand('dshLspActions.applyQuickfix', (filePath, root, diagnostic) => { void applyQuickfix(filePath, root, diagnostic) }),
|
|
177
|
+
vscode.commands.registerCommand('dshLspActions.openDiagnostic', (filePath, root, range) => { void openDiagnostic(filePath, root, range) }),
|
|
178
|
+
vscode.commands.registerCommand('dshLspActions.formatDocument', () => { void formatDocument() }),
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function deactivate() {
|
|
183
|
+
client?.close()
|
|
184
|
+
client = undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { activate, deactivate }
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidebar tree providers: a DSH session list and a diagnostics list with
|
|
3
|
+
* quickfix actions. Both are pure presentation of backend state — the
|
|
4
|
+
* extension never computes diagnostics or applies edits itself.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict'
|
|
8
|
+
|
|
9
|
+
const vscode = require('vscode')
|
|
10
|
+
|
|
11
|
+
const SEVERITY_ICON = new Map([
|
|
12
|
+
[1, 'error'],
|
|
13
|
+
[2, 'warning'],
|
|
14
|
+
[3, 'info'],
|
|
15
|
+
[4, 'hint'],
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
/** The "DSH Sessions" view: sessionId + cwd + live marker. */
|
|
19
|
+
class SessionTreeProvider {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.sessions = []
|
|
22
|
+
this._onDidChange = new vscode.EventEmitter()
|
|
23
|
+
this.onDidChangeTreeData = this._onDidChange.event
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setSessions(sessions) {
|
|
27
|
+
this.sessions = sessions
|
|
28
|
+
this._onDidChange.fire()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getTreeItem(element) {
|
|
32
|
+
return element
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getChildren() {
|
|
36
|
+
if (this.sessions.length === 0) {
|
|
37
|
+
return [new vscode.TreeItem('No sessions in the backend runtime (connect first)', vscode.TreeItemCollapsibleState.None)]
|
|
38
|
+
}
|
|
39
|
+
return this.sessions.map((session) => {
|
|
40
|
+
const item = new vscode.TreeItem(`${session.sessionId}${session.live ? ' ●' : ''}`, vscode.TreeItemCollapsibleState.None)
|
|
41
|
+
item.description = session.cwd || '(no cwd)'
|
|
42
|
+
item.tooltip = session.live ? 'Live agent in this session' : 'No live agent'
|
|
43
|
+
item.iconPath = new vscode.ThemeIcon(session.live ? 'pulse' : 'circle-outline')
|
|
44
|
+
return item
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One file grouping in the diagnostics view. */
|
|
50
|
+
class FileItem extends vscode.TreeItem {
|
|
51
|
+
constructor(filePath, workspaceRoot, count) {
|
|
52
|
+
super(filePath, vscode.TreeItemCollapsibleState.Expanded)
|
|
53
|
+
this.description = `${count} diagnostic${count === 1 ? '' : 's'}`
|
|
54
|
+
this.iconPath = vscode.ThemeIcon.File
|
|
55
|
+
this.payload = { kind: 'file', filePath, workspaceRoot }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One diagnostic row; clicking it opens the file at the range. */
|
|
60
|
+
class DiagnosticItem extends vscode.TreeItem {
|
|
61
|
+
constructor(diagnostic, filePath, workspaceRoot) {
|
|
62
|
+
const where = `${diagnostic.range.start.line + 1}:${diagnostic.range.start.character + 1}`
|
|
63
|
+
super(diagnostic.message, vscode.TreeItemCollapsibleState.Collapsed)
|
|
64
|
+
this.description = `${where} — ${diagnostic.source || 'LSP'}`
|
|
65
|
+
this.iconPath = new vscode.ThemeIcon(SEVERITY_ICON.get(diagnostic.severity) || 'info')
|
|
66
|
+
this.command = {
|
|
67
|
+
command: 'dshLspActions.openDiagnostic',
|
|
68
|
+
title: 'Open file',
|
|
69
|
+
arguments: [filePath, workspaceRoot, diagnostic.range],
|
|
70
|
+
}
|
|
71
|
+
this.payload = { kind: 'diagnostic', filePath, workspaceRoot, diagnostic }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The "LSP Diagnostics" view. */
|
|
76
|
+
class DiagnosticTreeProvider {
|
|
77
|
+
constructor() {
|
|
78
|
+
this.files = new Map()
|
|
79
|
+
this._onDidChange = new vscode.EventEmitter()
|
|
80
|
+
this.onDidChangeTreeData = this._onDidChange.event
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Replace the diagnostics of one file (usually from a `diagnostics.updated` event). */
|
|
84
|
+
update(filePath, workspaceRoot, diagnostics) {
|
|
85
|
+
this.files.set(`${workspaceRoot}::${filePath}`, { filePath, workspaceRoot, diagnostics })
|
|
86
|
+
this._onDidChange.fire()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
clear() {
|
|
90
|
+
this.files.clear()
|
|
91
|
+
this._onDidChange.fire()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getTreeItem(element) {
|
|
95
|
+
return element
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getChildren(element) {
|
|
99
|
+
if (element === undefined) {
|
|
100
|
+
const rows = []
|
|
101
|
+
for (const { filePath, workspaceRoot, diagnostics } of this.files.values()) {
|
|
102
|
+
rows.push(new FileItem(filePath, workspaceRoot, diagnostics.length))
|
|
103
|
+
}
|
|
104
|
+
if (rows.length === 0) {
|
|
105
|
+
return [new vscode.TreeItem('No diagnostics yet — open a file and press the ↻ refresh button', vscode.TreeItemCollapsibleState.None)]
|
|
106
|
+
}
|
|
107
|
+
return rows
|
|
108
|
+
}
|
|
109
|
+
if (element.payload.kind === 'file') {
|
|
110
|
+
const entry = this.files.get(`${element.payload.workspaceRoot}::${element.payload.filePath}`)
|
|
111
|
+
return (entry?.diagnostics || []).map(diagnostic => new DiagnosticItem(diagnostic, entry.filePath, entry.workspaceRoot))
|
|
112
|
+
}
|
|
113
|
+
// One-click quickfix: the backend selects the server's preferred action for this
|
|
114
|
+
// diagnostic's range. Applying is the BACKEND's write (official write policy);
|
|
115
|
+
// this extension never edits the buffer.
|
|
116
|
+
const apply = new vscode.TreeItem('Apply quickfix', vscode.TreeItemCollapsibleState.None)
|
|
117
|
+
apply.iconPath = new vscode.ThemeIcon('lightbulb-autofix')
|
|
118
|
+
apply.command = {
|
|
119
|
+
command: 'dshLspActions.applyQuickfix',
|
|
120
|
+
title: 'Apply quickfix',
|
|
121
|
+
arguments: [element.payload.filePath, element.payload.workspaceRoot, element.payload.diagnostic],
|
|
122
|
+
}
|
|
123
|
+
return [apply]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { SessionTreeProvider, DiagnosticTreeProvider }
|
package/lib/client.d.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
-
import type { HostSource } from './host.
|
|
12
|
-
import type { LspCodeActionsResult, LspCompletionResult, LspDiagnosticsResult, LspEditsResult, LspInlayHintsResult, LspPosition, LspRange, LspRenameResult, LspSignaturesResult, LspSymbolsResult } from './vocabulary.
|
|
13
|
-
import type { ResolvedServer } from './servers.
|
|
11
|
+
import type { HostSource } from './host.js';
|
|
12
|
+
import type { LspCodeActionsResult, LspCompletionResult, LspDiagnosticsResult, LspEditsResult, LspInlayHintsResult, LspPosition, LspRange, LspRenameResult, LspSignaturesResult, LspSymbolsResult } from './vocabulary.js';
|
|
13
|
+
import type { ResolvedServer } from './servers.js';
|
|
14
14
|
/** One action call as the client receives it: everything the tool already knows. */
|
|
15
15
|
export interface ActionRequest {
|
|
16
16
|
/** The source file (relative to `workspaceRoot` or absolute). */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four editor-protocol actions (`diagnostics.get`, `completion.get`, `quickfix.apply`,
|
|
3
|
+
* `format`) over the same seam-first action runner the model tools use. Read-only actions are
|
|
4
|
+
* reference-only; the two write actions apply server-verified edits through the official
|
|
5
|
+
* `fs/write-intent` waterfall and the per-session sandbox policy — the same official permission
|
|
6
|
+
* presets and approval choreography as `lsp_format`/`lsp_rename`, with the editor's escalation
|
|
7
|
+
* pair (`sandbox_permissions` + `justification`) resolved through `approveEscalation`.
|
|
8
|
+
*
|
|
9
|
+
* Positions and ranges here are zero-based (LSP convention); the model tools keep one-based.
|
|
10
|
+
* @module dsh-lsp-actions/editor/actions
|
|
11
|
+
*/
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
import type { ActionRunner } from '../runner.js';
|
|
14
|
+
import type { FormatSandboxController } from '../sandbox.js';
|
|
15
|
+
import type { ResolvedConfig } from '../servers.js';
|
|
16
|
+
import type { LruDiagnosticsCache } from './cache.js';
|
|
17
|
+
import type { EditorActionDescriptor, EditorActionResult, EditorEvent } from './types.js';
|
|
18
|
+
/** The editor-facing agent shape (structurally, so no agent-package dependency). */
|
|
19
|
+
export interface EditorAgent {
|
|
20
|
+
readonly session: {
|
|
21
|
+
readonly id: unknown;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** The per-run context the service assembles: identity, cancellation, and the approval actor. */
|
|
25
|
+
export interface EditorRunContext {
|
|
26
|
+
readonly requestId: string;
|
|
27
|
+
readonly signal: AbortSignal;
|
|
28
|
+
/** The live agent of the addressed session, when the session exists and has one. */
|
|
29
|
+
readonly agent: EditorAgent | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** The dependencies one action call needs. */
|
|
32
|
+
export interface EditorActionDeps {
|
|
33
|
+
readonly ctx: Context;
|
|
34
|
+
readonly runner: ActionRunner;
|
|
35
|
+
readonly sandbox: FormatSandboxController;
|
|
36
|
+
readonly config: ResolvedConfig;
|
|
37
|
+
readonly cache: LruDiagnosticsCache;
|
|
38
|
+
readonly onEvent: (event: EditorEvent) => void;
|
|
39
|
+
}
|
|
40
|
+
/** The static v1 action catalog, in the order `lsp.actions.list` advertises it. */
|
|
41
|
+
export declare const EDITOR_ACTIONS: readonly EditorActionDescriptor[];
|
|
42
|
+
/** Dispatch one action id to its handler. */
|
|
43
|
+
export declare function runEditorAction(deps: EditorActionDeps, action: string, params: Record<string, unknown> | undefined, run: EditorRunContext): Promise<EditorActionResult>;
|
|
44
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/editor/actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAOlD,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,cAAc,CAAA;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAInD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,KAAK,EACV,sBAAsB,EACtB,kBAAkB,EAGlB,WAAW,EAGZ,MAAM,YAAY,CAAA;AAEnB,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAA;CAC3C;AAED,iGAAiG;AACjG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAAA;CACxC;AAED,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAA;IACzC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;IACnC,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;CAC/C;AAED,mFAAmF;AACnF,eAAO,MAAM,cAAc,EAAE,SAAS,sBAAsB,EAqB3D,CAAA;AAED,6CAA6C;AAC7C,wBAAgB,eAAe,CAC7B,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC3C,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,kBAAkB,CAAC,CAa7B"}
|