1use crate::app;
2use crate::enhancement_trace::{PromptOptimizeTrace, read_latest_prompt_optimize_trace};
3use crate::memory_gateway::load_config;
4use serde::Serialize;
5use std::fs;
6use std::path::Path;
7use ts_rs::TS;
8
9#[derive(Debug, Clone, Serialize, TS)]
10#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
11pub struct DesktopStatusResponse {
12 pub config_exists: bool,
13 pub vault_available: bool,
14 pub vault_root: Option<String>,
15 pub cwd_exists: bool,
16 pub cwd: String,
17 pub session_sources_available: bool,
18 pub claude_mcp_registered: bool,
19 pub codex_mcp_registered: bool,
20 pub mcp_config_detected: bool,
21 pub spool_mcp_command: String,
22 pub claude_mcp_snippet: String,
23 pub codex_mcp_snippet: String,
24 pub recent_enhancement: Option<PromptOptimizeTrace>,
25}
26
27pub fn collect_status(
28 config_path: &Path,
29 cwd: &Path,
30 vault_override: Option<&Path>,
31 provider_session_count: usize,
32) -> DesktopStatusResponse {
33 let config_exists = config_path.exists() && config_path.is_file();
34 let cwd_exists = cwd.exists() && cwd.is_dir();
35
36 let resolved_vault = if let Some(override_path) = vault_override {
37 app::resolve_override_path(override_path, config_path)
38 .ok()
39 .map(|path| path.display().to_string())
40 } else if config_exists {
41 load_config(config_path)
42 .ok()
43 .map(|cfg| cfg.vault.root.display().to_string())
44 } else {
45 None
46 };
47
48 let vault_available = resolved_vault
49 .as_deref()
50 .map(|path| fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false))
51 .unwrap_or(false);
52 let claude_mcp_registered = detect_claude_spool_mcp();
53 let codex_mcp_registered = detect_codex_spool_mcp();
54 let spool_mcp_command = suggested_spool_mcp_command();
55 let recent_enhancement = read_latest_prompt_optimize_trace(config_path)
56 .ok()
57 .flatten();
58 let claude_mcp_snippet = format!(
59 r#""spool": {{
60 "type": "stdio",
61 "command": "{}",
62 "args": ["--config", "{}"]
63}}"#,
64 spool_mcp_command,
65 config_path.display()
66 );
67 let codex_mcp_snippet = format!(
68 r#"[mcp_servers.spool]
69type = "stdio"
70command = "{}"
71args = ["--config", "{}"]"#,
72 spool_mcp_command,
73 config_path.display()
74 );
75
76 DesktopStatusResponse {
77 config_exists,
78 vault_available,
79 vault_root: resolved_vault,
80 cwd_exists,
81 cwd: cwd.display().to_string(),
82 session_sources_available: provider_session_count > 0,
83 claude_mcp_registered,
84 codex_mcp_registered,
85 mcp_config_detected: claude_mcp_registered || codex_mcp_registered,
86 spool_mcp_command,
87 claude_mcp_snippet,
88 codex_mcp_snippet,
89 recent_enhancement,
90 }
91}
92
93fn suggested_spool_mcp_command() -> String {
94 Path::new(env!("CARGO_MANIFEST_DIR"))
95 .join("target/debug/spool-mcp")
96 .display()
97 .to_string()
98}
99
100fn detect_claude_spool_mcp() -> bool {
101 let Some(home) = crate::support::home_dir() else {
102 return false;
103 };
104 let path = home.join(".claude.json");
105 let content = match fs::read_to_string(path) {
106 Ok(content) => content,
107 Err(_) => return false,
108 };
109 let value: serde_json::Value = match serde_json::from_str(&content) {
110 Ok(value) => value,
111 Err(_) => return false,
112 };
113 value
114 .get("mcpServers")
115 .and_then(|servers| servers.get("spool"))
116 .is_some()
117}
118
119fn detect_codex_spool_mcp() -> bool {
120 let Some(home) = crate::support::home_dir() else {
121 return false;
122 };
123 let path = home.join(".codex/config.toml");
124 let content = match fs::read_to_string(path) {
125 Ok(content) => content,
126 Err(_) => return false,
127 };
128 let value: toml::Value = match toml::from_str(&content) {
129 Ok(value) => value,
130 Err(_) => return false,
131 };
132 value
133 .get("mcp_servers")
134 .and_then(|servers| servers.get("spool"))
135 .is_some()
136}