zagens-cli 0.8.1

Zagens headless CLI + HTTP/SSE runtime sidecar (`zagens`, `zagens-runtime` binaries)
Documentation
//! Post-edit LSP diagnostics hooks for engine tool execution.
//!
//! The turn loop only needs to ask "did a successful edit produce diagnostics?"
//! This module owns the synthetic diagnostic message injection so the top-level
//! engine module stays focused on session orchestration. Path extraction lives
//! in `zagens-core::engine::lsp_edit_paths`.
//!
//! M3 routes the two call sites through the
//! [`LspHost`](zagens_core::engine::hosts::LspHost) trait so the future
//! core-side Engine struct can hold `Arc<dyn LspHost>` instead of
//! `Arc<LspManager>`.

use zagens_core::engine::edited_paths_for_tool;
use zagens_core::engine::hosts::LspHost;

use super::*;

impl Engine {
    /// #136: post-edit hook. Inspects the tool name + input, derives the
    /// edited file path, and asks the LSP manager for diagnostics. The
    /// rendered block is queued in `pending_lsp_blocks` and flushed to the
    /// session message stream just before the next API request. Failure is
    /// silent by design — a missing/crashing LSP server must never block
    /// the agent.
    pub(super) async fn run_post_edit_lsp_hook(
        &mut self,
        tool_name: &str,
        tool_input: &serde_json::Value,
    ) {
        let host: &dyn LspHost = self.lsp.as_ref();
        if !host.enabled() {
            return;
        }
        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)
            };
            let seq = self.turn_counter;
            let block = {
                let host: &dyn LspHost = self.0.lsp.as_ref();
                host.diagnostics_for(&absolute, seq).await
            };
            if let Some(block) = block {
                self.0.pending_lsp_blocks.push(block);
            }
        }
    }

    /// Drain `pending_lsp_blocks` into a single synthetic user message so the
    /// model sees the diagnostics on its next request. Skips when nothing is
    /// pending. The message uses the standard `text` content block shape
    /// (the same shape as the post-tool steer messages) so we don't need to
    /// invent a new envelope.
    pub(super) async fn flush_pending_lsp_diagnostics(&mut self) {
        if self.effect_replay_anchor_only() {
            self.pending_lsp_blocks.clear();
            return;
        }
        if self.pending_lsp_blocks.is_empty() {
            return;
        }
        let blocks = std::mem::take(&mut self.pending_lsp_blocks);
        let rendered = crate::lsp::render_blocks(&blocks);
        if rendered.is_empty() {
            return;
        }
        self.add_session_message(Message {
            role: "user".to_string(),
            content: vec![ContentBlock::Text {
                text: rendered,
                cache_control: None,
            }],
        })
        .await;
    }
}