1use crate::context::AppContext;
4use crate::context::SemanticIndexStatus;
5use crate::protocol::{RawRequest, Response, StatusPayload, DEFAULT_SESSION_ID};
6
7pub fn handle_status(req: &RawRequest, ctx: &AppContext) -> Response {
8 Response::success(
9 &req.id,
10 ctx.build_status_snapshot_for_session(req.session()),
11 )
12}
13
14impl AppContext {
15 pub fn build_status_snapshot(&self) -> StatusPayload {
16 self.build_status_snapshot_for_session(DEFAULT_SESSION_ID)
17 }
18
19 pub fn build_status_snapshot_for_session(&self, session_id: &str) -> StatusPayload {
20 let config = self.config();
21
22 let search_index_info = {
24 let index = self.search_index().borrow();
25 match index.as_ref() {
26 Some(idx) if idx.ready => {
27 let file_count = idx.file_count();
28 let trigram_count = idx.trigram_count();
29 serde_json::json!({
30 "status": "ready",
31 "files": file_count,
32 "trigrams": trigram_count,
33 })
34 }
35 Some(_) => serde_json::json!({ "status": "building" }),
36 None => {
37 let status = if self.config().search_index {
38 "loading"
39 } else {
40 "disabled"
41 };
42 serde_json::json!({ "status": status })
43 }
44 }
45 };
46
47 let semantic_index_info = {
49 let index = self.semantic_index().borrow();
50 match index.as_ref() {
51 Some(idx) => {
52 serde_json::json!({
53 "status": idx.status_label(),
54 "entries": idx.entry_count(),
55 "dimension": idx.dimension(),
56 "backend": idx.backend_label().unwrap_or(config.semantic_backend_label()),
57 "model": idx.model_label().unwrap_or(config.semantic.model.as_str()),
58 })
59 }
60 None => match &*self.semantic_index_status().borrow() {
61 SemanticIndexStatus::Disabled => serde_json::json!({
62 "status": "disabled",
63 "backend": config.semantic_backend_label(),
64 "model": config.semantic.model.as_str(),
65 }),
66 SemanticIndexStatus::Building {
67 stage,
68 files,
69 entries_done,
70 entries_total,
71 } => serde_json::json!({
72 "status": "loading",
73 "stage": stage,
74 "files": files,
75 "entries_done": entries_done,
76 "entries_total": entries_total,
77 "backend": config.semantic_backend_label(),
78 "model": config.semantic.model.as_str(),
79 }),
80 SemanticIndexStatus::Ready => serde_json::json!({
81 "status": "ready",
82 "backend": config.semantic_backend_label(),
83 "model": config.semantic.model.as_str(),
84 }),
85 SemanticIndexStatus::Failed(error) => serde_json::json!({
86 "status": "failed",
87 "error": error,
88 "backend": config.semantic_backend_label(),
89 "model": config.semantic.model.as_str(),
90 }),
91 },
92 }
93 };
94
95 let storage_dir = config.storage_dir.as_ref().map(|d| d.display().to_string());
111 let disk_info = match (&config.storage_dir, &config.project_root) {
112 (Some(dir), Some(root)) => {
113 let key = crate::search_index::project_cache_key(root);
114 let trigram_size = dir_size(&dir.join("index").join(&key));
115 let semantic_size = dir_size(&dir.join("semantic").join(&key));
116 serde_json::json!({
117 "storage_dir": dir.display().to_string(),
118 "project_cache_key": key,
119 "trigram_disk_bytes": trigram_size,
120 "semantic_disk_bytes": semantic_size,
121 })
122 }
123 (Some(dir), None) => serde_json::json!({
124 "storage_dir": dir.display().to_string(),
125 "project_cache_key": null,
126 "trigram_disk_bytes": 0,
127 "semantic_disk_bytes": 0,
128 }),
129 _ => serde_json::json!({
130 "storage_dir": null,
131 "project_cache_key": null,
132 "trigram_disk_bytes": 0,
133 "semantic_disk_bytes": 0,
134 }),
135 };
136
137 let lsp_count = self.lsp_server_count();
139
140 let symbol_cache_stats = self.symbol_cache_stats();
142
143 let checkpoint_total = self.checkpoint().borrow().total_count();
147 let session_checkpoints = self.checkpoint().borrow().list(session_id).len();
148 let session_tracked_files = self.backup().borrow().tracked_files(session_id).len();
149
150 let degraded_reasons = self.degraded_reasons();
158 let degraded = !degraded_reasons.is_empty();
159
160 serde_json::json!({
161 "version": env!("CARGO_PKG_VERSION"),
162 "project_root": config.project_root.as_ref().map(|p| p.display().to_string()),
163 "canonical_root": self.canonical_cache_root_opt().map(|p| p.display().to_string()),
164 "cache_role": self.cache_role(),
165 "degraded": degraded,
166 "degraded_reasons": degraded_reasons,
167 "features": {
168 "format_on_edit": config.format_on_edit,
169 "validate_on_edit": config.validate_on_edit.as_deref().unwrap_or("off"),
170 "restrict_to_project_root": config.restrict_to_project_root,
171 "search_index": config.search_index,
172 "semantic_search": config.semantic_search,
173 },
174 "search_index": search_index_info,
175 "semantic_index": semantic_index_info,
176 "disk": disk_info,
177 "lsp_servers": lsp_count,
178 "symbol_cache": symbol_cache_stats,
179 "storage_dir": storage_dir,
180 "checkpoints_total": checkpoint_total,
182 "session": {
184 "id": session_id,
185 "tracked_files": session_tracked_files,
186 "checkpoints": session_checkpoints,
187 },
188 })
189 }
190}
191
192fn dir_size(path: &std::path::Path) -> u64 {
194 if !path.exists() {
195 return 0;
196 }
197 dir_size_recursive(path)
198}
199
200fn dir_size_recursive(path: &std::path::Path) -> u64 {
201 let mut total = 0u64;
202 let entries = match std::fs::read_dir(path) {
203 Ok(e) => e,
204 Err(_) => return 0,
205 };
206 for entry in entries.flatten() {
207 let ft = match entry.file_type() {
208 Ok(ft) => ft,
209 Err(_) => continue,
210 };
211 if ft.is_file() {
212 total += entry.metadata().map(|m| m.len()).unwrap_or(0);
213 } else if ft.is_dir() {
214 total += dir_size_recursive(&entry.path());
215 }
216 }
217 total
218}
219
220#[cfg(test)]
221mod tests {
222 use super::handle_status;
223 use crate::config::Config;
224 use crate::context::AppContext;
225 use crate::parser::TreeSitterProvider;
226 use crate::protocol::RawRequest;
227 use serde_json::json;
228
229 fn request() -> RawRequest {
230 RawRequest {
231 id: "status".to_string(),
232 command: "status".to_string(),
233 lsp_hints: None,
234 session_id: None,
235 params: json!({}),
236 }
237 }
238
239 #[test]
240 fn status_exposes_cache_role_and_canonical_root() {
241 let ctx = AppContext::new(Box::new(TreeSitterProvider::new()), Config::default());
242 let response = handle_status(&request(), &ctx);
243 assert_eq!(response.data["cache_role"], "not_initialized");
244 assert!(response.data["canonical_root"].is_null());
245
246 let temp = tempfile::tempdir().unwrap();
247 ctx.config_mut().project_root = Some(temp.path().to_path_buf());
248 ctx.set_canonical_cache_root(std::fs::canonicalize(temp.path()).unwrap());
249 ctx.set_cache_role(false, None);
250 let response = handle_status(&request(), &ctx);
251 assert_eq!(response.data["cache_role"], "main");
252 assert!(response.data["canonical_root"].as_str().is_some());
253
254 ctx.set_cache_role(true, None);
255 let response = handle_status(&request(), &ctx);
256 assert_eq!(response.data["cache_role"], "worktree");
257 }
258}