feat: hapus fitur LSP bawaan (language server protocol)

Hapus seluruh pipeline LSP (client, manager, provisioner, dan 7 tool
lsp_*) dari codebase:

- apps/infrastructure/src/lsp/ (client.rs, manager.rs, provisioner/*)
- apps/infrastructure/src/tools/lsp/ (connect, disconnect, diagnostics,
  hover, completion, definition, references)
- ToolCtx/ToolCtxBuilder: hapus field lsp_manager
- Daemon state: hapus lsp_manager, lsp_provision_msgs, shutdown_lsp
- Registry: hapus registrasi 7 tool lsp_*
- Settings: hapus lsp_auto_provision + lsp_languages
- Agent definitions: hapus lsp_* dari allowed tools coder/reviewer
- Cargo: hapus dependency lsp-types (workspace + infra)
- Update dokumentasi mod + arch_audit forbidden list

Verifikasi: cargo check/clippy/test semua hijau (54 test), tidak ada
referensi lsp_* tersisa di luar CHANGELOG.
This commit is contained in:
asepharyana
2026-08-27 22:38:21 +07:00
parent 9ea3d361b1
commit 93f3c2a357
30 changed files with 14 additions and 1009 deletions
Generated
+11 -45
View File
@@ -1088,15 +1088,6 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "fluent-uri"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
dependencies = [
"bitflags 1.3.2",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -1972,19 +1963,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lsp-types"
version = "0.97.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071"
dependencies = [
"bitflags 1.3.2",
"fluent-uri",
"serde",
"serde_json",
"serde_repr",
]
[[package]] [[package]]
name = "mac_address" name = "mac_address"
version = "1.1.8" version = "1.1.8"
@@ -3369,17 +3347,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@@ -4895,7 +4862,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-api" name = "zesdex-api"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -4918,7 +4885,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-application" name = "zesdex-application"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@@ -4935,7 +4902,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-bootstrap" name = "zesdex-bootstrap"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -4952,7 +4919,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-daemon" name = "zesdex-daemon"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@@ -4976,7 +4943,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-domain" name = "zesdex-domain"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@@ -4992,7 +4959,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-gateway" name = "zesdex-gateway"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -5019,7 +4986,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-grpc" name = "zesdex-grpc"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -5036,7 +5003,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-infrastructure" name = "zesdex-infrastructure"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -5055,7 +5022,6 @@ dependencies = [
"infer", "infer",
"jsonwebtoken", "jsonwebtoken",
"libc", "libc",
"lsp-types",
"nucleo-matcher", "nucleo-matcher",
"percent-encoding", "percent-encoding",
"pulldown-cmark", "pulldown-cmark",
@@ -5085,7 +5051,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-tui" name = "zesdex-tui"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@@ -5111,7 +5077,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-web" name = "zesdex-web"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -5131,7 +5097,7 @@ dependencies = [
[[package]] [[package]]
name = "zesdex-ws" name = "zesdex-ws"
version = "1.18.2" version = "1.18.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
-1
View File
@@ -61,7 +61,6 @@ ignore = "0.4"
nucleo-matcher = "0.3" nucleo-matcher = "0.3"
futures-util = "0.3" futures-util = "0.3"
rmcp = { version = "2.2", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "macros"] } rmcp = { version = "2.2", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "macros"] }
lsp-types = "0.97"
tiktoken-rs = "0.12" tiktoken-rs = "0.12"
similar = "3" similar = "3"
syntect = { version = "5", default-features = false, features = ["default-fancy"] } syntect = { version = "5", default-features = false, features = ["default-fancy"] }
-10
View File
@@ -46,10 +46,6 @@ pub struct SettingsPatch {
pub review_enabled: Option<bool>, pub review_enabled: Option<bool>,
/// Override the session-archive-enabled flag. /// Override the session-archive-enabled flag.
pub session_archive_enabled: Option<bool>, pub session_archive_enabled: Option<bool>,
/// Override the LSP auto-provision flag.
pub lsp_auto_provision: Option<bool>,
/// Override the list of LSP-managed languages.
pub lsp_languages: Option<Vec<String>>,
/// Override the hive-mind node timeout in milliseconds. /// Override the hive-mind node timeout in milliseconds.
pub hive_mind_node_timeout_ms: Option<u64>, pub hive_mind_node_timeout_ms: Option<u64>,
} }
@@ -111,12 +107,6 @@ impl SettingsPatch {
if let Some(val) = self.session_archive_enabled { if let Some(val) = self.session_archive_enabled {
settings.flags.session_archive_enabled = val; settings.flags.session_archive_enabled = val;
} }
if let Some(val) = self.lsp_auto_provision {
settings.flags.lsp_auto_provision = val;
}
if let Some(ref val) = self.lsp_languages {
settings.lsp_languages = val.clone();
}
if let Some(val) = self.hive_mind_node_timeout_ms { if let Some(val) = self.hive_mind_node_timeout_ms {
settings.hive_mind_node_timeout_ms = val; settings.hive_mind_node_timeout_ms = val;
} }
-6
View File
@@ -18,7 +18,6 @@
//! - `workflow_max_concurrency` — max parallel hive-mind nodes //! - `workflow_max_concurrency` — max parallel hive-mind nodes
//! - `hive_mind_node_timeout_ms` — per-node timeout for hive-mind orchestration //! - `hive_mind_node_timeout_ms` — per-node timeout for hive-mind orchestration
//! - `flags` — grouped boolean feature toggles //! - `flags` — grouped boolean feature toggles
//! - `lsp_languages` — list of language IDs for LSP auto-provisioning
use std::collections::HashMap; use std::collections::HashMap;
@@ -49,12 +48,10 @@ pub enum InternetMode {
/// ## Fields /// ## Fields
/// - `review_enabled` — enable automatic inline review after edits /// - `review_enabled` — enable automatic inline review after edits
/// - `session_archive_enabled` — enable periodic session archiving /// - `session_archive_enabled` — enable periodic session archiving
/// - `lsp_auto_provision` — auto-provision LSP language servers on project open
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsFlags { pub struct SettingsFlags {
pub review_enabled: bool, pub review_enabled: bool,
pub session_archive_enabled: bool, pub session_archive_enabled: bool,
pub lsp_auto_provision: bool,
} }
impl Default for SettingsFlags { impl Default for SettingsFlags {
@@ -63,7 +60,6 @@ impl Default for SettingsFlags {
Self { Self {
review_enabled: true, review_enabled: true,
session_archive_enabled: true, session_archive_enabled: true,
lsp_auto_provision: true,
} }
} }
} }
@@ -93,7 +89,6 @@ pub struct Settings {
pub workflow_max_concurrency: usize, pub workflow_max_concurrency: usize,
#[serde(flatten)] #[serde(flatten)]
pub flags: SettingsFlags, pub flags: SettingsFlags,
pub lsp_languages: Vec<String>,
#[serde(default = "default_hive_mind_node_timeout_ms")] #[serde(default = "default_hive_mind_node_timeout_ms")]
pub hive_mind_node_timeout_ms: u64, pub hive_mind_node_timeout_ms: u64,
} }
@@ -113,7 +108,6 @@ impl Default for Settings {
verify_timeout_ms: 30_000, verify_timeout_ms: 30_000,
workflow_max_concurrency: 5, workflow_max_concurrency: 5,
flags: SettingsFlags::default(), flags: SettingsFlags::default(),
lsp_languages: Vec::new(),
hive_mind_node_timeout_ms: 600_000, hive_mind_node_timeout_ms: 600_000,
} }
} }
+1 -1
View File
@@ -10,6 +10,6 @@ pub enum AccessTier {
Read, Read,
/// Read + Write: above plus write, edit, delete, git, memory. /// Read + Write: above plus write, edit, delete, git, memory.
Write, Write,
/// Full: above plus bash, shell, LSP, workflow, plan tools. /// Full: above plus bash, shell, workflow, plan tools.
Full, Full,
} }
-1
View File
@@ -32,7 +32,6 @@ ignore.workspace = true
nucleo-matcher.workspace = true nucleo-matcher.workspace = true
futures-util.workspace = true futures-util.workspace = true
rmcp.workspace = true rmcp.workspace = true
lsp-types.workspace = true
tiktoken-rs.workspace = true tiktoken-rs.workspace = true
similar.workspace = true similar.workspace = true
syntect.workspace = true syntect.workspace = true
@@ -125,7 +125,6 @@ fn forbidden_imports(layer: &str) -> &'static [&'static str] {
"argon2", "argon2",
"jsonwebtoken", "jsonwebtoken",
"rmcp", "rmcp",
"lsp_types",
"tiktoken_rs", "tiktoken_rs",
"syntect", "syntect",
"pulldown_cmark", "pulldown_cmark",
-2
View File
@@ -15,7 +15,6 @@
//! ├── auth/ — JWT, Argon2, OAuth loopback //! ├── auth/ — JWT, Argon2, OAuth loopback
//! ├── llm/ — LLM provider HTTP client //! ├── llm/ — LLM provider HTTP client
//! ├── ipc/ — Unix-socket IPC protocol //! ├── ipc/ — Unix-socket IPC protocol
//! ├── lsp/ — Native LSP client + provisioner
//! ├── mcp/ — Model Context Protocol bridge //! ├── mcp/ — Model Context Protocol bridge
//! ├── bgbash/ — Background bash job management //! ├── bgbash/ — Background bash job management
//! ├── tools/ — All 37 agent-invocable tools //! ├── tools/ — All 37 agent-invocable tools
@@ -32,7 +31,6 @@ pub mod bgbash;
pub mod guard; pub mod guard;
pub mod ipc; pub mod ipc;
pub mod llm; pub mod llm;
pub mod lsp;
pub mod mcp; pub mod mcp;
pub mod middleware; pub mod middleware;
pub mod persistence; pub mod persistence;
-132
View File
@@ -1,132 +0,0 @@
//! LSP client — sends JSON-RPC requests to language servers.
use anyhow::Result;
use serde_json::Value;
use std::io::{BufRead, BufReader, Read, Write};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::Mutex;
use tracing::{debug, info};
/// Mutable inner state of an LSP client, protected by a mutex so that
/// `send_request` and `shutdown` can be called via `&self` (required by
/// [`LspManager`](super::manager::LspManager)).
struct LspClientInner {
process: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
request_id: u64,
}
/// A minimal but functional LSP client.
pub struct LspClient {
inner: Mutex<LspClientInner>,
}
impl LspClient {
/// Spawn a language server process.
pub fn start(command: &str, args: &[String]) -> Result<Self> {
let mut child = Command::new(command)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdin = child
.stdin
.take()
.ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?;
let stdout = BufReader::new(
child
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?,
);
info!("LSP client spawned: {command}");
Ok(LspClient {
inner: Mutex::new(LspClientInner {
process: child,
stdin,
stdout,
request_id: 0,
}),
})
}
/// Send a JSON-RPC request and read the response.
pub fn send_request(&self, method: &str, params: &Value) -> Result<Value> {
let mut inner = match self.inner.lock() {
Ok(g) => g,
Err(poisoned) => {
tracing::error!("LSP client mutex poisoned, recovering");
poisoned.into_inner()
}
};
inner.request_id += 1;
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": inner.request_id,
"method": method,
"params": params.clone(),
});
// Write Content-Length header + body
let body = serde_json::to_string(&request)?;
let header = format!("Content-Length: {}\r\n\r\n", body.len());
inner.stdin.write_all(header.as_bytes())?;
inner.stdin.write_all(body.as_bytes())?;
inner.stdin.flush()?;
debug!("LSP request: {method} (id={})", inner.request_id);
// Read Content-Length header
let mut content_length = 0usize;
loop {
let mut line = String::new();
inner.stdout.read_line(&mut line)?;
let trimmed = line.trim();
if trimmed.is_empty() {
break; // end of headers
}
if let Some(len_str) = trimmed.strip_prefix("Content-Length: ") {
content_length = len_str.parse::<usize>()?;
}
}
// Read the JSON body
let mut buf = vec![0u8; content_length];
inner.stdout.read_exact(&mut buf)?;
let response: Value = serde_json::from_slice(&buf)?;
debug!("LSP response for {method}: response received");
Ok(response)
}
/// Gracefully shut down the server.
pub fn shutdown(&self) -> Result<()> {
let null = Value::Null;
if let Err(e) = self.send_request("shutdown", &null) {
tracing::warn!("LSP shutdown error: {e}");
}
if let Err(e) = self.send_request("exit", &null) {
tracing::warn!("LSP exit error: {e}");
}
if let Ok(mut inner) = self.inner.lock() {
let _ = inner.process.wait();
}
info!("LSP client shut down");
Ok(())
}
}
impl Drop for LspClient {
fn drop(&mut self) {
if let Ok(mut inner) = self.inner.lock() {
if let Err(e) = inner.process.kill() {
tracing::warn!("LSP process kill error: {e}");
}
let _ = inner.process.wait();
}
}
}
-55
View File
@@ -1,55 +0,0 @@
//! Manages multiple LSP server processes, keyed by language ID.
//!
//! Each language (e.g. "rust", "python") maps to one `LspClient`.
//! The manager provides a unified `request` method that dispatches
//! to the correct client by language.
use std::collections::HashMap;
use super::client::LspClient;
/// Manages one `LspClient` per language.
pub struct LspManager {
clients: HashMap<String, LspClient>,
}
impl LspManager {
pub fn new() -> Self {
LspManager {
clients: HashMap::new(),
}
}
}
impl Default for LspManager {
fn default() -> Self {
Self::new()
}
}
impl LspManager {
pub fn start(&mut self, language: &str, command: &str, args: &[String]) -> anyhow::Result<()> {
let client = LspClient::start(command, args)?;
self.clients.insert(language.to_string(), client);
Ok(())
}
pub fn get_client(&self, language: &str) -> Option<&LspClient> {
self.clients.get(language)
}
pub fn shutdown_all(&mut self) {
for client in self.clients.values() {
let _ = client.shutdown();
}
self.clients.clear();
}
pub fn languages(&self) -> Vec<String> {
self.clients.keys().cloned().collect()
}
pub fn is_empty(&self) -> bool {
self.clients.is_empty()
}
}
-6
View File
@@ -1,6 +0,0 @@
//! Native LSP client integration — manage language server processes and
//! dispatch requests for completion, hover, diagnostics, etc.
pub mod client;
pub mod manager;
pub mod provisioner;
@@ -1,16 +0,0 @@
//! Configuration for LSP language server provisioning.
use serde::{Deserialize, Serialize};
/// Describes how to provision a language server for a given language.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LspProvisionerConfig {
/// Language identifier, e.g. "rust", "python".
pub language: String,
/// The command to start the language server.
pub command: String,
/// Arguments for the command.
pub args: Vec<String>,
/// How to install the language server (if not found).
pub install_hint: Option<String>,
}
@@ -1,54 +0,0 @@
//! Discovers installed language servers on the system PATH.
use std::collections::HashMap;
use super::config::LspProvisionerConfig;
/// Known language server configurations keyed by language.
fn known_configs() -> HashMap<&'static str, (&'static str, Vec<&'static str>)> {
let mut m = HashMap::new();
m.insert("rust", ("rust-analyzer", vec![]));
m.insert("python", ("pyright-langserver", vec!["--stdio"]));
m.insert(
"typescript",
("typescript-language-server", vec!["--stdio"]),
);
m.insert(
"javascript",
("typescript-language-server", vec!["--stdio"]),
);
m.insert("go", ("gopls", vec![]));
m
}
/// Check if a command is available on PATH.
fn command_exists(cmd: &str) -> bool {
std::env::var_os("PATH")
.and_then(|path| {
std::env::split_paths(&path).find_map(|dir| {
let full_path = dir.join(cmd);
if full_path.is_file() {
Some(())
} else {
None
}
})
})
.is_some()
}
/// Discover which language servers are already on PATH.
pub fn discover_installed() -> Vec<LspProvisionerConfig> {
let mut configs = Vec::new();
for (lang, (cmd, args)) in known_configs() {
if command_exists(cmd) {
configs.push(LspProvisionerConfig {
language: lang.to_string(),
command: cmd.to_string(),
args: args.iter().map(|s| s.to_string()).collect(),
install_hint: None,
});
}
}
configs
}
@@ -1,38 +0,0 @@
//! Installs language servers (non-interactive, via package managers or
//! direct download).
/// Install a language server for the given language.
///
/// Returns a success message or an error describing why installation failed.
pub fn install_language_server(language: &str) -> anyhow::Result<String> {
match language {
"rust" => {
// rust-analyzer is typically installed via rustup
let output = std::process::Command::new("rustup")
.args(["component", "add", "rust-analyzer"])
.output()?;
if output.status.success() {
Ok("rust-analyzer installed via rustup".to_string())
} else {
anyhow::bail!(
"failed to install rust-analyzer: {}",
String::from_utf8_lossy(&output.stderr)
)
}
}
"python" => {
let output = std::process::Command::new("npm")
.args(["install", "-g", "pyright"])
.output()?;
if output.status.success() {
Ok("pyright installed via npm".to_string())
} else {
anyhow::bail!(
"failed to install pyright: {}",
String::from_utf8_lossy(&output.stderr)
)
}
}
lang => anyhow::bail!("no install method known for language '{lang}'"),
}
}
@@ -1,46 +0,0 @@
//! High-level manager that discovers, installs (if needed), and starts
//! LSP servers.
use super::discovery::discover_installed;
use super::install::install_language_server;
use crate::lsp::manager::LspManager;
/// Auto-provision language servers for the given list of languages.
///
/// Flow: discover already-installed servers → for each requested language
/// not yet available, attempt auto-install → start each server.
pub fn auto_provision(lsp_manager: &mut LspManager, languages: &[String]) -> Vec<String> {
let mut started = Vec::new();
let installed = discover_installed();
let mut installed_map: std::collections::HashMap<
&str,
&crate::lsp::provisioner::config::LspProvisionerConfig,
> = std::collections::HashMap::new();
for cfg in &installed {
installed_map.insert(cfg.language.as_str(), cfg);
}
for lang in languages {
if let Some(cfg) = installed_map.get(lang.as_str()) {
if lsp_manager.start(lang, &cfg.command, &cfg.args).is_ok() {
started.push(lang.clone());
}
} else {
// Not installed — try auto-install
if install_language_server(lang).is_ok() {
// Re-discover after install
let refreshed = discover_installed();
for cfg in refreshed {
if cfg.language == *lang {
if lsp_manager.start(lang, &cfg.command, &cfg.args).is_ok() {
started.push(lang.clone());
}
break;
}
}
}
}
}
started
}
@@ -1,7 +0,0 @@
//! LSP language server provisioner — discovers, installs, and manages
//! language server executables.
pub mod config;
pub mod discovery;
pub mod install;
pub mod manager;
@@ -63,13 +63,6 @@ pub fn tools_for(access: &AccessTier) -> Vec<Box<dyn Tool>> {
| "plan_enter" | "plan_enter"
| "plan_ready" | "plan_ready"
| "sequential_think" | "sequential_think"
| "lsp_connect"
| "lsp_disconnect"
| "lsp_hover"
| "lsp_completion"
| "lsp_definition"
| "lsp_references"
| "lsp_diagnostics"
) )
}) })
.collect(), .collect(),
-4
View File
@@ -16,7 +16,6 @@ pub struct ToolCtx {
pub mention_index: crate::MentionIndex, pub mention_index: crate::MentionIndex,
pub origin: crate::Origin, pub origin: crate::Origin,
pub graduated_checks: Vec<crate::tools::GraduatedCheck>, pub graduated_checks: Vec<crate::tools::GraduatedCheck>,
pub lsp_manager: Arc<Mutex<crate::lsp::manager::LspManager>>,
pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>, pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>, pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>,
pub abort_flag: Option<Arc<AtomicBool>>, pub abort_flag: Option<Arc<AtomicBool>>,
@@ -39,7 +38,6 @@ pub struct ToolCtxBuilder {
pub mention_index: crate::MentionIndex, pub mention_index: crate::MentionIndex,
pub origin: crate::Origin, pub origin: crate::Origin,
pub graduated_checks: Vec<crate::tools::GraduatedCheck>, pub graduated_checks: Vec<crate::tools::GraduatedCheck>,
pub lsp_manager: Arc<Mutex<crate::lsp::manager::LspManager>>,
pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>, pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>, pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>,
pub abort_flag: Option<Arc<AtomicBool>>, pub abort_flag: Option<Arc<AtomicBool>>,
@@ -56,7 +54,6 @@ impl Default for ToolCtxBuilder {
mention_index: crate::MentionIndex::new(), mention_index: crate::MentionIndex::new(),
origin: crate::Origin::Main, origin: crate::Origin::Main,
graduated_checks: Vec::new(), graduated_checks: Vec::new(),
lsp_manager: Arc::new(Mutex::new(crate::lsp::manager::LspManager::new())),
turn_events: None, turn_events: None,
workflow_findings: None, workflow_findings: None,
abort_flag: None, abort_flag: None,
@@ -98,7 +95,6 @@ impl ToolCtxBuilder {
mention_index: self.mention_index, mention_index: self.mention_index,
origin: self.origin, origin: self.origin,
graduated_checks: self.graduated_checks, graduated_checks: self.graduated_checks,
lsp_manager: self.lsp_manager,
turn_events: self.turn_events, turn_events: self.turn_events,
workflow_findings: self.workflow_findings, workflow_findings: self.workflow_findings,
abort_flag: self.abort_flag, abort_flag: self.abort_flag,
@@ -1,80 +0,0 @@
//! Get completion suggestions from LSP.
//!
//! Sends a `textDocument/completion` request to the connected language
//! server for a given file position.
use crate::tools::{Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
use tracing::{error, info, instrument};
/// Tool that requests code completion suggestions from an LSP server.
///
/// Flow: parse language/path/line/character → lock LSP manager → find client
/// → send `textDocument/completion` → return pretty-printed JSON response.
pub struct LspCompletion;
impl Tool for LspCompletion {
fn name(&self) -> &'static str {
"lsp_completion"
}
fn description(&self) -> &'static str {
"Get completion suggestions at a position"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"language": {
"type": "string",
"description": "Language identifier"
},
"path": {
"type": "string",
"description": "File path"
},
"line": {
"type": "integer",
"description": "Line number (0-based)"
},
"character": {
"type": "integer",
"description": "Character offset (0-based)"
}
},
"required": ["language", "path", "line", "character"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let language = crate::tools::arg_str(args, "language")?;
let path = crate::tools::arg_str(args, "path")?;
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
info!(language, path, line, character, "LSP completion requested");
let manager = match ctx.lsp_manager.lock() {
Ok(g) => g,
Err(poisoned) => {
error!("LSP manager mutex poisoned, recovering");
poisoned.into_inner()
}
};
if let Some(client) = manager.get_client(&language) {
let result = client.send_request(
"textDocument/completion",
&json!({
"textDocument": { "uri": format!("file://{}", path) },
"position": { "line": line, "character": character }
}),
)?;
Ok(serde_json::to_string_pretty(&result)?)
} else {
anyhow::bail!("no LSP client connected for '{language}'")
}
}
}
@@ -1,76 +0,0 @@
//! Connect to an LSP language server.
//!
//! Starts a new language server process and registers it in the
//! shared LSP manager for subsequent tool invocations.
use crate::tools::{Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
use tracing::{error, info, instrument};
/// Tool that connects to an LSP language server for a given language.
///
/// Flow: parse language + command + args → lock LSP manager → call
/// `manager.start()` → confirm connection in the response string.
pub struct LspConnect;
impl Tool for LspConnect {
fn name(&self) -> &'static str {
"lsp_connect"
}
fn description(&self) -> &'static str {
"Connect to an LSP language server for a given language"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"language": {
"type": "string",
"description": "Language identifier (e.g. 'rust', 'python')"
},
"command": {
"type": "string",
"description": "Command to start the language server"
},
"args": {
"type": "array",
"items": {"type": "string"},
"description": "Arguments for the language server command"
}
},
"required": ["language", "command"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let language = crate::tools::arg_str(args, "language")?;
let command = crate::tools::arg_str(args, "command")?;
let extra_args: Vec<String> = args
.get("args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
info!(language, command, extra_args = ?extra_args, "LSP connect requested");
let mut manager = match ctx.lsp_manager.lock() {
Ok(g) => g,
Err(poisoned) => {
error!("LSP manager mutex poisoned, recovering");
poisoned.into_inner()
}
};
manager.start(&language, &command, &extra_args)?;
info!(language, "LSP connected successfully");
Ok(format!("Connected LSP for '{language}'"))
}
}
@@ -1,81 +0,0 @@
//! Go-to-definition via LSP.
//!
//! Sends a `textDocument/definition` request to the connected language
//! server for a symbol at a given file position.
use crate::tools::{Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
use tracing::{error, info, instrument};
/// Tool that resolves a symbol's definition location via LSP.
///
/// Flow: parse language/path/line/character → lock LSP manager → find client
/// → send `textDocument/definition` → return pretty-printed JSON response
/// containing the target URI and range.
pub struct LspDefinition;
impl Tool for LspDefinition {
fn name(&self) -> &'static str {
"lsp_definition"
}
fn description(&self) -> &'static str {
"Go to definition for a symbol at a position"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"language": {
"type": "string",
"description": "Language identifier"
},
"path": {
"type": "string",
"description": "File path"
},
"line": {
"type": "integer",
"description": "Line number (0-based)"
},
"character": {
"type": "integer",
"description": "Character offset (0-based)"
}
},
"required": ["language", "path", "line", "character"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let language = crate::tools::arg_str(args, "language")?;
let path = crate::tools::arg_str(args, "path")?;
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
info!(language, path, line, character, "LSP definition requested");
let manager = match ctx.lsp_manager.lock() {
Ok(g) => g,
Err(poisoned) => {
error!("LSP manager mutex poisoned, recovering");
poisoned.into_inner()
}
};
if let Some(client) = manager.get_client(&language) {
let result = client.send_request(
"textDocument/definition",
&json!({
"textDocument": { "uri": format!("file://{}", path) },
"position": { "line": line, "character": character }
}),
)?;
Ok(serde_json::to_string_pretty(&result)?)
} else {
anyhow::bail!("no LSP client connected for '{language}'")
}
}
}
@@ -1,70 +0,0 @@
//! Get diagnostics from LSP.
//!
//! Sends a `textDocument/diagnostic` request to the connected language
//! server for a given file and returns errors, warnings, and other
//! diagnostics.
use crate::tools::{Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
use tracing::{error, info, instrument};
/// Tool that retrieves diagnostics (errors, warnings) from the LSP for a file.
///
/// Flow: parse language/path → lock LSP manager → find client
/// → send `textDocument/diagnostic` → return pretty-printed JSON response.
pub struct LspDiagnostics;
impl Tool for LspDiagnostics {
fn name(&self) -> &'static str {
"lsp_diagnostics"
}
fn description(&self) -> &'static str {
"Get diagnostics (errors, warnings) from the LSP for a file"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"language": {
"type": "string",
"description": "Language identifier"
},
"path": {
"type": "string",
"description": "File path to get diagnostics for"
}
},
"required": ["language", "path"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let language = crate::tools::arg_str(args, "language")?;
let path = crate::tools::arg_str(args, "path")?;
info!(language, path, "LSP diagnostics requested");
let manager = match ctx.lsp_manager.lock() {
Ok(g) => g,
Err(poisoned) => {
error!("LSP manager mutex poisoned, recovering");
poisoned.into_inner()
}
};
if let Some(client) = manager.get_client(&language) {
let result = client.send_request(
"textDocument/diagnostic",
&json!({
"textDocument": { "uri": format!("file://{}", path) }
}),
)?;
Ok(serde_json::to_string_pretty(&result)?)
} else {
anyhow::bail!("no LSP client connected for '{language}'")
}
}
}
@@ -1,54 +0,0 @@
//! Disconnect from an LSP language server.
//!
//! Removes the registered LSP client for a given language from
//! the shared LSP manager.
use crate::tools::{Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
use tracing::{error, info, instrument};
/// Tool that disconnects an LSP language server for a given language.
///
/// Flow: parse language → lock LSP manager → remove the client
/// for that language from the manager's registry.
pub struct LspDisconnect;
impl Tool for LspDisconnect {
fn name(&self) -> &'static str {
"lsp_disconnect"
}
fn description(&self) -> &'static str {
"Disconnect from an LSP language server"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"language": {
"type": "string",
"description": "Language identifier to disconnect"
}
},
"required": ["language"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let language = crate::tools::arg_str(args, "language")?;
info!(language, "LSP disconnect requested");
let _manager = match ctx.lsp_manager.lock() {
Ok(g) => g,
Err(poisoned) => {
error!("LSP manager mutex poisoned, recovering");
poisoned.into_inner()
}
};
info!(language, "LSP disconnected");
Ok(format!("Disconnected LSP for '{language}'"))
}
}
@@ -1,81 +0,0 @@
//! Get hover information from LSP.
//!
//! Sends a `textDocument/hover` request to the connected language
//! server for a symbol at a given file position.
use crate::tools::{Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
use tracing::{error, info, instrument};
/// Tool that retrieves hover information for a symbol at a position via LSP.
///
/// Flow: parse language/path/line/character → lock LSP manager → find client
/// → send `textDocument/hover` → return pretty-printed JSON response
/// containing the hover contents and range.
pub struct LspHover;
impl Tool for LspHover {
fn name(&self) -> &'static str {
"lsp_hover"
}
fn description(&self) -> &'static str {
"Get hover information for a symbol at a position"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"language": {
"type": "string",
"description": "Language identifier"
},
"path": {
"type": "string",
"description": "File path"
},
"line": {
"type": "integer",
"description": "Line number (0-based)"
},
"character": {
"type": "integer",
"description": "Character offset (0-based)"
}
},
"required": ["language", "path", "line", "character"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let language = crate::tools::arg_str(args, "language")?;
let path = crate::tools::arg_str(args, "path")?;
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
info!(language, path, line, character, "LSP hover requested");
let manager = match ctx.lsp_manager.lock() {
Ok(g) => g,
Err(poisoned) => {
error!("LSP manager mutex poisoned, recovering");
poisoned.into_inner()
}
};
if let Some(client) = manager.get_client(&language) {
let result = client.send_request(
"textDocument/hover",
&json!({
"textDocument": { "uri": format!("file://{}", path) },
"position": { "line": line, "character": character }
}),
)?;
Ok(serde_json::to_string_pretty(&result)?)
} else {
anyhow::bail!("no LSP client connected for '{language}'")
}
}
}
-18
View File
@@ -1,18 +0,0 @@
//! LSP tool implementations — connect, diagnostics, hover, completion,
//! definition, references, disconnect.
pub mod completion;
pub mod connect;
pub mod definition;
pub mod diagnostics;
pub mod disconnect;
pub mod hover;
pub mod references;
pub use completion::LspCompletion;
pub use connect::LspConnect;
pub use definition::LspDefinition;
pub use diagnostics::LspDiagnostics;
pub use disconnect::LspDisconnect;
pub use hover::LspHover;
pub use references::LspReferences;
@@ -1,81 +0,0 @@
//! Find references via LSP.
//!
//! Sends a `textDocument/references` request to the connected language
//! server for a symbol at a given file position.
use crate::tools::{Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
use tracing::{error, info, instrument};
/// Tool that finds all references to a symbol at a position via LSP.
///
/// Flow: parse language/path/line/character → lock LSP manager → find client
/// → send `textDocument/references` → return pretty-printed JSON response
/// containing all reference locations.
pub struct LspReferences;
impl Tool for LspReferences {
fn name(&self) -> &'static str {
"lsp_references"
}
fn description(&self) -> &'static str {
"Find all references to a symbol at a position"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"language": {
"type": "string",
"description": "Language identifier"
},
"path": {
"type": "string",
"description": "File path"
},
"line": {
"type": "integer",
"description": "Line number (0-based)"
},
"character": {
"type": "integer",
"description": "Character offset (0-based)"
}
},
"required": ["language", "path", "line", "character"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let language = crate::tools::arg_str(args, "language")?;
let path = crate::tools::arg_str(args, "path")?;
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(0);
let character = args.get("character").and_then(|v| v.as_i64()).unwrap_or(0);
info!(language, path, line, character, "LSP references requested");
let manager = match ctx.lsp_manager.lock() {
Ok(g) => g,
Err(poisoned) => {
error!("LSP manager mutex poisoned, recovering");
poisoned.into_inner()
}
};
if let Some(client) = manager.get_client(&language) {
let result = client.send_request(
"textDocument/references",
&json!({
"textDocument": { "uri": format!("file://{}", path) },
"position": { "line": line, "character": character }
}),
)?;
Ok(serde_json::to_string_pretty(&result)?)
} else {
anyhow::bail!("no LSP client connected for '{language}'")
}
}
}
-2
View File
@@ -18,7 +18,6 @@
//! ├── executor.rs — InfrastructureToolExecutor //! ├── executor.rs — InfrastructureToolExecutor
//! ├── fs/ — read, write, edit, delete //! ├── fs/ — read, write, edit, delete
//! ├── git/ — git_operator, git_worktree, git_cred //! ├── git/ — git_operator, git_worktree, git_cred
//! ├── lsp/ — connect, disconnect, diagnostics, completion, etc.
//! ├── memory/ — remember, forget, recall //! ├── memory/ — remember, forget, recall
//! ├── utility/ — cd, dir_list, pong, todowrite, todofinish, etc. //! ├── utility/ — cd, dir_list, pong, todowrite, todofinish, etc.
//! ├── shell.rs — Bash tool //! ├── shell.rs — Bash tool
@@ -41,7 +40,6 @@ pub mod executor;
pub mod fs; pub mod fs;
pub mod git; pub mod git;
pub mod graduated; pub mod graduated;
pub mod lsp;
pub mod memory; pub mod memory;
pub mod parallel_delegate; pub mod parallel_delegate;
pub mod plan; pub mod plan;
@@ -34,13 +34,6 @@ pub fn all_tools() -> Vec<Box<dyn super::Tool>> {
Box::new(super::utility::pong::Pong), Box::new(super::utility::pong::Pong),
Box::new(super::utility::todowrite::Todowrite), Box::new(super::utility::todowrite::Todowrite),
Box::new(super::utility::todofinish::Todofinish), Box::new(super::utility::todofinish::Todofinish),
Box::new(super::lsp::LspConnect),
Box::new(super::lsp::LspDiagnostics),
Box::new(super::lsp::LspHover),
Box::new(super::lsp::LspCompletion),
Box::new(super::lsp::LspDefinition),
Box::new(super::lsp::LspReferences),
Box::new(super::lsp::LspDisconnect),
Box::new(super::web_search::WebSearch), Box::new(super::web_search::WebSearch),
Box::new(super::semantic_search::SemanticSearch), Box::new(super::semantic_search::SemanticSearch),
Box::new(super::semantic_search::RebuildIndex), Box::new(super::semantic_search::RebuildIndex),
-14
View File
@@ -23,7 +23,6 @@ use zesdex_domain::SessionLockRepository;
use zesdex_domain::SessionRepository; use zesdex_domain::SessionRepository;
use zesdex_domain::Settings; use zesdex_domain::Settings;
use zesdex_domain::SettingsRepository; use zesdex_domain::SettingsRepository;
use zesdex_infrastructure::lsp::manager::LspManager;
use zesdex_infrastructure::mcp::manager::McpManager; use zesdex_infrastructure::mcp::manager::McpManager;
use zesdex_infrastructure::persistence::FileSystemSessionLockRepository; use zesdex_infrastructure::persistence::FileSystemSessionLockRepository;
use zesdex_infrastructure::persistence::JsonAppConfigRepository; use zesdex_infrastructure::persistence::JsonAppConfigRepository;
@@ -460,10 +459,6 @@ pub struct AppStateRest {
pub workflow_engine: WorkflowEngine, pub workflow_engine: WorkflowEngine,
/// MCP server manager. /// MCP server manager.
pub mcp_manager: McpManager, pub mcp_manager: McpManager,
/// LSP server manager, shared with tool context.
pub lsp_manager: Arc<Mutex<LspManager>>,
/// Shared queue for LSP provisioning messages.
pub lsp_provision_msgs: Arc<Mutex<VecDeque<String>>>,
/// Whether the state has been modified since the last render sweep. /// Whether the state has been modified since the last render sweep.
pub dirty: bool, pub dirty: bool,
/// Whether the application has been requested to quit. /// Whether the application has been requested to quit.
@@ -537,8 +532,6 @@ impl AppStateRest {
session_runtime: Some(SessionRuntime::new(session_dir.to_path_buf())), session_runtime: Some(SessionRuntime::new(session_dir.to_path_buf())),
workflow_engine: WorkflowEngine::new(), workflow_engine: WorkflowEngine::new(),
mcp_manager: McpManager::new(), mcp_manager: McpManager::new(),
lsp_provision_msgs: Arc::new(Mutex::new(VecDeque::new())),
lsp_manager: Arc::new(Mutex::new(LspManager::new())),
sessions: Vec::new(), sessions: Vec::new(),
transcript_cache: TranscriptCache::new(200), transcript_cache: TranscriptCache::new(200),
scroll: ScrollState::new(), scroll: ScrollState::new(),
@@ -624,13 +617,6 @@ impl AppStateRest {
) )
} }
/// Shut down every running LSP server process.
pub fn shutdown_lsp(&mut self) {
if let Ok(mut mgr) = self.lsp_manager.lock() {
mgr.shutdown_all();
}
}
/// Append a message to the transcript, evicting the oldest entry once /// Append a message to the transcript, evicting the oldest entry once
/// `max_lines` is exceeded. /// `max_lines` is exceeded.
pub fn push_transcript(&mut self, msg: ChatMessageDisplay) { pub fn push_transcript(&mut self, msg: ChatMessageDisplay) {
@@ -6,8 +6,8 @@
//! ## Available agents //! ## Available agents
//! | Agent | Purpose | Key tools | //! | Agent | Purpose | Key tools |
//! |-------|---------|-----------| //! |-------|---------|-----------|
//! | coder | Write/edit code | read, write, edit, bash, lsp_* | //! | coder | Write/edit code | read, write, edit, bash, git_operator |
//! | reviewer | Review code for correctness/safety | read, grep, lsp_diagnostics | //! | reviewer | Review code for correctness/safety | read, grep, recall |
//! | researcher | Search and summarise | read, grep, bash, search_web | //! | researcher | Search and summarise | read, grep, bash, search_web |
//! | planner | Break down tasks into steps | read, write, edit, bash, todo_* | //! | planner | Break down tasks into steps | read, write, edit, bash, todo_* |
@@ -81,13 +81,6 @@ pub fn builtin_agents() -> Vec<AgentDefinition> {
"grep".to_string(), "grep".to_string(),
"glob".to_string(), "glob".to_string(),
"git_operator".to_string(), "git_operator".to_string(),
"lsp_connect".to_string(),
"lsp_diagnostics".to_string(),
"lsp_hover".to_string(),
"lsp_definition".to_string(),
"lsp_references".to_string(),
"lsp_completion".to_string(),
"lsp_disconnect".to_string(),
]) ])
.with_max_steps(usize::MAX), .with_max_steps(usize::MAX),
AgentDefinition::new("reviewer".to_string(), "reviewer".to_string()) AgentDefinition::new("reviewer".to_string(), "reviewer".to_string())
@@ -101,10 +94,6 @@ pub fn builtin_agents() -> Vec<AgentDefinition> {
"glob".to_string(), "glob".to_string(),
"recall".to_string(), "recall".to_string(),
"remember".to_string(), "remember".to_string(),
"lsp_diagnostics".to_string(),
"lsp_hover".to_string(),
"lsp_definition".to_string(),
"lsp_references".to_string(),
]) ])
.with_max_steps(usize::MAX), .with_max_steps(usize::MAX),
AgentDefinition::new("researcher".to_string(), "researcher".to_string()) AgentDefinition::new("researcher".to_string(), "researcher".to_string())