Extension Diagnostics @piex-dev/lsp

lsp — Language-Server Eyes for Agents

TL;DR

@piex-dev/lsp lets the agent ask IDE-class questions; more importantly, it auto-surfaces ERRORs after edit without relying on the model to remember to run diagnostics.

Overview

Without LSP, an agent understands code via read, grep, and the model guessing types/references. This breaks at scale: missed call sites after a signature change, type errors only found by running tsc/cargo check, "where is this defined" reduced to guessing paths. @piex-dev/lsp turns LSP into a Pi tool so the model can actively ask a language server before/after editing.

How it works

The most valuable capabilities: diagnostics (errors/warnings), definition/references (blast radius), hover (symbol type), symbols (structure outline), format (consistent style). It matches a server by file suffix in defaults.json, finds the workspace root via root markers, spawns a child process on stdio JSON-RPC, and caches per root+server within a session to avoid cold starts. Diagnostics come from server-pushed publishDiagnostics, cached by URI — so after an edit you can immediately ask "any red lines left?", closing an edit→verify→re-edit loop.

LSP flow
Editor/Agent          Language Server
     |                        |
     |  initialize            |
     |  textDocument/*        |
     | ---------------------> |
     |  publishDiagnostics    |
     | <--------------------- |

Usage

Install

bash
pi install npm:@piex-dev/lsp

Source: extensions/lsp

Prerequisites

The extension is a client, not a server distributor. The language server binary must be locally executable (e.g. typescript-language-server, rust-analyzer, pyright, gopls). defaults.json ships start commands and args for ~50 servers.

Configuration

Defaults live in extensions/lsp/defaults.json; each server has command / fileTypes / rootMarkers / initOptions / settings / isLinter. A single server's command can be overridden via PI_<NAME>_LSP_COMMAND; post-edit diagnostics can be disabled with PI_LSP_DIAGNOSTICS_ON_EDIT=0.

Verify

bash
cd extensions/lsp && npm install && bun test   # mock server unit tests
pi -e ./extensions/lsp/src/lsp.ts -p "what is 1+1" --no-session  # smoke

Implementation

lsp.ts is the client + router + tools + post-edit diagnostic hook; defaults.json configures ~50 servers. Post-edit diagnostics hook edit/write (incl. hashline): sync to disk → wait for publishDiagnostics → ERRORs only, capped 20 per file, appended to the result.

actionpurpose
diagnosticsmatch multiple servers, aggregate
definition/type_definition/implementationnavigation
references/hover/symbols/workspace_symbolsread intelligence
renamepreview by default; apply=true writes
code_actionslist or apply by index
formatTextEdit write-back
status/reloadops

Correctness: diagnostic settle (silent N ms after last publish, default 800ms); LSP 3.17 pull diagnostics (only if diagnosticProvider declared); resolveProvider gating; overlapping TextEdit intersection rejected; stderr captured into error messages (cap 16KB); which includes node_modules/.bin, .venv/bin; Windows .bat/.cmd wrapped via cmd.exe /d /s /c.

Design notes

ProjectMechanismpiex choice
oh-my-pi lspfull LSP client: multi-server routing, didChange, full action surface, diagnostic aggregationAdopted: JSON-RPC client, defaults.json-driven, lazy start/session reuse. Not adopted: Bun runtime, all-actions-at-once
OpenCode post-edit diagnosticstool_result hook on edit/write → wait publishDiagnostics → ERRORs only, per-file capAdopted the whole pattern: sync → wait → only ERROR → cap 20. PI_LSP_DIAGNOSTICS_ON_EDIT=0 disables it
VS Code LSPinitialize + settings/didChangeConfiguration + workspace/configurationBorrowed: initOptions/settings dispatch; full-text didChange; which checks .bin/.venv
pi-extensions pi-lspdiagnostic settle, pull diagnostics, stderr capture, gating, overlap detection, cmd.exe wrap, command overrideAdopted all protocol details (fused into resident-process architecture); not adopted: spawn-per-call, two-tools-only

Core trade-off: prioritize the post-edit ERROR loop (à la OpenCode), diagnostics over navigation exposure; linters don't hijack the primary server's navigation role.

Changelog

Roadmap

StatusItem
init/settings, didChange, multi-server, post-edit ERROR, rename/code_actions
diagnostic settle, pull diagnostics, resolveProvider gating, overlap guard, stderr capture
mock server unit tests
Nextproject-level .lsp.json override; module split (client/config/edits)
Nextindexing/ready state to avoid cold-start false negatives; directory-level batch diagnostics
Deferredlspmux, auto-download LS, completion, TUI, repo-wide CLI diagnostics

Version history

VersionDateChanges
0.2.02026-07-19Early version: multi-server routing, didChange, diagnostic aggregation; returned on first push; blindly called codeAction/resolve; server exits gave only exit code, stderr lost
0.3.02026-07-21push settle window + LSP 3.17 pull diagnostics dual-track; call resolveProvider/diagnosticProvider only when declared; stderr captured into timeout/exit errors; .bat/.cmd via cmd.exe wrap; PI_<NAME>_LSP_COMMAND override; overlapping TextEdit detection prevents file corruption

0.2.0 lesson: servers like intelephense push empty diagnostics first then real ones — returning on first push reported erroneous files as clean; server crashes gave only exit codes, making debugging guesswork. 0.3.0 fills in the protocol details.

Appendix: pi LSP ecosystem capability comparison

Objective capability differences across four projects (omp / OpenCode / pi-extensions pi-lsp / piex lsp).

CapabilityompOpenCodepi-extensionspiex
Action surface14 actionsexperimental, off by default2 (diagnostics+fix)13 actions
Process modelsession-reusesession-reusespawn-per-callsession-reuse
push settle window✅ (800ms)
LSP 3.17 pull diagnostics
Post-edit diagnosticswritethroughedit injects ERRORtool_result appends ERROR
stderr capture✅ (cap 16KB)
Overlapping TextEdit guard✅ (cwd-limited)
Windows .bat/.cmd✅ cmd.exe
rename / format✅ (preview default)
Unit tests✅ (mock server)