Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aec53651ed | ||
|
|
93f3c2a357 | ||
|
|
9ea3d361b1 | ||
|
|
271721694b |
@@ -1,3 +1,17 @@
|
|||||||
|
# [1.19.0](https://github.com/asepharyana/zesdex/compare/v1.18.4...v1.19.0) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* hapus fitur LSP bawaan (language server protocol) ([93f3c2a](https://github.com/asepharyana/zesdex/commit/93f3c2a3572511b5a84f244980ad71bd1b455e70))
|
||||||
|
|
||||||
|
## [1.18.4](https://github.com/asepharyana/zesdex/compare/v1.18.3...v1.18.4) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **tui:** render streaming token secara inkremental + kurangi redraw sia-sia ([2717216](https://github.com/asepharyana/zesdex/commit/271721694beb62e9fa5ff932312b293aa1d56823))
|
||||||
|
|
||||||
## [1.18.3](https://github.com/asepharyana/zesdex/compare/v1.18.2...v1.18.3) (2026-08-27)
|
## [1.18.3](https://github.com/asepharyana/zesdex/compare/v1.18.2...v1.18.3) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Generated
+11
-45
@@ -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
-2
@@ -15,7 +15,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.18.3"
|
version = "1.19.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["asepharyana <superaseph@gmail.com>"]
|
authors = ["asepharyana <superaseph@gmail.com>"]
|
||||||
|
|
||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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}'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -112,9 +112,12 @@ fn run_loop_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Drain expired toasts exactly once per loop iteration (was being
|
// Drain expired toasts exactly once per loop iteration (was being
|
||||||
// done here AND in Action::Tick before this fix).
|
// done here AND in Action::Tick before this fix). Skip the clock
|
||||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
// syscall + Vec scan when there are no toasts at all.
|
||||||
state.misc.drain_expired_toasts(now_ms);
|
if !state.misc.toasts.is_empty() {
|
||||||
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||||
|
state.misc.drain_expired_toasts(now_ms);
|
||||||
|
}
|
||||||
|
|
||||||
// Skip render when nothing has changed — avoids expensive markdown
|
// Skip render when nothing has changed — avoids expensive markdown
|
||||||
// re-parse and layout recalculation every cycle while idle.
|
// re-parse and layout recalculation every cycle while idle.
|
||||||
|
|||||||
@@ -179,16 +179,9 @@ impl MiscState {
|
|||||||
self.toasts.push(toast);
|
self.toasts.push(toast);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove and return all toasts whose lifetime has expired at `now_ms`.
|
/// Remove all toasts whose lifetime has expired at `now_ms`.
|
||||||
pub fn drain_expired_toasts(&mut self, now_ms: i64) -> Vec<Toast> {
|
pub fn drain_expired_toasts(&mut self, now_ms: i64) {
|
||||||
let expired: Vec<_> = self
|
|
||||||
.toasts
|
|
||||||
.iter()
|
|
||||||
.filter(|t| t.expired(now_ms))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
self.toasts.retain(|t| !t.expired(now_ms));
|
self.toasts.retain(|t| !t.expired(now_ms));
|
||||||
expired
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ pub struct AppStateRest {
|
|||||||
pub last_render_width: u16,
|
pub last_render_width: u16,
|
||||||
/// Number of messages that were in the cache when it was last built.
|
/// Number of messages that were in the cache when it was last built.
|
||||||
pub cached_msg_count: usize,
|
pub cached_msg_count: usize,
|
||||||
|
/// Cache index where the last message's rendered lines begin. Used to
|
||||||
|
/// splice streaming updates (only re-render the trailing message).
|
||||||
|
pub cached_last_start: usize,
|
||||||
|
/// Content+reasoning byte length of the last message when it was last
|
||||||
|
/// rendered. Guards the streaming branch from re-rendering on ticks
|
||||||
|
/// where no new token arrived (spinner-only frames).
|
||||||
|
pub cached_last_len: usize,
|
||||||
/// Terminal width at the time of the last full cache build.
|
/// Terminal width at the time of the last full cache build.
|
||||||
pub render_width_at_cache: u16,
|
pub render_width_at_cache: u16,
|
||||||
/// Atomic flag set when the user aborts the current turn.
|
/// Atomic flag set when the user aborts the current turn.
|
||||||
@@ -180,6 +187,8 @@ impl Default for AppStateRest {
|
|||||||
token_count_dirty: true,
|
token_count_dirty: true,
|
||||||
last_render_width: 0,
|
last_render_width: 0,
|
||||||
cached_msg_count: 0,
|
cached_msg_count: 0,
|
||||||
|
cached_last_start: 0,
|
||||||
|
cached_last_len: 0,
|
||||||
render_width_at_cache: 0,
|
render_width_at_cache: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,6 +239,8 @@ impl AppStateRest {
|
|||||||
token_count_dirty: true,
|
token_count_dirty: true,
|
||||||
last_render_width: 0,
|
last_render_width: 0,
|
||||||
cached_msg_count: 0,
|
cached_msg_count: 0,
|
||||||
|
cached_last_start: 0,
|
||||||
|
cached_last_len: 0,
|
||||||
render_width_at_cache: 0,
|
render_width_at_cache: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ impl Component for ChatComponent {
|
|||||||
}
|
}
|
||||||
state.display_lines_cache = all_lines;
|
state.display_lines_cache = all_lines;
|
||||||
state.cached_msg_count = msg_count;
|
state.cached_msg_count = msg_count;
|
||||||
|
state.cached_last_start = state.display_lines_cache.len();
|
||||||
|
state.cached_last_len = last_msg_len(state);
|
||||||
state.render_width_at_cache = state.last_render_width;
|
state.render_width_at_cache = state.last_render_width;
|
||||||
state.transcript_cache.dirty = false;
|
state.transcript_cache.dirty = false;
|
||||||
} else if state.transcript_cache.dirty && msg_count > cached_count {
|
} else if state.transcript_cache.dirty && msg_count > cached_count {
|
||||||
@@ -205,6 +207,27 @@ impl Component for ChatComponent {
|
|||||||
state.display_lines_cache.extend(msg_lines);
|
state.display_lines_cache.extend(msg_lines);
|
||||||
}
|
}
|
||||||
state.cached_msg_count = msg_count;
|
state.cached_msg_count = msg_count;
|
||||||
|
state.cached_last_start = state.display_lines_cache.len();
|
||||||
|
state.cached_last_len = last_msg_len(state);
|
||||||
|
state.transcript_cache.dirty = false;
|
||||||
|
} else if state.transcript_cache.dirty {
|
||||||
|
// Streaming update: message count unchanged, but the last
|
||||||
|
// assistant message's content grew (StreamToken). Re-render only
|
||||||
|
// that message and splice its lines back into the cache — this
|
||||||
|
// keeps updates O(new content), not O(whole history) per token.
|
||||||
|
// Without this branch, streamed tokens would never appear (the
|
||||||
|
// cache is only rebuilt on new-message or resize paths).
|
||||||
|
let cur_len = last_msg_len(state);
|
||||||
|
if cur_len != state.cached_last_len {
|
||||||
|
if let Some(last_msg) = state.transcript_cache.messages.back() {
|
||||||
|
if state.cached_last_start <= state.display_lines_cache.len() {
|
||||||
|
let new_lines = render_one_message(last_msg, content_width);
|
||||||
|
state.display_lines_cache.truncate(state.cached_last_start);
|
||||||
|
state.display_lines_cache.extend(new_lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.cached_last_len = cur_len;
|
||||||
|
}
|
||||||
state.transcript_cache.dirty = false;
|
state.transcript_cache.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,3 +349,15 @@ fn split_spans_into_lines(spans: Vec<Span<'_>>) -> Vec<Line<'_>> {
|
|||||||
}
|
}
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Byte length of the last message's content + reasoning. Used as a cheap
|
||||||
|
/// change detector for the streaming render branch — avoids re-rendering on
|
||||||
|
/// spinner-only ticks where no new token arrived.
|
||||||
|
fn last_msg_len(state: &crate::state::AppStateRest) -> usize {
|
||||||
|
state
|
||||||
|
.transcript_cache
|
||||||
|
.messages
|
||||||
|
.back()
|
||||||
|
.map(|m| m.content.len() + m.reasoning.len())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ pub fn draw(frame: &mut Frame, state: &AppStateRest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_toasts(frame: &mut Frame, state: &AppStateRest) {
|
fn render_toasts(frame: &mut Frame, state: &AppStateRest) {
|
||||||
|
// Fast path: nothing to render.
|
||||||
|
if state.misc.toasts.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||||
let active: Vec<&zesdex_infrastructure::Toast> = state
|
let active: Vec<&zesdex_infrastructure::Toast> = state
|
||||||
.misc
|
.misc
|
||||||
|
|||||||
Reference in New Issue
Block a user