feat(tui): add usage overlay and sidebar for displaying usage statistics and tasks
feat(tui): implement status bar with connection and turn state indicators feat(tui): create workflow panel for agent status and progress visualization feat(web): introduce web frontend interface with static file serving feat(ws): add WebSocket interface for real-time communication and session management
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
//! 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().unwrap();
|
||||
let stdout = BufReader::new(child.stdout.take().unwrap());
|
||||
|
||||
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 = self.inner.lock().unwrap();
|
||||
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;
|
||||
let _ = self.send_request("shutdown", &null);
|
||||
let _ = self.send_request("exit", &null);
|
||||
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() {
|
||||
let _ = inner.process.kill();
|
||||
let _ = inner.process.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//! 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(),
|
||||
}
|
||||
}
|
||||
|
||||
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 (_lang, client) in &self.clients {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
//! 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;
|
||||
@@ -0,0 +1,16 @@
|
||||
//! 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>,
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//! 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
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! 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}'"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! High-level manager that discovers, installs (if needed), and starts
|
||||
//! LSP servers.
|
||||
|
||||
use crate::lsp::manager::LspManager;
|
||||
use super::discovery::discover_installed;
|
||||
use super::install::install_language_server;
|
||||
|
||||
/// 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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! LSP language server provisioner — discovers, installs, and manages
|
||||
//! language server executables.
|
||||
|
||||
pub mod config;
|
||||
pub mod discovery;
|
||||
pub mod install;
|
||||
pub mod manager;
|
||||
Reference in New Issue
Block a user