LSP action surface for DSH: diagnostics, formatting, completion, code actions, symbols, signature help, inlay hints and rename, all backed by real language servers.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-lsp-actions
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:PerryLink/dsh-lsp-actions
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).
README
🛰️ dsh-lsp-actions
The LSP action surface for DeepSeek Harness — real language servers, real feedback.
Diagnostics, formatting, code completion, quickfixes, symbols, signature help, inlay hints, and workspace-wide rename for your agent's editor loop, powered by the same language servers your IDE uses.
English · 简体中文 · Español · हिन्दी · Português
What this plugin gives your agent
The official DeepSeek Harness ctx.lsp seam covers navigation (go-to-definition, references, implementation, hover). dsh-lsp-actions completes the action surface — the feedback loop an agent needs while it writes and fixes code:
| Tool | What it does | Writes? |
|---|---|---|
lsp_diagnostics <file> |
Compiler/analyzer errors, warnings and hints with severity, range, message and source server | ❌ read-only |
lsp_format <file> [range?] |
Formats a file or selection through the language server and applies the result, returning the diff | ✅ via fs/write-intent + sandbox policy |
lsp_completion <file> <line> <character> |
Completion suggestions at a cursor position, including the actual insertion text | ❌ read-only |
lsp_code_action <file> [range?] [only?] |
Server-verified quickfixes/refactorings (with their edits) for a range or the first diagnostic | ❌ reference-only |
lsp_symbols <query?> <file_path?> |
Workspace-wide symbol search by name, or one file's symbol outline | ❌ read-only |
lsp_signature <file> <line> <character> |
Signature help (parameters and documentation) inside a call | ❌ read-only |
lsp_inlay_hints <file> [range?] |
Type annotations and parameter-name hints from the server | ❌ read-only |
lsp_rename <file> <line> <character> <new_name> |
Server-verified symbol rename, applied workspace-wide with per-file diffs | ✅ via fs/write-intent + sandbox policy |
✨ A real
typescript-language-serverrun is part of the test suite: diagnostics, formatting, completion, symbol search, and rename are verified end-to-end against a live server, not just mocks. The suite is self-contained (tsls is a devDependency) and runs in CI on Node 22/24 across Linux, Windows, and macOS.
Quick start
dsh plugin --profile <name> add dsh-lsp-actions
Remove it with:
dsh plugin --profile <name> remove dsh-lsp-actions
Configure one entry per language server (the shape mirrors the official lsp-stdio config):
# in your profile's cordis.patch.yml (or the bundle row)
- insert:
- id: lsp-actions
name: dsh-lsp-actions
inject: [tools, fs, subprocess]
config:
servers:
ts:
command: typescript-language-server
args: [--stdio]
extensionToLanguage:
".ts": typescript
formattingOptions: { tabSize: 2, insertSpaces: true }
py:
command: pyright-langserver
args: [--stdio]
extensionToLanguage:
".py": python
maxDiagnostics: 200
maxCompletionItems: 20
maxCodeActions: 50
maxSymbols: 100
maxSignatures: 10
maxInlayHints: 200
maxResultChars: 16000
timeoutMs: 60000
The eight tools are always registered. With an empty servers table and no ctx.lsp seam mounted, calls fail loudly with LSP_ACTION_UNAVAILABLE telling the user what to configure — the plugin never starts servers you did not configure. A ctx.lsp seam mounted after this plugin is picked up on the next call (seam detection is per-call, so load order does not matter).
Why it is safe by construction
- Formatting and rename are real mutations, treated like
write/edit. Every byte goes through thefs/write-intentwaterfall (observation → guarded write → observation) and the per-call sandbox policy.lsp_renamepre-flights every edited file (workspace containment, overlap check, byte-capped read) before the first write, so a bad server response cannot leave a half-applied rename. - Everything else is read-only by design. Code actions, completions, symbols, signatures, and hints are reported as reference material; applying them is the model's own write/edit decision. Command forms are reported and never executed.
- Read-only sessions fail loud, fast, and structured —
LSP_ACTION_READ_ONLYwith the shared[sandbox: …]marker, raised before any server round-trip. - Escalation matches the official tools. Under a confining filesystem,
lsp_formatandlsp_renameadvertise the samesandbox_permissions/justificationone-shot retry aswrite/edit, resolved throughctx.approval. - Conflicts never clobber. If the file changed on disk after it was read, the guarded write fails with
LSP_ACTION_CONFLICTand the model is told to choose: re-read and re-run, or apply the diff manually. - Timeouts are the platform's. Each tool declares
timeoutMs; the officialdsh-tool-call-timeout-policyenforces it, and every await honorsexec.signal. - Nothing is cached. Results live only in the session log; there is no cross-session persistence.
- Bad servers fail loudly. A missing executable fails at load; a server that dies at startup fails the call with
LSP_ACTION_SERVER_FAILEDplus its stderr tail (after one fresh-spawn retry).
Architecture
Actions run official-seam-first and fall back to the plugin's own minimal stdio client:
lsp_diagnostics / lsp_format / lsp_completion / lsp_code_action /
lsp_symbols / lsp_signature / lsp_inlay_hints / lsp_rename
│
▼
ctx.lsp seam (extended: diagnostics / formatDocument / completion)
│ absent · legacy · no provider for this file
▼
built-in stdio client ← servers table (ctx.subprocess.spawn + JSON-RPC)
The seam extension is proposed upstream (upstream/lsp-action-seam.patch, PR description in upstream/PR-description.md). Once it lands, the plugin keeps working unchanged — the built-in client simply stops being used. The built-in client stays as the standalone fallback for the servers table. Full research and design notes: docs/seam-extension-notes.md.
Configuration reference
interface Config {
/** Named language servers; empty = the plugin's own client serves nothing. */
servers?: Record<string, LspServerEntry>
maxDiagnostics?: number // default 200
maxCompletionItems?: number // default 20
maxCodeActions?: number // default 50
maxSymbols?: number // default 100
maxSignatures?: number // default 10
maxInlayHints?: number // default 200
maxResultChars?: number // default 16000 (complete rendered result cap)
maxDocumentBytes?: number // default 4000000
timeoutMs?: number // default 60000 (enforced by the official timeout policy)
}
interface LspServerEntry {
command: string // executable, resolved on PATH at load
extensionToLanguage: Record<string, string> // ".ts" → "typescript"
fileGlobs?: string[] // optional; glob matches beat the extension map
args?: string[] // no shell
env?: Record<string, string>
initializationOptions?: unknown
configuration?: unknown // object form answers workspace/configuration per section
formattingOptions?: unknown // e.g. { tabSize: 2, insertSpaces: true }
maxMessageBytes?: number // default 16000000
maxStderrBytes?: number // default 1000000
killGraceMs?: number // default 2000
shutdownTimeoutMs?: number // default 5000
diagnosticsSettleMs?: number // default 2000 (push-only diagnostics window)
diagnosticsDebounceMs?: number // default 250 (quiet period after the last pushed batch)
idleTimeoutMs?: number // default 0 (0 = keep the server process alive)
}
Error codes
Every failure carries a stable code on the error result; models and callers route on the code, never on message text.
| Code | Meaning |
|---|---|
LSP_ACTION_UNAVAILABLE |
No server entry and no seam provider handles this file. |
LSP_ACTION_UNSUPPORTED |
The server (or seam provider) does not advertise the operation. |
LSP_ACTION_SERVER_FAILED |
The server failed (with its stderr tail); startup failures retry once. |
LSP_ACTION_MALFORMED_RESPONSE |
The server sent a structurally invalid payload. |
LSP_ACTION_CONFLICT |
The file changed since it was read, or the server's edits overlap / go out of bounds / leave the workspace. |
LSP_ACTION_READ_ONLY |
The session's sandbox mode forbids the formatting/rename write. |
LSP_ACTION_WORKSPACE_REQUIRED |
The calling session has no workspace cwd to root the server in. |
LSP_ACTION_NO_SYMBOL |
The server found no renameable symbol at the cursor position. |
Host version support
The plugin declares its DeepSeek Harness packages as peer dependencies (@deepseek-ai/dsh-fs, dsh-llm, dsh-sandbox, dsh-subprocess, dsh-tools ≥ 0.1.0-rc.6), so one copy serves both the host and the plugin. Tested against 0.1.0-rc.6; last verified 2026-08-15.
Known limitations
- Transient documents. Every action opens the file, runs one request, and closes it again (matching the official stdio host). Project-based servers that require a resident open file for document-free requests (tsls refuses
workspace/symbolwithout one) are served by passingfile_pathtolsp_symbols, which keeps the routing file open for that request. tsls also answerstextDocument/signatureHelpwithnullunder this lifecycle; other servers (gopls, pyright, rust-analyzer) serve it normally. - Range formatting requires the server's range provider. Servers that only advertise whole-document formatting fail range requests with
LSP_ACTION_UNSUPPORTED. - Rename applies text edits only. Resource operations (create/delete/rename files) in a server's rename answer are refused with
LSP_ACTION_UNSUPPORTED, and edits outside the workspace fail asLSP_ACTION_CONFLICTbefore anything is written. Onutf-8/utf-32servers, cross-file rename positions are decoded by reading each edited file; an unreadable edited file fails the call as a conflict instead of mis-decoding positions.
Development
pnpm install
pnpm run lint # oxlint over src/ and tests/
pnpm test # 240+ tests: unit + fixture-server integration + real tsls e2e
pnpm run test:coverage # gates: lines/statements/functions ≥ 90%, branches ≥ 85%
pnpm build # emits lib/
Releasing
CI runs the lint/build/test matrix plus the coverage gate on every push and pull request. Pushing a v* tag triggers the publish workflow, which verifies the suite and publishes the package to npm — it needs an NPM_TOKEN Actions secret (a publish-scoped npm access token) set once on the repository. The version is bumped manually in package.json/CHANGELOG.md before tagging.
License
Links
More in this category
liustack/modlens★ 1582
Vision bridge for text-only models: paste an image, get structured JSON evidence (OCR, layout, semantics).
superdesigndev/treg★ 412
Tool catalog for agents: search ~2,600 external endpoints (SEO and SERP, backlinks, social, people and company enrichment, ad libraries, scraping) by the task you want done, read each one's parameters and per-call price, then call it with the credential injected server-side. Ships the skill plus an MCP row that stays disabled until TREG_TOKEN is set.
Anionex/dsh-vision-toolkit★ 395
Vision tasks for text-only models: intent-aware image Q&A, long-screenshot OCR, UI reproduction, grounding, and pixel diff.
zhaoolee/notes★ 141
Export DSH conversations as Smartisan Notes-style PNGs, or create and update Markdown notes in a configured account-scoped workspace.
Lum1104/dsh-browser★ 129
Chrome sidebar extension that lets DSH operate your browser directly, no vision capabilities required.
liustack/modsearch★ 101
Web search bridge for text-only agents: ask the web or X, get structured JSON evidence (search, fetch, citations).