feat(lsp): implement LSP client and server management

- Added LspClient for handling communication with LSP servers, including methods for initialization, notifications, and requests.
- Introduced LspManager to manage multiple LSP server connections, allowing for connection, disconnection, and retrieval of server capabilities.
- Created tools for connecting to LSP servers, retrieving diagnostics, hover information, code completion, definitions, and references.
- Enhanced UI rendering to display token usage and settings in the overlay.
- Updated status bar to show current token usage and selected provider/model.
This commit is contained in:
asepharyana
2026-07-12 13:40:58 +07:00
parent 8ad042139e
commit abc7a58e31
19 changed files with 1317 additions and 60 deletions
+56 -11
View File
@@ -81,6 +81,7 @@ pub enum Action {
},
ModelList,
AbortTurn,
Compact,
}
/// Apply an `Action` to the application state.
@@ -441,6 +442,8 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
if let Some(ref mut rt) = state.session_runtime {
rt.usage.tokens_in += tokens_in;
rt.usage.tokens_out += tokens_out;
rt.usage.last_tokens_in = tokens_in;
rt.usage.last_tokens_out = tokens_out;
rt.usage.api_calls += 1;
}
}
@@ -463,6 +466,13 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
state.misc.thinking = false;
turn_finished = true;
}
TurnEvent::Compacted(new_msgs) => {
if let Some(ref mut rt) = state.session_runtime {
rt.messages = new_msgs;
state.push_toast(Toast::new(ToastKind::Info, "History auto-compacted by AI.".to_string()));
state.dirty = true;
}
}
}
}
if turn_finished {
@@ -476,6 +486,23 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
state.abort_flag.store(true, std::sync::atomic::Ordering::SeqCst);
state.push_toast(Toast::new(ToastKind::Warning, "Aborting generation...".to_string()));
}
Action::Compact => {
let max_wire_tokens = state.app_config.model_roles.values()
.find(|role| role.provider == state.settings.provider && role.model == state.settings.model)
.and_then(|role| role.context_window)
.unwrap_or(state.app_config.default_context_window) as usize;
if let Some(ref mut rt) = state.session_runtime {
let total_chars: usize = rt.messages.iter()
.filter_map(|m| m.content.as_deref())
.map(|c| c.len())
.sum();
let token_estimate = total_chars / 4;
rt.messages = crate::app::runtime::shortsend::shape_messages(&rt.messages, token_estimate, max_wire_tokens, true, None);
state.push_toast(Toast::new(ToastKind::Success, "Conversation history compacted.".to_string()));
state.dirty = true;
}
}
Action::LessonAccept { name } => {
if let Some(ref rt) = state.session_runtime {
let _ = crate::app::review::resolve_pending_lesson(
@@ -533,6 +560,10 @@ fn spawn_turn(state: &AppStateRest) {
let model = state.settings.model.clone();
let base_url = state.app_config.providers.get(&state.settings.provider)
.map(|p| p.api_base.clone());
let context_window = state.app_config.model_roles.values()
.find(|role| role.provider == state.settings.provider && role.model == state.settings.model)
.and_then(|role| role.context_window)
.unwrap_or(state.app_config.default_context_window) as usize;
if api_key.is_empty() {
if let Some(provider_cfg) = state.app_config.providers.get(&state.settings.provider) {
api_key = provider_cfg.api_key_env.as_ref()
@@ -570,10 +601,11 @@ fn spawn_turn(state: &AppStateRest) {
.ok()
.map(|c| std::sync::Arc::new(std::sync::Mutex::new(c)));
let tc = TurnCtx {
client: crate::service::provider::LlmClient::new(api_key, model, base_url),
client: crate::service::provider::LlmClient::new(api_key, model.clone(), base_url),
tdefs: tool_defs,
tools,
ctx,
context_window,
workspace_roots,
edit_log_session_dir: edit_session_dir,
@@ -601,13 +633,14 @@ struct TurnCtx {
tdefs: Vec<crate::dto::provider::request::ToolDef>,
tools: Vec<Box<dyn crate::tool::Tool>>,
ctx: crate::tool::ToolCtx,
context_window: usize,
workspace_roots: Vec<std::path::PathBuf>,
edit_log_session_dir: std::path::PathBuf,
session_id: String,
db: Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>>,
temperature: f32,
max_tokens: u32,
max_tokens: Option<u32>,
abort_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
@@ -786,14 +819,26 @@ fn run_agent_turn(
}
loop {
let wire_msgs = if crate::app::runtime::shortsend::should_shape(msgs.len(), prev_shaped) {
let total_chars: usize = msgs.iter()
.filter_map(|m| m.content.as_deref())
.map(|c| c.len())
.sum();
let token_estimate = total_chars / 4;
let total_chars: usize = msgs.iter()
.filter_map(|m| m.content.as_deref())
.map(|c| c.len())
.sum();
let token_estimate = total_chars / 4;
let max_wire_tokens = tc.context_window;
let wire_msgs = if crate::app::runtime::shortsend::should_shape(token_estimate, max_wire_tokens, prev_shaped) {
prev_shaped = true;
crate::app::runtime::shortsend::shape_messages(&msgs, token_estimate)
let compacted = crate::app::runtime::shortsend::shape_messages(&msgs, token_estimate, max_wire_tokens, false, Some(&tc.client));
// Dispatch the compacted messages to the main thread so the local session history
// is permanently compacted and doesn't trigger shaping again immediately on next turn.
if let Ok(mut q) = events_q.lock() {
q.push_back(TurnEvent::Compacted(compacted.clone()));
}
// Also update our local `msgs` variable so the rest of the loop operates on the compacted version
msgs = compacted.clone();
compacted
} else {
prev_shaped = false;
msgs.clone()
@@ -805,9 +850,9 @@ fn run_agent_turn(
let mut usage = None;
let result = tc.client.chat_with_tools_streaming(
&wire_msgs,
Some(tc.tdefs.clone()),
if tc.tdefs.is_empty() { None } else { Some(tc.tdefs.clone()) },
Some(tc.temperature),
Some(tc.max_tokens),
tc.max_tokens,
|event| -> bool {
if tc.abort_flag.load(std::sync::atomic::Ordering::SeqCst) {
return false;