use crate::AppState;
use crate::protocol::BackendKind;
use crate::tool_defs::ToolSurface;
use crate::tool_runtime::{ToolResult, success_meta};
use serde_json::json;
#[cfg(feature = "semantic")]
use crate::tool_defs::is_tool_in_surface;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DiagnosticsStatus {
Available,
FilePathRequired,
UnsupportedExtension,
LspBinaryMissing,
}
impl DiagnosticsStatus {
pub(crate) fn status_key(&self) -> &'static str {
match self {
Self::Available => "available",
Self::FilePathRequired => "file_path_required",
Self::UnsupportedExtension => "unsupported_extension",
Self::LspBinaryMissing => "lsp_binary_missing",
}
}
pub(crate) fn is_available(&self) -> bool {
matches!(self, Self::Available)
}
}
#[derive(Debug, Clone)]
pub(crate) struct DiagnosticsGuidance {
status: DiagnosticsStatus,
file_extension: Option<String>,
language: Option<&'static str>,
lsp_command: Option<&'static str>,
server_name: Option<&'static str>,
install_command: Option<&'static str>,
package_manager: Option<&'static str>,
}
impl DiagnosticsGuidance {
fn for_file(file_path: Option<&str>) -> Self {
let extension = file_path.and_then(|path| {
std::path::Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
});
let recipe = extension
.as_deref()
.and_then(codelens_engine::get_lsp_recipe);
let status = match (file_path, recipe) {
(None, _) => DiagnosticsStatus::FilePathRequired,
(Some(_), None) => DiagnosticsStatus::UnsupportedExtension,
(Some(_), Some(recipe)) if !codelens_engine::lsp_binary_exists(recipe.binary_name) => {
DiagnosticsStatus::LspBinaryMissing
}
(Some(_), Some(_)) => DiagnosticsStatus::Available,
};
Self {
status,
file_extension: extension,
language: recipe.map(|recipe| recipe.language),
lsp_command: recipe.map(|recipe| recipe.binary_name),
server_name: recipe.map(|recipe| recipe.server_name),
install_command: recipe.map(|recipe| recipe.install_command),
package_manager: recipe.map(|recipe| recipe.package_manager),
}
}
fn reason_str(&self) -> Option<&'static str> {
match self.status {
DiagnosticsStatus::Available => None,
DiagnosticsStatus::FilePathRequired => Some(
"file_path required — provide a concrete source file so CodeLens can select an LSP recipe",
),
DiagnosticsStatus::UnsupportedExtension => Some(
"unsupported extension — no default LSP recipe is registered for this file type",
),
DiagnosticsStatus::LspBinaryMissing => Some(
"LSP binary missing — install the configured server or provide an explicit command",
),
}
}
fn reason_code(&self) -> Option<&'static str> {
match self.status {
DiagnosticsStatus::Available => None,
DiagnosticsStatus::FilePathRequired => Some("diagnostics_file_path_required"),
DiagnosticsStatus::UnsupportedExtension => Some("diagnostics_unsupported_extension"),
DiagnosticsStatus::LspBinaryMissing => Some("diagnostics_lsp_binary_missing"),
}
}
fn recommended_action(&self) -> Option<&'static str> {
match self.status {
DiagnosticsStatus::Available => None,
DiagnosticsStatus::FilePathRequired => Some("provide_file_path"),
DiagnosticsStatus::UnsupportedExtension => Some("pass_explicit_lsp_command"),
DiagnosticsStatus::LspBinaryMissing => Some("install_lsp_server"),
}
}
fn action_target(&self) -> Option<&'static str> {
match self.status {
DiagnosticsStatus::Available => None,
DiagnosticsStatus::FilePathRequired => Some("file_path"),
DiagnosticsStatus::UnsupportedExtension => Some("file_extension"),
DiagnosticsStatus::LspBinaryMissing => Some("lsp_server"),
}
}
fn guidance_payload(&self) -> serde_json::Value {
json!({
"status": self.status.status_key(),
"available": self.status.is_available(),
"reason": self.reason_str(),
"reason_code": self.reason_code(),
"recommended_action": self.recommended_action(),
"action_target": self.action_target(),
"file_extension": self.file_extension,
"language": self.language,
"lsp_command": self.lsp_command,
"server_name": self.server_name,
"install_command": self.install_command,
"package_manager": self.package_manager,
})
}
fn unavailable_payload(&self, feature: &str) -> serde_json::Value {
json!({
"feature": feature,
"reason": self.reason_str().unwrap_or("diagnostics available"),
"status": self.status.status_key(),
"reason_code": self.reason_code(),
"recommended_action": self.recommended_action(),
"action_target": self.action_target(),
"file_extension": self.file_extension,
"language": self.language,
"lsp_command": self.lsp_command,
"server_name": self.server_name,
"install_command": self.install_command,
"package_manager": self.package_manager,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SemanticSearchStatus {
#[cfg(feature = "semantic")]
Available,
#[cfg(feature = "semantic")]
ModelAssetsUnavailable,
#[cfg(feature = "semantic")]
NotInActiveSurface,
#[cfg(feature = "semantic")]
IndexMissing,
#[allow(dead_code)]
FeatureDisabled,
}
impl SemanticSearchStatus {
pub(crate) fn status_key(&self) -> &'static str {
match self {
#[cfg(feature = "semantic")]
Self::Available => "available",
#[cfg(feature = "semantic")]
Self::ModelAssetsUnavailable => "model_assets_unavailable",
#[cfg(feature = "semantic")]
Self::NotInActiveSurface => "not_in_active_surface",
#[cfg(feature = "semantic")]
Self::IndexMissing => "index_missing",
Self::FeatureDisabled => "feature_disabled",
}
}
pub(crate) fn reason_str(&self) -> Option<&'static str> {
match self {
#[cfg(feature = "semantic")]
Self::Available => None,
#[cfg(feature = "semantic")]
Self::ModelAssetsUnavailable => Some(
"model assets unavailable — reinstall with bundled model or set CODELENS_MODEL_DIR",
),
#[cfg(feature = "semantic")]
Self::NotInActiveSurface => Some(
"not in active surface — call set_profile/set_preset to include semantic_search",
),
#[cfg(feature = "semantic")]
Self::IndexMissing => {
Some("index missing — call index_embeddings to build the embedding index")
}
Self::FeatureDisabled => {
Some("feature disabled — rebuild with `cargo build --features semantic`")
}
}
}
pub(crate) fn reason_code(&self) -> Option<&'static str> {
match self {
#[cfg(feature = "semantic")]
Self::Available => None,
#[cfg(feature = "semantic")]
Self::ModelAssetsUnavailable => Some("semantic_model_assets_unavailable"),
#[cfg(feature = "semantic")]
Self::NotInActiveSurface => Some("semantic_not_in_active_surface"),
#[cfg(feature = "semantic")]
Self::IndexMissing => Some("semantic_index_missing"),
Self::FeatureDisabled => Some("semantic_feature_disabled"),
}
}
pub(crate) fn recommended_action(&self) -> Option<&'static str> {
match self {
#[cfg(feature = "semantic")]
Self::Available => None,
#[cfg(feature = "semantic")]
Self::ModelAssetsUnavailable => Some("configure_model_assets"),
#[cfg(feature = "semantic")]
Self::NotInActiveSurface => Some("switch_tool_surface"),
#[cfg(feature = "semantic")]
Self::IndexMissing => Some("run_index_embeddings"),
Self::FeatureDisabled => Some("rebuild_with_semantic_feature"),
}
}
pub(crate) fn action_target(&self) -> Option<&'static str> {
match self {
#[cfg(feature = "semantic")]
Self::Available => None,
#[cfg(feature = "semantic")]
Self::ModelAssetsUnavailable => Some("model_assets"),
#[cfg(feature = "semantic")]
Self::NotInActiveSurface => Some("tool_surface"),
#[cfg(feature = "semantic")]
Self::IndexMissing => Some("embedding_index"),
Self::FeatureDisabled => Some("binary"),
}
}
pub(crate) fn guidance_payload(&self) -> serde_json::Value {
json!({
"status": self.status_key(),
"available": self.is_available(),
"reason": self.reason_str(),
"reason_code": self.reason_code(),
"recommended_action": self.recommended_action(),
"action_target": self.action_target(),
})
}
pub(crate) fn is_available(&self) -> bool {
#[cfg(feature = "semantic")]
{
matches!(self, Self::Available)
}
#[cfg(not(feature = "semantic"))]
{
false
}
}
}
#[cfg(feature = "semantic")]
pub(crate) fn determine_semantic_search_status(
state: &AppState,
surface: ToolSurface,
) -> SemanticSearchStatus {
if !codelens_engine::embedding_model_assets_available() {
return SemanticSearchStatus::ModelAssetsUnavailable;
}
if !is_tool_in_surface("semantic_search", surface) {
return SemanticSearchStatus::NotInActiveSurface;
}
let indexed_count = {
let guard = state.embedding_ref();
match guard.as_ref() {
Some(engine) => engine.index_info().indexed_symbols,
None => codelens_engine::EmbeddingEngine::inspect_existing_index(&state.project())
.ok()
.flatten()
.map(|info| info.indexed_symbols)
.unwrap_or(0),
}
};
if indexed_count == 0 {
return SemanticSearchStatus::IndexMissing;
}
SemanticSearchStatus::Available
}
#[cfg(not(feature = "semantic"))]
pub(crate) fn determine_semantic_search_status(
_state: &AppState,
_surface: ToolSurface,
) -> SemanticSearchStatus {
SemanticSearchStatus::FeatureDisabled
}
pub(crate) fn build_health_summary(
index_stats: Option<&codelens_engine::IndexStats>,
semantic_status: &SemanticSearchStatus,
daemon_binary_drift: &serde_json::Value,
) -> serde_json::Value {
let indexed_files = index_stats.map(|s| s.indexed_files).unwrap_or(0);
let supported_files = index_stats.map(|s| s.supported_files).unwrap_or(0);
let stale_files = index_stats.map(|s| s.stale_files).unwrap_or(0);
let mut warnings = Vec::new();
let mut push_warning = |code: &str,
message: String,
recommended_action: Option<&str>,
action_target: Option<&str>| {
warnings.push(json!({
"code": code,
"severity": "warn",
"message": message,
"recommended_action": recommended_action,
"action_target": action_target,
}));
};
if supported_files == 0 {
push_warning(
"no_supported_files",
"no supported source files detected".to_string(),
None,
None,
);
}
if indexed_files == 0 {
push_warning(
"empty_index",
"symbol index is empty".to_string(),
Some("refresh_symbol_index"),
Some("symbol_index"),
);
}
if supported_files > 0 && indexed_files < supported_files {
push_warning(
"partial_index_coverage",
format!("index coverage incomplete ({indexed_files}/{supported_files})"),
Some("refresh_symbol_index"),
Some("symbol_index"),
);
}
if stale_files > 0 {
push_warning(
"stale_index",
format!("{stale_files} indexed files are stale"),
Some("refresh_symbol_index"),
Some("symbol_index"),
);
}
#[cfg(feature = "semantic")]
match semantic_status {
SemanticSearchStatus::ModelAssetsUnavailable | SemanticSearchStatus::IndexMissing => {
push_warning(
semantic_status
.reason_code()
.unwrap_or("semantic_unavailable"),
semantic_status
.reason_str()
.unwrap_or("semantic search unavailable")
.to_string(),
semantic_status.recommended_action(),
semantic_status.action_target(),
);
}
_ => {}
}
#[cfg(not(feature = "semantic"))]
if matches!(semantic_status, SemanticSearchStatus::FeatureDisabled) {
push_warning(
semantic_status
.reason_code()
.unwrap_or("semantic_feature_disabled"),
semantic_status
.reason_str()
.unwrap_or("semantic feature disabled")
.to_string(),
semantic_status.recommended_action(),
semantic_status.action_target(),
);
}
if daemon_binary_drift
.get("stale_daemon")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
push_warning(
daemon_binary_drift
.get("reason_code")
.and_then(|v| v.as_str())
.unwrap_or("stale_daemon"),
daemon_binary_drift
.get("reason")
.and_then(|v| v.as_str())
.unwrap_or("daemon binary drift detected")
.to_string(),
daemon_binary_drift
.get("recommended_action")
.and_then(|v| v.as_str()),
daemon_binary_drift
.get("action_target")
.and_then(|v| v.as_str()),
);
}
json!({
"status": if warnings.is_empty() { "ok" } else { "warn" },
"warning_count": warnings.len(),
"warnings": warnings,
})
}
#[derive(Debug, Clone)]
pub(crate) struct RuntimeHealthSnapshot {
pub(crate) index_stats: Option<codelens_engine::IndexStats>,
pub(crate) semantic_status: SemanticSearchStatus,
pub(crate) daemon_binary_drift: serde_json::Value,
pub(crate) health_summary: serde_json::Value,
}
impl RuntimeHealthSnapshot {
pub(crate) fn index_fresh(&self) -> bool {
self.index_stats
.as_ref()
.map(|stats| stats.stale_files == 0 && stats.indexed_files > 0)
.unwrap_or(false)
}
pub(crate) fn indexed_files(&self) -> usize {
self.index_stats
.as_ref()
.map(|stats| stats.indexed_files)
.unwrap_or(0)
}
pub(crate) fn supported_files(&self) -> usize {
self.index_stats
.as_ref()
.map(|stats| stats.supported_files)
.unwrap_or(0)
}
pub(crate) fn stale_files(&self) -> usize {
self.index_stats
.as_ref()
.map(|stats| stats.stale_files)
.unwrap_or(0)
}
}
pub(crate) fn collect_runtime_health_snapshot(
state: &AppState,
surface: ToolSurface,
) -> RuntimeHealthSnapshot {
let index_stats = state.symbol_index().stats().ok();
let semantic_status = determine_semantic_search_status(state, surface);
let daemon_binary_drift =
crate::build_info::daemon_binary_drift_payload(state.daemon_started_at());
let health_summary =
build_health_summary(index_stats.as_ref(), &semantic_status, &daemon_binary_drift);
RuntimeHealthSnapshot {
index_stats,
semantic_status,
daemon_binary_drift,
health_summary,
}
}
pub fn get_capabilities(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
let file_path = arguments.get("file_path").and_then(|v| v.as_str());
let language = file_path
.and_then(|fp| {
std::path::Path::new(fp)
.extension()
.and_then(|e| e.to_str())
})
.map(|ext| ext.to_ascii_lowercase());
let diagnostics_guidance = DiagnosticsGuidance::for_file(file_path);
let lsp_attached = diagnostics_guidance.status.is_available();
#[cfg(feature = "semantic")]
let embeddings_loaded = state.embedding_ref().is_some();
#[cfg(not(feature = "semantic"))]
let embeddings_loaded = false;
let active_surface = *state.surface();
let runtime_health = collect_runtime_health_snapshot(state, active_surface);
let semantic_search_guidance = runtime_health.semantic_status.guidance_payload();
let configured_embedding_model = codelens_engine::configured_embedding_model_name();
#[cfg(feature = "semantic")]
let embedding_runtime = {
let guard = state.embedding_ref();
guard
.as_ref()
.map(|engine| engine.runtime_info().clone())
.unwrap_or_else(codelens_engine::configured_embedding_runtime_info)
};
#[cfg(not(feature = "semantic"))]
let embedding_runtime = codelens_engine::configured_embedding_runtime_info();
#[cfg(feature = "semantic")]
let embedding_index_info = {
let guard = state.embedding_ref();
guard
.as_ref()
.map(|engine| engine.index_info())
.or_else(|| {
codelens_engine::EmbeddingEngine::inspect_existing_index(&state.project())
.ok()
.flatten()
})
};
#[cfg(not(feature = "semantic"))]
let embedding_index_info =
codelens_engine::EmbeddingEngine::inspect_existing_index(&state.project())
.ok()
.flatten();
let index_fresh = runtime_health.index_fresh();
let mut available = vec![
"symbols",
"imports",
"calls",
"rename",
"search",
"blast_radius",
"dead_code",
];
let mut unavailable: Vec<serde_json::Value> = Vec::new();
if lsp_attached {
available.extend_from_slice(&[
"type_hierarchy",
"diagnostics",
"workspace_symbols",
"rename_plan",
]);
} else {
unavailable.push(diagnostics_guidance.unavailable_payload("type_hierarchy_lsp"));
unavailable.push(diagnostics_guidance.unavailable_payload("diagnostics"));
available.push("type_hierarchy_native");
}
if runtime_health.semantic_status.is_available() {
available.push("semantic_search");
} else if let Some(reason) = runtime_health.semantic_status.reason_str() {
unavailable.push(json!({
"feature": "semantic_search",
"reason": reason,
"status": runtime_health.semantic_status.status_key(),
"reason_code": runtime_health.semantic_status.reason_code(),
"recommended_action": runtime_health.semantic_status.recommended_action(),
"action_target": runtime_health.semantic_status.action_target(),
}));
}
if !index_fresh {
unavailable.push(json!({"feature": "cached_queries", "reason": "index may be stale — call refresh_symbol_index"}));
}
let binary_build_info = json!({
"version": crate::build_info::BUILD_VERSION,
"git_sha": crate::build_info::BUILD_GIT_SHA,
"git_dirty": crate::build_info::build_git_dirty(),
"build_time": crate::build_info::BUILD_TIME,
});
let semantic_search_status = runtime_health.semantic_status.status_key();
let indexed_files = runtime_health.indexed_files();
let supported_files = runtime_health.supported_files();
let stale_files = runtime_health.stale_files();
let health_summary = runtime_health.health_summary.clone();
let daemon_binary_drift = runtime_health.daemon_binary_drift.clone();
let mut intelligence_sources = vec!["tree_sitter"];
if lsp_attached {
intelligence_sources.push("lsp");
}
if runtime_health.semantic_status.is_available() {
intelligence_sources.push("semantic");
}
let project_root = state.project();
let scip_available = project_root.as_path().join("index.scip").exists()
|| project_root.as_path().join(".scip/index.scip").exists()
|| project_root.as_path().join(".codelens/index.scip").exists();
#[allow(unused_mut)]
let mut scip_file_count: Option<usize> = None;
#[allow(unused_mut)]
let mut scip_symbol_count: Option<usize> = None;
if scip_available {
intelligence_sources.push("scip");
#[cfg(feature = "scip-backend")]
if let Some(backend) = state.scip() {
scip_file_count = Some(backend.file_count());
scip_symbol_count = Some(backend.symbol_count());
}
}
Ok((
json!({
"language": language,
"lsp_attached": lsp_attached,
"intelligence_sources": intelligence_sources,
"diagnostics_guidance": diagnostics_guidance.guidance_payload(),
"embeddings_loaded": embeddings_loaded,
"semantic_search_status": semantic_search_status,
"semantic_search_guidance": semantic_search_guidance,
"embedding_model": configured_embedding_model,
"embedding_runtime_preference": embedding_runtime.runtime_preference,
"embedding_runtime_backend": embedding_runtime.backend,
"embedding_threads": embedding_runtime.threads,
"embedding_max_length": embedding_runtime.max_length,
"embedding_coreml_model_format": embedding_runtime.coreml_model_format,
"embedding_coreml_compute_units": embedding_runtime.coreml_compute_units,
"embedding_coreml_static_input_shapes": embedding_runtime.coreml_static_input_shapes,
"embedding_coreml_profile_compute_plan": embedding_runtime.coreml_profile_compute_plan,
"embedding_coreml_specialization_strategy": embedding_runtime.coreml_specialization_strategy,
"embedding_coreml_model_cache_dir": embedding_runtime.coreml_model_cache_dir,
"embedding_runtime_fallback_reason": embedding_runtime.fallback_reason,
"embedding_indexed": embedding_index_info.as_ref().map(|info| info.indexed_symbols > 0).unwrap_or(false),
"embedding_indexed_symbols": embedding_index_info.as_ref().map(|info| info.indexed_symbols).unwrap_or(0),
"index_fresh": index_fresh,
"indexed_files": indexed_files,
"supported_files": supported_files,
"stale_files": stale_files,
"health_summary": health_summary,
"available": available,
"unavailable": unavailable,
"binary_version": crate::build_info::BUILD_VERSION,
"binary_git_sha": crate::build_info::BUILD_GIT_SHA,
"binary_build_time": crate::build_info::BUILD_TIME,
"daemon_started_at": state.daemon_started_at(),
"daemon_binary_drift": daemon_binary_drift,
"binary_build_info": binary_build_info,
"scip_available": scip_available,
"scip_file_count": scip_file_count,
"scip_symbol_count": scip_symbol_count,
}),
success_meta(BackendKind::Config, 0.95),
))
}