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_tool in crates/tui/src/core/engine/lsp_hooks.rs for edit_file, write_file, and apply_patch tools
  • LspManager in crates/tui/src/lsp/mod.rs provides lazy, per-language server management with configurable timeouts
  • StdioLpsTransport in crates/tui/src/lsp/client.rs handles JSON-RPC over stdio, sending didChange notifications and parsing publishDiagnostics
  • Diagnostic rendering uses DiagnosticBlock in crates/tui/src/lsp/diagnostics.rs to produce compact XML-like blocks with severity filtering
  • Injection workflow queues diagnostics via run_post_edit_lsp_hook and flushes them via flush_pending_lsp_diagnostics before 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →