返回 CodeWhale
lsp_hooks.rs
根目录 / crates / tui / src / core / engine / lsp_hooks.rs
1 //! Post-edit LSP diagnostics hooks for engine tool execution.
2 //!
3 //! The turn loop only needs to ask "did a successful edit produce diagnostics?"
4 //! This module owns the tool-input path extraction and the synthetic diagnostic
5 //! message injection so the top-level engine module stays focused on session
6 //! orchestration.
7
8 use std::path::PathBuf;
9
10 use crate::tools::apply_patch::preflight_apply_patch;
11
12 use super::*;
13
14 /// #136: derive the file path(s) edited by a tool call. Returns the empty
15 /// vec for tools that don't modify files. We intentionally only handle the
16 /// three known edit tools — adding more (e.g. specialized refactor tools)
17 /// is a one-line change here.
18 pub(super) fn edited_paths_for_tool(tool_name: &str, input: &serde_json::Value) -> Vec<PathBuf> {
19 match tool_name {
20 "edit_file" | "write_file" => {
21 if let Some(path) = input.get("path").and_then(|v| v.as_str()) {
22 vec![PathBuf::from(path)]
23 } else {
24 Vec::new()
25 }
26 }
27 "apply_patch" => preflight_apply_patch(input)
28 .map(|preflight| {
29 preflight
30 .touched_files
31 .into_iter()
32 .map(PathBuf::from)
33 .collect()
34 })
35 .unwrap_or_default(),
36 _ => Vec::new(),
37 }
38 }
39
40 impl Engine {
41 /// #136: post-edit hook. Inspects the tool name + input, derives the
42 /// edited file path, and asks the LSP manager for diagnostics. The
43 /// rendered block is queued in `pending_lsp_blocks` and flushed to the
44 /// session message stream just before the next API request. Failure is
45 /// silent by design — a missing/crashing LSP server must never block
46 /// the agent.
47 pub(super) async fn run_post_edit_lsp_hook(
48 &mut self,
49 tool_name: &str,
50 tool_input: &serde_json::Value,
51 ) {
52 if !self.lsp_manager.config().enabled {
53 return;
54 }
55 let paths = edited_paths_for_tool(tool_name, tool_input);
56 let mut found = 0usize;
57 let mut files = 0usize;
58 for path in paths {
59 let absolute = if path.is_absolute() {
60 path.clone()
61 } else {
62 self.session.workspace.join(&path)
63 };
64 // Use a short edit-sequence based on the existing turn counter so
65 // log output stays correlated even though we do not currently
66 // batch by sequence.
67 let seq = self.turn_counter;
68 if let Some(block) = self.lsp_manager.diagnostics_for(&absolute, seq).await {
69 found = found.saturating_add(block.items.len());
70 files = files.saturating_add(1);
71 self.pending_lsp_blocks.push(block);
72 }
73 }
74 if found > 0 {
75 let _ = self
76 .tx_event
77 .send(Event::LspRepairUpdate {
78 diagnostics_found: found,
79 files,
80 injected: false,
81 })
82 .await;
83 }
84 }
85
86 /// Drain `pending_lsp_blocks` into a single synthetic user message so the
87 /// model sees the diagnostics on its next request. Skips when nothing is
88 /// pending. The message uses the standard `text` content block shape
89 /// (the same shape as the post-tool steer messages) so we don't need to
90 /// invent a new envelope.
91 pub(super) async fn flush_pending_lsp_diagnostics(&mut self) {
92 if self.pending_lsp_blocks.is_empty() {
93 return;
94 }
95 let blocks = std::mem::take(&mut self.pending_lsp_blocks);
96 let found: usize = blocks.iter().map(|b| b.items.len()).sum();
97 let files = blocks.len();
98 let rendered = crate::lsp::render_blocks(&blocks);
99 if rendered.is_empty() {
100 return;
101 }
102 self.add_session_message(self.runtime_text_message_with_turn_metadata(
103 rendered,
104 crate::core::ops::UserInputProvenance::Runtime,
105 ))
106 .await;
107 let _ = self
108 .tx_event
109 .send(Event::LspRepairUpdate {
110 diagnostics_found: found,
111 files,
112 injected: true,
113 })
114 .await;
115 }
116 }
117
117 lines RUST