Move pickServer() into the try-catch in runLsp() so UnsupportedExtensionError is caught directly. Add message-based fallback in both runLsp() and runDiagnostics() to handle daemon-wrapped errors that come through as plain Error instances rather than the original typed exception. This eliminates spurious 'No LSP server registered' warnings during auto-check after edit/write on files without LSP support (e.g. .md, .txt, .sh).
evan/pi-lsp
LSP extension for pi coding agent. Provides LSP tools that the LLM can use to query language servers, plus automatic diagnostics after edit/write operations.
Features
LSP Tools (callable by LLM)
lsp_hover- Get hover documentation for a symbollsp_definition- Find the definition of a symbollsp_references- Find all references to a symbollsp_completion- Get completion suggestionslsp_documentSymbol- Get the symbol outline of a filelsp_diagnostics- Get lint/type-check diagnostics
Auto-Check
Automatically runs LSP diagnostics after edit or write tool calls. If issues are found, sends a message with the diagnostics to the LLM.
Enable/disable:
pi --lsp-auto-check=false # Disable auto-check
pi --lsp-auto-check=true # Enable (default)
Manual Check Command
Run diagnostics manually on specific files:
/lsp-check main.go utils.go
Server Control Commands
Disable a server so this pi instance won't use it (the shared daemon and other instances are unaffected). When all servers are disabled, LSP tools are removed from the active tool set.
| Command | Args | Behavior |
|---|---|---|
/lsp-servers |
none | List running servers and disabled state |
/lsp-disable |
[<id>] |
Disable all (no arg) or specific server. Bare command disables all. |
/lsp-enable |
[<id>] |
Enable all (no arg) or specific server. Restores tools when any is enabled. |
/lsp-destroy |
[<id>] |
Kill running daemon entries for all (no arg) or specific server. Explicitly destructive. |
/lsp-disable gopls # Disable just gopls; other LSP tools still work
/lsp-disable # Disable all — removes LSP tools from active set
/lsp-enable gopls # Re-enable gopls; restores tools
/lsp-enable # Re-enable all
/lsp-destroy gopls # Kill running gopls process(es) in the daemon
/lsp-destroy # Kill all running server processes
Install
cd ~/.pi/extensions/lsp
npm install
CLI Usage (for development/testing)
tsx ./cli.ts <file> <lsp_command> <req_data_json> [--no-daemon]
tsx ./cli.ts daemon <status|stop>
Requests use a long-lived background daemon by default. The daemon is
autospawned on first use, keeps one language server alive per
(server.id, project root), and evicts idle servers after
ServerConfig.idleTtlMs (default: 5 minutes). Pass --no-daemon to use the
legacy one-shot path for debugging.
req_data_json is the raw LSP params for the command, minus
textDocument.uri (we inject that from <file>).
Commands
hoverdefinitionreferencescompletiondocumentSymboldiagnostics(waits briefly for the firstpublishDiagnostics)
Examples
# Hover at line 224, col 23 (LSP is 0-indexed, so subtract 1)
npm run lsp -- backend/api/server.go hover \
'{"position":{"line":223,"character":22}}'
# Go to definition
npm run lsp -- backend/api/server.go definition \
'{"position":{"line":223,"character":22}}'
# Document symbols (no params needed)
npm run lsp -- backend/api/server.go documentSymbol '{}'
# Diagnostics
npm run lsp -- backend/api/server.go diagnostics '{}'
# Inspect/stop the background daemon
npm run lsp -- daemon status
npm run lsp -- daemon stop
Set LSP_DEBUG=1 to forward server stderr to the daemon log. The daemon
socket is $XDG_RUNTIME_DIR/pi-lsp-$UID.sock (tmpdir fallback); logs are in
/tmp/pi-lsp-daemon.log.
Adding A Server
Edit server.ts:
{
id: "rust-analyzer",
match: ["rs"],
command: "rust-analyzer",
args: [],
rootMarkers: ["Cargo.toml"],
languageId: "rust",
}
Adding A Command
- Add to the
LspCommandunion insrc/types.ts. - Add a handler in
src/commands.ts.
Future
- Daemon hardening - persistent metrics, health checks, and richer status output.
- Build output - ship compiled JS entrypoints instead of relying on tsx for development.