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>), CreateRepository, DiscoverRepos, InstallApps, OpenRecent(String), SetupWorkspace, SyncRepositories, CleanupMissing, }
#[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>,
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,
});
}
if !self.workspace_state.unconfigured_repos.is_empty() {
let count = self.workspace_state.unconfigured_repos.len();
actions.push(SmartAction {
label: format!(
"โ๏ธ Configure apps for {} repo{}",
count,
if count == 1 { "" } else { "s" }
),
description: "Set up app integrations for repositories".to_string(),
action_type: SmartActionType::ConfigureApps(
self.workspace_state.unconfigured_repos.clone(),
),
priority: 80,
});
}
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: 70,
});
}
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: 60,
});
}
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: 50,
});
}
}
actions.push(SmartAction {
label: "๐ Create new repository".to_string(),
description: "Create a new local repository for prototyping".to_string(),
action_type: SmartActionType::CreateRepository,
priority: 35,
});
actions.push(SmartAction {
label: "๐ฅ Clone new repository".to_string(),
description: "Search and clone from GitHub".to_string(),
action_type: SmartActionType::CloneAndOpen("".to_string()),
priority: 30,
});
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 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);
}
}