use anyhow::Result;
use std::path::PathBuf;
use crate::ui::formatting;
use crate::ui::state::VibeState;
use crate::workspace::WorkspaceManager;
#[derive(Debug, Clone)]
pub struct SmartAction {
pub label: String,
pub description: String,
pub action_type: SmartActionType,
pub priority: u8, }
#[derive(Debug, Clone)]
pub enum SmartActionType {
CloneAndOpen(String), ConfigureApps(Vec<String>), ConfigureAndOpen(String), CreateRepository, DiscoverRepos, InstallApps, OpenRecent(String), OpenWithPreferred(String, String), QuickConfigureBatch(Vec<String>), SetupWorkspace, SyncRepositories, CleanupMissing, BulkClone(String), }
#[derive(Debug, Clone)]
pub struct QuickLaunchItem {
pub number: usize, pub repo_name: String,
pub repo_path: PathBuf,
pub last_app: Option<String>,
pub last_accessed: String, pub access_count: u32,
}
pub struct SmartMenu {
workspace_state: WorkspaceState,
user_state: VibeState,
}
#[derive(Debug)]
struct WorkspaceState {
total_repos: usize,
unconfigured_repos: Vec<String>,
missing_repos: Vec<String>,
available_apps: Vec<String>,
#[allow(dead_code)]
has_uncommitted_changes: bool,
days_since_last_sync: Option<i64>,
}
impl SmartMenu {
pub async fn new(workspace_manager: &WorkspaceManager) -> Result<Self> {
let user_state = VibeState::load().unwrap_or_default();
let workspace_state = Self::analyze_workspace(workspace_manager).await?;
Ok(Self {
workspace_state,
user_state,
})
}
async fn analyze_workspace(manager: &WorkspaceManager) -> Result<WorkspaceState> {
let repos = manager.list_repositories();
let total_repos = repos.len();
let unconfigured_repos: Vec<String> = repos
.iter()
.filter(|repo| repo.apps.is_empty())
.map(|repo| repo.name.clone())
.collect();
let mut missing_repos = Vec::new();
let workspace_root = manager.get_workspace_root();
for repo in repos {
let full_path = workspace_root.join(&repo.path);
if !full_path.exists() {
missing_repos.push(repo.name.clone());
}
}
let mut available_apps = Vec::new();
for app in &["vscode", "warp", "iterm2", "wezterm", "cursor", "windsurf"] {
if manager.is_app_available(app).await {
available_apps.push(app.to_string());
}
}
let has_uncommitted_changes = false;
let days_since_last_sync = None;
Ok(WorkspaceState {
total_repos,
unconfigured_repos,
missing_repos,
available_apps,
has_uncommitted_changes,
days_since_last_sync,
})
}
pub fn get_smart_actions(&self) -> Vec<SmartAction> {
let mut actions = Vec::new();
if self.user_state.is_first_run() && self.workspace_state.total_repos == 0 {
actions.push(SmartAction {
label: "🎉 Run setup wizard".to_string(),
description: "Get started with Vibe Workspace".to_string(),
action_type: SmartActionType::SetupWorkspace,
priority: 100,
});
}
if self.workspace_state.total_repos == 0 {
actions.push(SmartAction {
label: "🔍 Discover repositories".to_string(),
description: "Scan workspace for git repositories".to_string(),
action_type: SmartActionType::DiscoverRepos,
priority: 90,
});
}
actions.push(SmartAction {
label: "🆕 Create new repository".to_string(),
description: "Create a new local repository for prototyping".to_string(),
action_type: SmartActionType::CreateRepository,
priority: 85,
});
actions.push(SmartAction {
label: "📥 Clone new repository".to_string(),
description: "Search and clone from GitHub".to_string(),
action_type: SmartActionType::CloneAndOpen("".to_string()),
priority: 80,
});
if self.workspace_state.total_repos > 0 {
actions.push(SmartAction {
label: "📂 Open repository".to_string(),
description: "Browse and open any repository in your workspace".to_string(),
action_type: SmartActionType::OpenRecent("".to_string()),
priority: 90,
});
}
if let Some(days) = self.workspace_state.days_since_last_sync {
if days > 7 {
actions.push(SmartAction {
label: "🔄 Sync all repositories".to_string(),
description: format!("Last synced {days} days ago"),
action_type: SmartActionType::SyncRepositories,
priority: 70,
});
}
}
if self.workspace_state.available_apps.is_empty() && self.workspace_state.total_repos > 0 {
actions.push(SmartAction {
label: "📱 Install development apps".to_string(),
description: "Install VS Code, Warp, or other supported apps".to_string(),
action_type: SmartActionType::InstallApps,
priority: 60,
});
}
if !self.workspace_state.unconfigured_repos.is_empty() {
let count = self.workspace_state.unconfigured_repos.len();
actions.push(SmartAction {
label: format!(
"⚙️ Set up templates for {} repo{}",
count,
if count == 1 { "" } else { "s" }
),
description: "Configure advanced templates and automation (optional)".to_string(),
action_type: SmartActionType::ConfigureApps(
self.workspace_state.unconfigured_repos.clone(),
),
priority: 50,
});
}
if !self.workspace_state.missing_repos.is_empty() {
let count = self.workspace_state.missing_repos.len();
actions.push(SmartAction {
label: format!(
"🧹 Clean up {} missing repo{}",
count,
if count == 1 { "" } else { "s" }
),
description: "Remove deleted repositories from configuration".to_string(),
action_type: SmartActionType::CleanupMissing,
priority: 40,
});
}
if self.workspace_state.total_repos < 10 {
actions.push(SmartAction {
label: "📦 Bulk clone repositories".to_string(),
description: "Clone all repos from a GitHub user or organization".to_string(),
action_type: SmartActionType::BulkClone("".to_string()),
priority: 75,
});
}
actions.sort_by(|a, b| b.priority.cmp(&a.priority));
actions.truncate(5);
actions
}
pub fn get_quick_launch_items(&self) -> Vec<QuickLaunchItem> {
let recent_repos = self.user_state.get_recent_repos(15);
recent_repos
.iter()
.enumerate()
.map(|(index, repo)| {
let time_ago = formatting::format_time_ago(&repo.last_accessed);
QuickLaunchItem {
number: index + 1,
repo_name: repo.repo_id.clone(),
repo_path: repo.path.clone(),
last_app: repo.last_app.clone(),
last_accessed: time_ago,
access_count: repo.access_count,
}
})
.collect()
}
pub fn should_show_setup_wizard(&self) -> bool {
self.user_state.is_first_run() && self.user_state.user_preferences.show_setup_wizard
}
pub fn get_smart_open_actions(&self, workspace_manager: &WorkspaceManager) -> Vec<SmartAction> {
let mut actions = Vec::new();
let recent_repos = self.user_state.get_recent_repos(5);
let all_repos = workspace_manager.list_repositories();
let configured_repos: std::collections::HashMap<String, Vec<String>> = all_repos
.iter()
.filter(|repo| !repo.apps.is_empty())
.map(|repo| (repo.name.clone(), repo.apps.keys().cloned().collect()))
.collect();
for recent_repo in recent_repos {
if let Some(last_app) = &recent_repo.last_app {
if self.workspace_state.available_apps.contains(last_app) {
let is_configured = configured_repos.contains_key(&recent_repo.repo_id);
let description = if is_configured {
format!("Open with your preferred app ({})", last_app)
} else {
format!("Open with {} (basic mode)", last_app)
};
actions.push(SmartAction {
label: format!("🎯 Open {} → {}", recent_repo.repo_id, last_app),
description,
action_type: SmartActionType::OpenWithPreferred(
recent_repo.repo_id.clone(),
last_app.clone(),
),
priority: 95, });
}
}
}
for recent_repo in recent_repos {
if recent_repo.last_app.is_none() && !actions.iter().any(|a| {
matches!(&a.action_type, SmartActionType::OpenWithPreferred(name, _) if name == &recent_repo.repo_id)
}) {
for app in &self.workspace_state.available_apps {
if matches!(app.as_str(), "vscode" | "cursor" | "warp" | "iterm2") {
let is_configured = configured_repos.contains_key(&recent_repo.repo_id);
let description = if is_configured {
format!("Open with {} (configured)", app)
} else {
format!("Open with {} (basic)", app)
};
actions.push(SmartAction {
label: format!("📂 Open {} → {}", recent_repo.repo_id, app),
description,
action_type: SmartActionType::OpenWithPreferred(
recent_repo.repo_id.clone(),
app.clone(),
),
priority: 80,
});
break;
}
}
}
}
for unconfigured_repo in &self.workspace_state.unconfigured_repos {
if self.workspace_state.available_apps.len() >= 1 && !actions.iter().any(|a| {
matches!(&a.action_type, SmartActionType::OpenWithPreferred(name, _) if name == unconfigured_repo)
}) {
actions.push(SmartAction {
label: format!("⚙️ Configure templates for {}", unconfigured_repo),
description: "Set up advanced templates and automation".to_string(),
action_type: SmartActionType::ConfigureAndOpen(unconfigured_repo.clone()),
priority: 70, });
}
}
if self.workspace_state.unconfigured_repos.len() > 3 {
let count = self.workspace_state.unconfigured_repos.len();
actions.push(SmartAction {
label: format!("⚙️ Set up templates for {} repos", count),
description: "Configure advanced templates and automation".to_string(),
action_type: SmartActionType::QuickConfigureBatch(
self.workspace_state.unconfigured_repos.clone(),
),
priority: 60, });
}
actions.sort_by(|a, b| b.priority.cmp(&a.priority));
actions.truncate(5);
actions
}
}
pub fn create_menu_item(base_label: &str, context: Option<&str>) -> String {
match context {
Some(ctx) => format!("{base_label} {ctx}"),
None => base_label.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_smart_action_priority() {
let action1 = SmartAction {
label: "Action 1".to_string(),
description: "Test".to_string(),
action_type: SmartActionType::DiscoverRepos,
priority: 50,
};
let action2 = SmartAction {
label: "Action 2".to_string(),
description: "Test".to_string(),
action_type: SmartActionType::InstallApps,
priority: 100,
};
let mut actions = vec![action1, action2];
actions.sort_by(|a, b| b.priority.cmp(&a.priority));
assert_eq!(actions[0].priority, 100);
assert_eq!(actions[1].priority, 50);
}
}