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
17pub struct Orchestrator {
19 cli: Cli,
20 config: Config,
21}
22
23impl Orchestrator {
24 pub fn new(cli: Cli) -> Result<Self> {
26 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 pub async fn run(self) -> Result<()> {
43 let total_steps = 6; let mut current_step = 0;
46
47 current_step += 1;
49 log_progress(current_step, total_steps, "Processing commands");
50 if let Some(command) = &self.cli.command
51 && handle_command(command, &self.config).await?
52 {
53 return Ok(()); }
55 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 let model_uses_ollama = is_ollama_model(&model_id);
75
76 current_step += 1;
79 log_progress(current_step, total_steps, "Checking Ollama availability");
80 if model_uses_ollama {
81 let ollama_check =
82 check_ollama_available(&self.config.ollama.host, self.config.ollama.port).await;
83
84 if !ollama_check.available {
85 log_error("OLLAMA", &ollama_check.message);
86 anyhow::bail!("{}", ollama_check.message);
87 }
88 }
89
90 current_step += 1;
93 log_progress(current_step, total_steps, "Checking model availability");
94 if model_uses_ollama {
95 ensure_ollama_model(&model_id, &self.config).await?;
96 }
97
98 current_step += 1;
101 log_progress(current_step, total_steps, "Initializing model");
102 let model = ModelFactory::create(&model_id, Some(&self.config))
103 .await
104 .map_err(|e| {
105 log_error("ERROR", format!("Failed to initialize model: {}", e));
106 anyhow::anyhow!(actionable_init_error(&model_id, e))
107 })?;
108
109 if cli_model_provided && let Err(e) = persist_last_model(&model_id) {
112 log_warn("CONFIG", format!("Failed to persist model choice: {}", e));
113 }
114
115 let project_path = self.cli.path.clone().unwrap_or_else(|| PathBuf::from("."));
117
118 current_step += 1;
120 log_progress(current_step, total_steps, "Starting UI");
121 let mut base_config = ModelConfig::from_app_config(&self.config, &model_id);
122 if let Some(level) = self.cli.reasoning {
125 base_config.reasoning = level;
126 }
127 let mut app = App::new(model, model_id.clone(), base_config);
128
129 {
133 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
134 if let Some(loaded) = crate::app::instructions::find_mermaid_md(&cwd)
135 .and_then(|p| crate::app::instructions::load_from_path(&p))
136 {
137 log_info(
138 "INSTRUCTIONS",
139 format!(
140 "Loaded MERMAID.md ({} bytes{}) from {}",
141 loaded.byte_len,
142 if loaded.truncated { ", truncated" } else { "" },
143 loaded.path.display()
144 ),
145 );
146 app.instructions = Some(loaded);
147 }
148 }
149
150 if !self.config.mcp_servers.is_empty() {
152 let server_count = self.config.mcp_servers.len();
153 log_info(
154 "MCP",
155 format!("Starting {} MCP server(s) in background...", server_count),
156 );
157 mark_mcp_init_started();
158
159 let mcp_configs = self.config.mcp_servers.clone();
160 app.mcp_init_task = Some(tokio::spawn(async move {
161 let manager = McpServerManager::start(&mcp_configs).await;
162 if manager.has_servers() {
163 let tools = crate::models::tools::mcp_tools_to_ollama(manager.get_all_tools());
164 log_info("MCP", format!("{} MCP tool(s) available", tools.len()));
165 McpInitResult {
166 tools,
167 manager: Some(Arc::new(manager)),
168 }
169 } else {
170 McpInitResult {
171 tools: Vec::new(),
172 manager: None,
173 }
174 }
175 }));
176 }
177
178 if self.cli.continue_session || self.cli.sessions {
183 let conversation_manager = ConversationManager::new(&project_path)?;
184
185 if self.cli.sessions {
186 let conversations = conversation_manager.list_conversations()?;
188 if !conversations.is_empty() {
189 if let Some(selected) = select_conversation(conversations)? {
190 log_info(
191 "RESUME",
192 format!("Resuming conversation: {}", selected.title),
193 );
194 app.load_conversation(selected);
195 }
196 } else {
197 log_info("INFO", "No previous conversations found in this directory");
198 }
199 } else {
200 if let Some(last_conv) = conversation_manager.load_last_conversation()? {
202 log_info("RESUME", format!("Resuming: {}", last_conv.title));
203 app.load_conversation(last_conv);
204 } else {
205 log_info("INFO", "No previous conversation to continue");
206 }
207 }
208 }
209
210 run_ui(app).await
212 }
213}
214
215fn is_ollama_model(model_id: &str) -> bool {
219 match model_id.split_once('/') {
220 Some((provider, _)) => provider.eq_ignore_ascii_case("ollama"),
221 None => true,
222 }
223}
224
225pub fn actionable_init_error(model_id: &str, source: impl std::fmt::Display) -> String {
233 format!(
234 "Failed to initialize model '{}': {}\n\n\
235 To recover, try one of:\n \
236 - List available models: mermaid list\n \
237 - Switch to a different model: mermaid --model <name>\n \
238 - Use a local Ollama model (no API key needed): mermaid --model ollama/<name>",
239 model_id, source
240 )
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
252 fn actionable_init_error_names_failing_model() {
253 let msg = actionable_init_error(
254 "anthropic/claude-opus-4-7",
255 "Authentication error: ANTHROPIC_API_KEY not set",
256 );
257 assert!(
258 msg.contains("anthropic/claude-opus-4-7"),
259 "model id missing from error: {}",
260 msg
261 );
262 }
263
264 #[test]
267 fn actionable_init_error_recommends_mermaid_list() {
268 let msg = actionable_init_error("foo/bar", "404 not found");
269 assert!(
270 msg.contains("mermaid list"),
271 "expected `mermaid list` recovery hint in: {}",
272 msg
273 );
274 }
275
276 #[test]
279 fn actionable_init_error_recommends_explicit_model_flag() {
280 let msg = actionable_init_error("foo/bar", "404 not found");
281 assert!(
282 msg.contains("--model"),
283 "expected `--model` recovery hint in: {}",
284 msg
285 );
286 }
287}