diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index 0d885e0..6a780be 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -331,6 +331,24 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { let _ = crate::app::review::process_pending_lessons(&rt.session_dir, &state.memory_dir); } + // Drain LSP provision progress messages into toast notifications. + // Collect messages under the lock, then push toasts outside it to avoid + // a borrow-conflict with state.push_toast (which also accesses state). + let pending: Vec = state.lsp_provision_msgs.lock() + .ok() + .map(|mut q| q.drain(..).collect()) + .unwrap_or_default(); + for msg in &pending { + let kind = if msg.contains("not available") || msg.contains("failed") { + ToastKind::Warning + } else if msg.contains("connected") || msg.contains("✓") { + ToastKind::Success + } else { + ToastKind::Info + }; + state.push_toast(Toast::new(kind, msg.clone())); + } + let events: Vec = { if let Ok(mut q) = state.turn_events.lock() { q.drain(..).collect() diff --git a/src/app/state/rest.rs b/src/app/state/rest.rs index 927239f..d8a7f39 100644 --- a/src/app/state/rest.rs +++ b/src/app/state/rest.rs @@ -68,6 +68,9 @@ pub struct AppStateRest { pub workflow_engine: WorkflowEngine, pub mcp_manager: McpManager, pub lsp_manager: Arc>, + /// Shared queue: provisioner thread pushes status updates, + /// drained into toasts on each Tick. + pub lsp_provision_msgs: Arc>>, pub dirty: bool, pub quit: bool, } @@ -119,6 +122,7 @@ impl AppStateRest { session_runtime: Some(SessionRuntime::new(session_dir.clone())), workflow_engine: WorkflowEngine::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(), transcript_cache: TranscriptCache::new(200), @@ -145,19 +149,35 @@ impl AppStateRest { // logged rather than surfaced, since editing still works without LSP. if state.settings.lsp_auto_provision { let lsp_mgr = state.lsp_manager.clone(); + let msg_queue = state.lsp_provision_msgs.clone(); std::thread::spawn(move || { use crate::app::lsp::provisioner::{self, ProvisionResult}; + fn push_msg(q: &Arc>>, msg: String) { + if let Ok(mut q) = q.lock() { + q.push_back(msg); + } + } + + push_msg(&msg_queue, "LSP: provisioning servers...".to_string()); let results = provisioner::provision_all(); + push_msg(&msg_queue, "LSP: connecting servers...".to_string()); let connected = provisioner::auto_connect(&lsp_mgr, &results); for name in &connected { tracing::info!("LSP: {} connected", name); + push_msg(&msg_queue, format!("LSP: {} connected ✓", name)); } for r in &results { if let ProvisionResult::Failed { language, server_name, reason, .. } = r { tracing::warn!("LSP {} ({}): {}", server_name, language, reason); + push_msg(&msg_queue, format!("LSP: {} ({}) ✗ - {}", server_name, language, reason)); } } + if connected.is_empty() { + push_msg(&msg_queue, "LSP: no servers available — install manually or check prerequisites".to_string()); + } else { + push_msg(&msg_queue, format!("LSP: {} server(s) connected", connected.len())); + } }); }