Skip to main content

vibe_workspace/ui/
smart_menu.rs

1use anyhow::Result;
2use std::path::PathBuf;
3
4use crate::ui::formatting;
5use crate::ui::state::VibeState;
6use crate::workspace::WorkspaceManager;
7
8/// Represents a smart action that can be taken based on context
9#[derive(Debug, Clone)]
10pub struct SmartAction {
11    pub label: String,
12    pub description: String,
13    pub action_type: SmartActionType,
14    pub priority: u8, // Higher is more important
15}
16
17#[derive(Debug, Clone)]
18pub enum SmartActionType {
19    CloneAndOpen(String),              // URL or search term
20    ConfigureApps(Vec<String>),        // Repo names that need app configuration
21    ConfigureAndOpen(String),          // Configure app for repo and open
22    CreateRepository,                  // Create new local repository
23    DiscoverRepos,                     // Scan for new repositories
24    InstallApps,                       // Install missing apps
25    OpenRecent(String),                // Repo name
26    OpenWithPreferred(String, String), // Repo name, preferred app
27    QuickConfigureBatch(Vec<String>),  // Batch configure multiple repos
28    SetupWorkspace,                    // First-time setup
29    SyncRepositories,                  // Pull updates for all repos
30    CleanupMissing,                    // Remove missing repos from config
31    BulkClone(String),                 // Bulk clone from user/org
32}
33
34/// Represents a quick launch item
35#[derive(Debug, Clone)]
36pub struct QuickLaunchItem {
37    pub number: usize, // 1-9
38    pub repo_name: String,
39    pub repo_path: PathBuf,
40    pub last_app: Option<String>,
41    pub last_accessed: String, // Human-readable time
42    pub access_count: u32,
43}
44
45/// Analyzes workspace state to provide smart menu options
46pub struct SmartMenu {
47    workspace_state: WorkspaceState,
48    user_state: VibeState,
49}
50
51/// Current state of the workspace
52#[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    /// Create a new smart menu analyzer
65    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    /// Analyze the current workspace state
76    async fn analyze_workspace(manager: &WorkspaceManager) -> Result<WorkspaceState> {
77        let repos = manager.list_repositories();
78        let total_repos = repos.len();
79
80        // Find unconfigured repos
81        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        // Find missing repos (in config but not on disk)
88        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        // Check available apps
98        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        // TODO: Check for uncommitted changes and sync status
106        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    /// Get smart actions based on current context
120    pub fn get_smart_actions(&self) -> Vec<SmartAction> {
121        let mut actions = Vec::new();
122
123        // First-time setup
124        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        // Discover repos if workspace is empty
134        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        // Create new repository action (always available) - HIGH PRIORITY
144        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        // Clone new repo action (always available) - HIGH PRIORITY
152        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        // Open any repository action (if repos exist) - HIGH PRIORITY
160        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        // Sync repositories if it's been a while - MEDIUM PRIORITY
170        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        // Install apps if none available - LOWER PRIORITY (optional)
182        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        // Configure apps for unconfigured repos - LOWER PRIORITY (optional)
192        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        // Clean up missing repos - LOWER PRIORITY (maintenance)
209        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        // Bulk clone suggestions for new/small workspaces - MEDIUM-HIGH PRIORITY (usage)
224        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        // Sort by priority (highest first)
234        actions.sort_by(|a, b| b.priority.cmp(&a.priority));
235
236        // Return top 5 actions
237        actions.truncate(5);
238        actions
239    }
240
241    /// Get quick launch items (recent repositories)
242    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    /// Check if setup wizard should be shown
263    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    /// Get smart open actions (open repo with any available app)
268    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        // Get configured repositories and their apps
274        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        // Create "Open with preferred app" actions for recent repos with known preferences
281        for recent_repo in recent_repos {
282            if let Some(last_app) = &recent_repo.last_app {
283                // Check if the app is available, regardless of configuration
284                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, // High priority for preferred actions
300                    });
301                }
302            }
303        }
304
305        // Add universal opening options for recent repos without preferences
306        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                // Add opening options for the most common available apps
311                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                        // Only show one app option per repo to avoid clutter
331                        break;
332                    }
333                }
334            }
335        }
336
337        // Add "Configure and open" for unconfigured repos (now as enhancement, not requirement)
338        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, // Lower priority since configuration is now optional
347                });
348            }
349        }
350
351        // Add batch configuration for multiple unconfigured repos (as enhancement)
352        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, // Lower priority since templates are enhancements
361            });
362        }
363
364        // Sort by priority and limit to top 5 smart open actions
365        actions.sort_by(|a, b| b.priority.cmp(&a.priority));
366        actions.truncate(5);
367        actions
368    }
369}
370
371/// Create a context-aware menu item label
372pub 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}