Skip to main content

spool/
desktop_status.rs

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}