Skip to main content

mermaid_cli/runtime/
orchestrator.rs

1use anyhow::Result;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use crate::{
6    agents::mark_mcp_init_started,
7    app::{Config, load_config, persist_last_model},
8    cli::{Cli, handle_command},
9    mcp::McpServerManager,
10    models::{ModelConfig, ModelFactory},
11    ollama::ensure_model as ensure_ollama_model,
12    session::{ConversationManager, select_conversation},
13    tui::{App, McpInitResult, run_ui},
14    utils::{check_ollama_available, log_error, log_info, log_progress, log_warn},
15};
16
17/// Main runtime orchestrator
18pub struct Orchestrator {
19    cli: Cli,
20    config: Config,
21}
22
23impl Orchestrator {
24    /// Create a new orchestrator from CLI args
25    pub fn new(cli: Cli) -> Result<Self> {
26        // Load configuration (single file + defaults)
27        let config = match load_config() {
28            Ok(cfg) => cfg,
29            Err(e) => {
30                log_warn(
31                    "CONFIG",
32                    format!("Config load failed: {:#}. Using defaults.", e),
33                );
34                Config::default()
35            },
36        };
37
38        Ok(Self { cli, config })
39    }
40
41    /// Run the orchestrator
42    pub async fn run(self) -> Result<()> {
43        // Progress tracking for startup
44        let total_steps = 6; // Total startup steps
45        let mut current_step = 0;
46
47        // Handle subcommands
48        current_step += 1;
49        log_progress(current_step, total_steps, "Processing commands");
50        if let Some(command) = &self.cli.command
51            && handle_command(command).await?
52        {
53            return Ok(()); // Command handled, exit
54        }
55        // Continue to chat for Commands::Chat
56
57        // Determine model to use (CLI arg > last_used > default_model)
58        current_step += 1;
59        log_progress(current_step, total_steps, "Configuring model");
60
61        let cli_model_provided = self.cli.model.is_some();
62        let model_id =
63            crate::app::resolve_model_id(self.cli.model.as_deref(), &self.config).await?;
64
65        log_info(
66            "MERMAID",
67            format!("Starting Mermaid with model: {}", model_id),
68        );
69
70        // Check Ollama availability (all models route through Ollama)
71        current_step += 1;
72        log_progress(current_step, total_steps, "Checking Ollama availability");
73        let ollama_check =
74            check_ollama_available(&self.config.ollama.host, self.config.ollama.port).await;
75
76        if !ollama_check.available {
77            log_error("OLLAMA", &ollama_check.message);
78            anyhow::bail!("{}", ollama_check.message);
79        }
80
81        // Validate model exists
82        current_step += 1;
83        log_progress(current_step, total_steps, "Checking model availability");
84        ensure_ollama_model(&model_id).await?;
85
86        // Persist model if CLI flag was used
87        if cli_model_provided && let Err(e) = persist_last_model(&model_id) {
88            log_warn("CONFIG", format!("Failed to persist model choice: {}", e));
89        }
90
91        // Create model instance with config for authentication
92        current_step += 1;
93        log_progress(current_step, total_steps, "Initializing model");
94        let model = ModelFactory::create(
95            &model_id,
96            Some(&self.config),
97        )
98        .await
99        .map_err(|e| {
100            log_error("ERROR", format!("Failed to initialize model: {}", e));
101            anyhow::anyhow!(
102                "Failed to initialize model: {}. Make sure the model is available and properly configured.",
103                e
104            )
105        })?;
106
107        // Set up project path for session management
108        let project_path = self.cli.path.clone().unwrap_or_else(|| PathBuf::from("."));
109
110        // Start UI - LLM explores codebase via tools, no context injection
111        current_step += 1;
112        log_progress(current_step, total_steps, "Starting UI");
113        let base_config = ModelConfig::from_app_config(&self.config, &model_id);
114        let mut app = App::new(model, model_id.clone(), base_config);
115
116        // Start MCP servers in background (non-blocking — TUI renders immediately)
117        if !self.config.mcp_servers.is_empty() {
118            let server_count = self.config.mcp_servers.len();
119            log_info("MCP", format!("Starting {} MCP server(s) in background...", server_count));
120            mark_mcp_init_started();
121
122            let mcp_configs = self.config.mcp_servers.clone();
123            app.mcp_init_task = Some(tokio::spawn(async move {
124                let manager = McpServerManager::start(&mcp_configs).await;
125                if manager.has_servers() {
126                    let tools =
127                        crate::models::tools::mcp_tools_to_ollama(manager.get_all_tools());
128                    log_info("MCP", format!("{} MCP tool(s) available", tools.len()));
129                    McpInitResult {
130                        tools,
131                        manager: Some(Arc::new(manager)),
132                    }
133                } else {
134                    McpInitResult {
135                        tools: Vec::new(),
136                        manager: None,
137                    }
138                }
139            }));
140        }
141
142        // Handle session loading
143        // Default: start fresh (no history)
144        // --continue: resume last conversation
145        // --sessions: show picker to choose a previous conversation
146        if self.cli.continue_session || self.cli.sessions {
147            let conversation_manager = ConversationManager::new(&project_path)?;
148
149            if self.cli.sessions {
150                // Show selection UI for choosing a conversation
151                let conversations = conversation_manager.list_conversations()?;
152                if !conversations.is_empty() {
153                    if let Some(selected) = select_conversation(conversations)? {
154                        log_info(
155                            "RESUME",
156                            format!("Resuming conversation: {}", selected.title),
157                        );
158                        app.load_conversation(selected);
159                    }
160                } else {
161                    log_info("INFO", "No previous conversations found in this directory");
162                }
163            } else {
164                // --continue: resume last conversation
165                if let Some(last_conv) = conversation_manager.load_last_conversation()? {
166                    log_info("RESUME", format!("Resuming: {}", last_conv.title));
167                    app.load_conversation(last_conv);
168                } else {
169                    log_info("INFO", "No previous conversation to continue");
170                }
171            }
172        }
173
174        // Run the TUI
175        run_ui(app).await
176    }
177}