1use anyhow::Result;
2use std::path::PathBuf;
3
4use crate::ui::formatting;
5use crate::ui::state::VibeState;
6use crate::workspace::WorkspaceManager;
7
8#[derive(Debug, Clone)]
10pub struct SmartAction {
11 pub label: String,
12 pub description: String,
13 pub action_type: SmartActionType,
14 pub priority: u8, }
16
17#[derive(Debug, Clone)]
18pub enum SmartActionType {
19 CloneAndOpen(String), ConfigureApps(Vec<String>), ConfigureAndOpen(String), CreateRepository, DiscoverRepos, InstallApps, OpenRecent(String), OpenWithPreferred(String, String), QuickConfigureBatch(Vec<String>), SetupWorkspace, SyncRepositories, CleanupMissing, BulkClone(String), }
33
34#[derive(Debug, Clone)]
36pub struct QuickLaunchItem {
37 pub number: usize, pub repo_name: String,
39 pub repo_path: PathBuf,
40 pub last_app: Option<String>,
41 pub last_accessed: String, pub access_count: u32,
43}
44
45pub struct SmartMenu {
47 workspace_state: WorkspaceState,
48 user_state: VibeState,
49}
50
51#[derive(Debug)]
53struct WorkspaceState {
54 total_repos: usize,
55 unconfigured_repos: Vec<String>,
56 missing_repos: Vec<String>,
57 available_apps: Vec<String>,
58 #[allow(dead_code)]
59 has_uncommitted_changes: bool,
60 days_since_last_sync: Option<i64>,
61}
62
63impl SmartMenu {
64 pub async fn new(workspace_manager: &WorkspaceManager) -> Result<Self> {
66 let user_state = VibeState::load().unwrap_or_default();
67 let workspace_state = Self::analyze_workspace(workspace_manager).await?;
68
69 Ok(Self {
70 workspace_state,
71 user_state,
72 })
73 }
74
75 async fn analyze_workspace(manager: &WorkspaceManager) -> Result<WorkspaceState> {
77 let repos = manager.list_repositories();
78 let total_repos = repos.len();
79
80 let unconfigured_repos: Vec<String> = repos
82 .iter()
83 .filter(|repo| repo.apps.is_empty())
84 .map(|repo| repo.name.clone())
85 .collect();
86
87 let mut missing_repos = Vec::new();
89 let workspace_root = manager.get_workspace_root();
90 for repo in repos {
91 let full_path = workspace_root.join(&repo.path);
92 if !full_path.exists() {
93 missing_repos.push(repo.name.clone());
94 }
95 }
96
97 let mut available_apps = Vec::new();
99 for app in &["vscode", "warp", "iterm2", "wezterm", "cursor", "windsurf"] {
100 if manager.is_app_available(app).await {
101 available_apps.push(app.to_string());
102 }
103 }
104
105 let has_uncommitted_changes = false;
107 let days_since_last_sync = None;
108
109 Ok(WorkspaceState {
110 total_repos,
111 unconfigured_repos,
112 missing_repos,
113 available_apps,
114 has_uncommitted_changes,
115 days_since_last_sync,
116 })
117 }
118
119 pub fn get_smart_actions(&self) -> Vec<SmartAction> {
121 let mut actions = Vec::new();
122
123 if self.user_state.is_first_run() && self.workspace_state.total_repos == 0 {
125 actions.push(SmartAction {
126 label: "๐ Run setup wizard".to_string(),
127 description: "Get started with Vibe Workspace".to_string(),
128 action_type: SmartActionType::SetupWorkspace,
129 priority: 100,
130 });
131 }
132
133 if self.workspace_state.total_repos == 0 {
135 actions.push(SmartAction {
136 label: "๐ Discover repositories".to_string(),
137 description: "Scan workspace for git repositories".to_string(),
138 action_type: SmartActionType::DiscoverRepos,
139 priority: 90,
140 });
141 }
142
143 actions.push(SmartAction {
145 label: "๐ Create new repository".to_string(),
146 description: "Create a new local repository for prototyping".to_string(),
147 action_type: SmartActionType::CreateRepository,
148 priority: 85,
149 });
150
151 actions.push(SmartAction {
153 label: "๐ฅ Clone new repository".to_string(),
154 description: "Search and clone from GitHub".to_string(),
155 action_type: SmartActionType::CloneAndOpen("".to_string()),
156 priority: 80,
157 });
158
159 if self.workspace_state.total_repos > 0 {
161 actions.push(SmartAction {
162 label: "๐ Open repository".to_string(),
163 description: "Browse and open any repository in your workspace".to_string(),
164 action_type: SmartActionType::OpenRecent("".to_string()),
165 priority: 90,
166 });
167 }
168
169 if let Some(days) = self.workspace_state.days_since_last_sync {
171 if days > 7 {
172 actions.push(SmartAction {
173 label: "๐ Sync all repositories".to_string(),
174 description: format!("Last synced {days} days ago"),
175 action_type: SmartActionType::SyncRepositories,
176 priority: 70,
177 });
178 }
179 }
180
181 if self.workspace_state.available_apps.is_empty() && self.workspace_state.total_repos > 0 {
183 actions.push(SmartAction {
184 label: "๐ฑ Install development apps".to_string(),
185 description: "Install VS Code, Warp, or other supported apps".to_string(),
186 action_type: SmartActionType::InstallApps,
187 priority: 60,
188 });
189 }
190
191 if !self.workspace_state.unconfigured_repos.is_empty() {
193 let count = self.workspace_state.unconfigured_repos.len();
194 actions.push(SmartAction {
195 label: format!(
196 "โ๏ธ Set up templates for {} repo{}",
197 count,
198 if count == 1 { "" } else { "s" }
199 ),
200 description: "Configure advanced templates and automation (optional)".to_string(),
201 action_type: SmartActionType::ConfigureApps(
202 self.workspace_state.unconfigured_repos.clone(),
203 ),
204 priority: 50,
205 });
206 }
207
208 if !self.workspace_state.missing_repos.is_empty() {
210 let count = self.workspace_state.missing_repos.len();
211 actions.push(SmartAction {
212 label: format!(
213 "๐งน Clean up {} missing repo{}",
214 count,
215 if count == 1 { "" } else { "s" }
216 ),
217 description: "Remove deleted repositories from configuration".to_string(),
218 action_type: SmartActionType::CleanupMissing,
219 priority: 40,
220 });
221 }
222
223 if self.workspace_state.total_repos < 10 {
225 actions.push(SmartAction {
226 label: "๐ฆ Bulk clone repositories".to_string(),
227 description: "Clone all repos from a GitHub user or organization".to_string(),
228 action_type: SmartActionType::BulkClone("".to_string()),
229 priority: 75,
230 });
231 }
232
233 actions.sort_by(|a, b| b.priority.cmp(&a.priority));
235
236 actions.truncate(5);
238 actions
239 }
240
241 pub fn get_quick_launch_items(&self) -> Vec<QuickLaunchItem> {
243 let recent_repos = self.user_state.get_recent_repos(15);
244
245 recent_repos
246 .iter()
247 .enumerate()
248 .map(|(index, repo)| {
249 let time_ago = formatting::format_time_ago(&repo.last_accessed);
250 QuickLaunchItem {
251 number: index + 1,
252 repo_name: repo.repo_id.clone(),
253 repo_path: repo.path.clone(),
254 last_app: repo.last_app.clone(),
255 last_accessed: time_ago,
256 access_count: repo.access_count,
257 }
258 })
259 .collect()
260 }
261
262 pub fn should_show_setup_wizard(&self) -> bool {
264 self.user_state.is_first_run() && self.user_state.user_preferences.show_setup_wizard
265 }
266
267 pub fn get_smart_open_actions(&self, workspace_manager: &WorkspaceManager) -> Vec<SmartAction> {
269 let mut actions = Vec::new();
270 let recent_repos = self.user_state.get_recent_repos(5);
271 let all_repos = workspace_manager.list_repositories();
272
273 let configured_repos: std::collections::HashMap<String, Vec<String>> = all_repos
275 .iter()
276 .filter(|repo| !repo.apps.is_empty())
277 .map(|repo| (repo.name.clone(), repo.apps.keys().cloned().collect()))
278 .collect();
279
280 for recent_repo in recent_repos {
282 if let Some(last_app) = &recent_repo.last_app {
283 if self.workspace_state.available_apps.contains(last_app) {
285 let is_configured = configured_repos.contains_key(&recent_repo.repo_id);
286 let description = if is_configured {
287 format!("Open with your preferred app ({})", last_app)
288 } else {
289 format!("Open with {} (basic mode)", last_app)
290 };
291
292 actions.push(SmartAction {
293 label: format!("๐ฏ Open {} โ {}", recent_repo.repo_id, last_app),
294 description,
295 action_type: SmartActionType::OpenWithPreferred(
296 recent_repo.repo_id.clone(),
297 last_app.clone(),
298 ),
299 priority: 95, });
301 }
302 }
303 }
304
305 for recent_repo in recent_repos {
307 if recent_repo.last_app.is_none() && !actions.iter().any(|a| {
308 matches!(&a.action_type, SmartActionType::OpenWithPreferred(name, _) if name == &recent_repo.repo_id)
309 }) {
310 for app in &self.workspace_state.available_apps {
312 if matches!(app.as_str(), "vscode" | "cursor" | "warp" | "iterm2") {
313 let is_configured = configured_repos.contains_key(&recent_repo.repo_id);
314 let description = if is_configured {
315 format!("Open with {} (configured)", app)
316 } else {
317 format!("Open with {} (basic)", app)
318 };
319
320 actions.push(SmartAction {
321 label: format!("๐ Open {} โ {}", recent_repo.repo_id, app),
322 description,
323 action_type: SmartActionType::OpenWithPreferred(
324 recent_repo.repo_id.clone(),
325 app.clone(),
326 ),
327 priority: 80,
328 });
329
330 break;
332 }
333 }
334 }
335 }
336
337 for unconfigured_repo in &self.workspace_state.unconfigured_repos {
339 if self.workspace_state.available_apps.len() >= 1 && !actions.iter().any(|a| {
340 matches!(&a.action_type, SmartActionType::OpenWithPreferred(name, _) if name == unconfigured_repo)
341 }) {
342 actions.push(SmartAction {
343 label: format!("โ๏ธ Configure templates for {}", unconfigured_repo),
344 description: "Set up advanced templates and automation".to_string(),
345 action_type: SmartActionType::ConfigureAndOpen(unconfigured_repo.clone()),
346 priority: 70, });
348 }
349 }
350
351 if self.workspace_state.unconfigured_repos.len() > 3 {
353 let count = self.workspace_state.unconfigured_repos.len();
354 actions.push(SmartAction {
355 label: format!("โ๏ธ Set up templates for {} repos", count),
356 description: "Configure advanced templates and automation".to_string(),
357 action_type: SmartActionType::QuickConfigureBatch(
358 self.workspace_state.unconfigured_repos.clone(),
359 ),
360 priority: 60, });
362 }
363
364 actions.sort_by(|a, b| b.priority.cmp(&a.priority));
366 actions.truncate(5);
367 actions
368 }
369}
370
371pub fn create_menu_item(base_label: &str, context: Option<&str>) -> String {
373 match context {
374 Some(ctx) => format!("{base_label} {ctx}"),
375 None => base_label.to_string(),
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_smart_action_priority() {
385 let action1 = SmartAction {
386 label: "Action 1".to_string(),
387 description: "Test".to_string(),
388 action_type: SmartActionType::DiscoverRepos,
389 priority: 50,
390 };
391
392 let action2 = SmartAction {
393 label: "Action 2".to_string(),
394 description: "Test".to_string(),
395 action_type: SmartActionType::InstallApps,
396 priority: 100,
397 };
398
399 let mut actions = vec![action1, action2];
400 actions.sort_by(|a, b| b.priority.cmp(&a.priority));
401
402 assert_eq!(actions[0].priority, 100);
403 assert_eq!(actions[1].priority, 50);
404 }
405}