DeepSeek TUI LSP Subsystem: Inline Diagnostics After File Edits
DeepSeek TUI automatically injects compiler-style diagnostics into the LLM context immediately after file edits by extracting paths from tool calls, querying language servers via JSON-RPC, and rendering compact <diagnostics> blocks before the next model request.
The Hmbown/DeepSeek-TUI repository implements a sophisticated LSP subsystem that bridges autonomous code editing with real-time error detection. When the AI agent modifies files using tools like edit_file or write_file, the engine immediately queries language-specific LSP servers and surfaces diagnostics inline. This architecture enables the LLM to reason about compilation errors and warnings without manual inspection, keeping the agent contextually aware of code quality.
How the LSP Subsystem Captures File Edits
The engine identifies modified files by inspecting tool outputs rather than monitoring the filesystem. This approach ensures diagnostics are always synchronized with the agent's intent and avoids race conditions with disk I/O.
Extracting Edited Paths from Tool Calls
In crates/tui/src/core/engine/lsp_hooks.rs (lines 12-46), the function edited_paths_for_tool examines tool names and inputs to determine which files changed. It handles edit_file, write_file, and apply_patch tools, extracting absolute or workspace-relative paths for downstream LSP queries.
// Inside lsp_hooks.rs
let paths = edited_paths_for_tool(tool_name, tool_input);
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
} else {
self.session.workspace.join(&path)
};
if let Some(block) = self.lsp_manager.diagnostics_for(&absolute, seq).await {
self.pending_lsp_blocks.push(block);
}
}
The LspManager and Transport Layer
The LspManager defined in crates/tui/src/lsp/mod.rs (lines 1-33) provides a lazy, per-language abstraction over LSP server connections. It maintains transport instances, handles configuration from the [lsp] table in ~/.deepseek/config.toml, and manages server lifecycle to prevent resource waste.
StdioLspTransport and JSON-RPC Communication
In crates/tui/src/lsp/client.rs (lines 1-30), the StdioLspTransport struct implements the LspTransport trait to communicate with language servers over standard I/O. The transport sends didOpen and didChange notifications, waits for the configured poll_after_edit_ms duration, and parses publishDiagnostics notifications into structured data.
// 1. Agent issues an edit_file tool call
let edit_result = tool::edit_file("src/main.rs", new_contents).await;
// 2. Engine runs the post-edit hook automatically
engine.run_post_edit_lsp_hook("edit_file", &json!({ "path": "src/main.rs" })).await;
// 3. Before the next model call the engine flushes diagnostics
engine.flush_pending_lsp_diagnostics().await;
// 4. The LLM now receives a user message containing:
// <diagnostics file="src/main.rs">
// ERROR [23:5] unexpected token `}`
// </diagnostics>
Rendering Diagnostics for LLM Context
Raw LSP diagnostics undergo normalization before entering the conversation context to ensure the LLM receives structured, token-efficient feedback.
Normalizing LSP Output to DiagnosticBlock
The DiagnosticBlock struct in crates/tui/src/lsp/diagnostics.rs (lines 1-110) aggregates multiple Diagnostic entries into a renderable XML-like format. Errors are always included, while warnings depend on the include_warnings configuration flag. The system truncates output to max_diagnostics_per_file to prevent context window overflow.
let block = DiagnosticBlock {
file: PathBuf::from("src/main.rs"),
items: vec![
Diagnostic {
line: 12,
column: 8,
severity: Severity::Error,
message: "missing semicolon".into(),
},
],
};
assert_eq!(block.render(),
"<diagnostics file=\"src/main.rs\">\n ERROR [12:8] missing semicolon\n</diagnostics>");
Injecting Diagnostics into the Conversation
The engine uses a two-phase process to make diagnostics available to the LLM without blocking the agent's execution flow.
The Post-Edit Hook
After every successful edit, run_post_edit_lsp_hook in crates/tui/src/core/engine/lsp_hooks.rs (lines 73-101) iterates over edited paths and queues diagnostic blocks in pending_lsp_blocks. This hook runs immediately after tool execution, ensuring minimal latency between file modification and error detection.
Flushing Pending Diagnostics
Before the next OpenAI-compatible request, flush_pending_lsp_diagnostics (lines 105-127) converts queued blocks into synthetic user messages. According to the DeepSeek-TUI source code, this method injects all pending <diagnostics> blocks into the session, making compilation errors and warnings part of the model's immediate reasoning context.
Configuration and Best-Effort Design
The LSP subsystem operates on a best-effort basis: missing binaries, crashes, or timeouts are logged but never block the agent. This design keeps the automated editing pipeline resilient while providing enhanced context when language servers are available.
Configuration options in crates/tui/src/config.rs control enable/disable states, timeouts, and diagnostic limits via the [lsp] table in ~/.deepseek/config.toml. Users can tune poll_after_edit_ms for slower language servers or reduce max_diagnostics_per_file to conserve context window space.
Summary
- Path extraction happens via
edited_paths_for_toolincrates/tui/src/core/engine/lsp_hooks.rsforedit_file,write_file, andapply_patchtools - LspManager in
crates/tui/src/lsp/mod.rsprovides lazy, per-language server management with configurable timeouts - StdioLpsTransport in
crates/tui/src/lsp/client.rshandles JSON-RPC over stdio, sendingdidChangenotifications and parsingpublishDiagnostics - Diagnostic rendering uses
DiagnosticBlockincrates/tui/src/lsp/diagnostics.rsto produce compact XML-like blocks with severity filtering - Injection workflow queues diagnostics via
run_post_edit_lsp_hookand flushes them viaflush_pending_lsp_diagnosticsbefore the next model request - Best-effort architecture ensures LSP failures are logged but never block the agent's editing workflow
Frequently Asked Questions
How does DeepSeek TUI know which files to check for diagnostics after an edit?
The system extracts file paths from tool outputs using edited_paths_for_tool in crates/tui/src/core/engine/lsp_hooks.rs. It recognizes edit_file, write_file, and apply_patch tools, resolving relative paths against the workspace root to produce absolute paths for LSP queries. This ensures the diagnostics request always targets the correct file regardless of whether the tool used absolute or relative paths.
What happens if the LSP server crashes or times out?
The subsystem follows a best-effort design philosophy implemented across crates/tui/src/lsp/mod.rs and crates/tui/src/core/engine/lsp_hooks.rs. Timeouts, missing binaries, and server crashes are logged as warnings but never block the agent's execution. The engine continues without injecting diagnostics for that specific file, maintaining workflow continuity.
Can I configure which diagnostics appear in the LLM context?
Yes. The [lsp] table in ~/.deepseek/config.toml controls behavior through LspConfig defined in crates/tui/src/config.rs. You can enable or disable warnings with include_warnings, set max_diagnostics_per_file limits to prevent context overflow, and configure poll_after_edit_ms timeouts to accommodate slower language servers.
How are diagnostics formatted when injected into the conversation?
Diagnostics render as compact XML-like blocks using the render() method on DiagnosticBlock in crates/tui/src/lsp/diagnostics.rs. Each block includes the file path, line numbers, severity levels, and messages in a structured format the LLM can parse, such as ERROR [12:8] missing semicolon. The system wraps these in <diagnostics file="path"> tags to distinguish them from user input.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →