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.
Files changed (59) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.es.md +11 -0
  3. package/README.hi.md +11 -0
  4. package/README.md +108 -6
  5. package/README.pt.md +11 -0
  6. package/README.zh-CN.md +110 -6
  7. package/cordis.patch.yml +6 -0
  8. package/docs/editor-protocol.md +159 -0
  9. package/docs/editor-protocol.zh-CN.md +159 -0
  10. package/docs/seam-extension-notes.md +76 -0
  11. package/examples/vscode/README.md +72 -0
  12. package/examples/vscode/README.zh-CN.md +72 -0
  13. package/examples/vscode/backend/bin.mjs +44 -0
  14. package/examples/vscode/backend/cordis.yml +53 -0
  15. package/examples/vscode/backend/package.json +22 -0
  16. package/examples/vscode/extension/package.json +96 -0
  17. package/examples/vscode/extension/src/client.js +100 -0
  18. package/examples/vscode/extension/src/extension.js +187 -0
  19. package/examples/vscode/extension/src/views.js +127 -0
  20. package/lib/editor/actions.d.ts +44 -0
  21. package/lib/editor/actions.d.ts.map +1 -0
  22. package/lib/editor/actions.js +370 -0
  23. package/lib/editor/actions.js.map +1 -0
  24. package/lib/editor/cache.d.ts +51 -0
  25. package/lib/editor/cache.d.ts.map +1 -0
  26. package/lib/editor/cache.js +75 -0
  27. package/lib/editor/cache.js.map +1 -0
  28. package/lib/editor/server.d.ts +61 -0
  29. package/lib/editor/server.d.ts.map +1 -0
  30. package/lib/editor/server.js +97 -0
  31. package/lib/editor/server.js.map +1 -0
  32. package/lib/editor/service.d.ts +63 -0
  33. package/lib/editor/service.d.ts.map +1 -0
  34. package/lib/editor/service.js +181 -0
  35. package/lib/editor/service.js.map +1 -0
  36. package/lib/editor/transport.d.ts +53 -0
  37. package/lib/editor/transport.d.ts.map +1 -0
  38. package/lib/editor/transport.js +143 -0
  39. package/lib/editor/transport.js.map +1 -0
  40. package/lib/editor/types.d.ts +185 -0
  41. package/lib/editor/types.d.ts.map +1 -0
  42. package/lib/editor/types.js +13 -0
  43. package/lib/editor/types.js.map +1 -0
  44. package/lib/index.d.ts +6 -0
  45. package/lib/index.d.ts.map +1 -1
  46. package/lib/index.js +26 -1
  47. package/lib/index.js.map +1 -1
  48. package/lib/servers.d.ts +20 -1
  49. package/lib/servers.d.ts.map +1 -1
  50. package/lib/servers.js +6 -0
  51. package/lib/servers.js.map +1 -1
  52. package/lib/tools.d.ts +13 -0
  53. package/lib/tools.d.ts.map +1 -1
  54. package/lib/tools.js +2 -2
  55. package/lib/tools.js.map +1 -1
  56. package/lib/vocabulary.d.ts +3 -1
  57. package/lib/vocabulary.d.ts.map +1 -1
  58. package/lib/vocabulary.js.map +1 -1
  59. package/package.json +51 -3
@@ -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 }
@@ -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.ts';
14
+ import type { FormatSandboxController } from '../sandbox.ts';
15
+ import type { ResolvedConfig } from '../servers.ts';
16
+ import type { LruDiagnosticsCache } from './cache.ts';
17
+ import type { EditorActionDescriptor, EditorActionResult, EditorEvent } from './types.ts';
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"}