Problem
We have a `diagnostics` tool the model must explicitly call. In practice, it almost never calls it after edits — so the agent edits `crates/tui/src/foo.rs`, breaks the build, and finds out only when the user runs `cargo check` themselves and pastes the error back.
A human reading code sees the squiggle line under their cursor automatically. The agent has no equivalent. This is the single biggest "agent quality" gap between us and opencode.
What opencode does
`packages/opencode/src/lsp/{client,diagnostic,language}.ts` — opencode boots one or more LSP servers (TypeScript, Rust, Go, Python, etc., picked from a 70+ extension map) per workspace, listens for `textDocument/publishDiagnostics`, and after every edit waits up to 5 seconds for fresh reports.
Errors-only are formatted as a compact block:
<diagnostics file=\"crates/tui/src/foo.rs\">
ERROR [12:8] missing semicolon
ERROR [13:1] expected `,`, found `}`
</diagnostics>
Top 20 per file. Re-injected into the next turn as a synthetic system message so the model sees what compilation broke. The agent fixes it on the next turn instead of waiting for the user.
Plan
- New `crates/tui/src/lsp/` (~1200 LOC):
- `mod.rs` — `LspManager` keyed by language
- `client.rs` — wraps `tower-lsp` or rolls a thin JSON-RPC client over stdio
- `registry.rs` — fixed dictionary of `(language → server-binary, args)` for: `rust-analyzer`, `gopls`, `pyright`, `typescript-language-server`, `clangd`. (Plus a config-overridable map for niche languages.)
- `diagnostics.rs` — collect + format the `` block
- Hook into `crates/tui/src/core/engine.rs`:
- After every `edit_file` / `apply_patch` / `write_file` tool call, call `lsp_manager.poll_after_edit(file)` with a 5s timeout
- Attach the formatted diagnostics block as a synthetic system message before the next API call (or as a `tool_result` continuation — pick whichever fits the engine's existing message-shape better)
- Config:
[lsp]
enabled = true
poll_after_edit_ms = 5000
max_diagnostics_per_file = 20
include_warnings = false # errors-only by default to keep tokens down
servers = { rust = \"rust-analyzer\", python = \"pyright\" } # override registry
- Lifecycle: spin up servers lazily on first edit to a file in a new language; shut down with the session.
- Failure mode: if the LSP server isn't installed or crashes, log a one-time warning and fall back to no-op (don't block the agent's work).
Acceptance
- Edit `crates/tui/src/main.rs` to introduce a type error (e.g. `let x: i32 = "not a number";`)
- The next turn's request body contains a `<diagnostics file="…">` block listing the type error at the right line/col
- Disabling via `[lsp] enabled = false` removes it
- A snapshot test exercises the full path with a fixture LSP server (mock the LSP transport, not the network)
- If `rust-analyzer` isn't on PATH: one-time warning in the audit log; no agent disruption
Files
- `crates/tui/src/lsp/` (NEW)
- `crates/tui/src/core/engine.rs` (~30 LOC for the post-edit hook)
- `crates/tui/src/core/turn.rs` (~20 LOC for the synthetic-message attachment)
- `crates/config/src/lib.rs` (config schema)
Sizing
M-L — ~1200 LOC. Largest agent-quality lever in opencode.
Source
opencode `packages/opencode/src/lsp/{client,diagnostic,language}.ts`
Problem
We have a `diagnostics` tool the model must explicitly call. In practice, it almost never calls it after edits — so the agent edits `crates/tui/src/foo.rs`, breaks the build, and finds out only when the user runs `cargo check` themselves and pastes the error back.
A human reading code sees the squiggle line under their cursor automatically. The agent has no equivalent. This is the single biggest "agent quality" gap between us and opencode.
What opencode does
`packages/opencode/src/lsp/{client,diagnostic,language}.ts` — opencode boots one or more LSP servers (TypeScript, Rust, Go, Python, etc., picked from a 70+ extension map) per workspace, listens for `textDocument/publishDiagnostics`, and after every edit waits up to 5 seconds for fresh reports.
Errors-only are formatted as a compact block:
Top 20 per file. Re-injected into the next turn as a synthetic system message so the model sees what compilation broke. The agent fixes it on the next turn instead of waiting for the user.
Plan
Acceptance
Files
Sizing
M-L — ~1200 LOC. Largest agent-quality lever in opencode.
Source
opencode `packages/opencode/src/lsp/{client,diagnostic,language}.ts`