use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use tauri::AppHandle; use tauri_plugin_store::StoreExt; const QUICK_ACTIONS_STORE_KEY: &str = "quick_actions"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QuickAction { pub id: String, pub name: String, pub prompt: String, pub icon: String, pub is_default: bool, pub created_at: DateTime, pub updated_at: DateTime, } fn get_default_quick_actions() -> Vec { let now = Utc::now(); vec![ QuickAction { id: "default-review-pr".to_string(), name: "Review PR".to_string(), prompt: "Please review this pull request and provide feedback on code quality, potential issues, and suggestions for improvement.".to_string(), icon: "git-pull-request".to_string(), is_default: true, created_at: now, updated_at: now, }, QuickAction { id: "default-run-tests".to_string(), name: "Run Tests".to_string(), prompt: "Please run the test suite for this project and report any failures or issues.".to_string(), icon: "play".to_string(), is_default: true, created_at: now, updated_at: now, }, QuickAction { id: "default-explain-file".to_string(), name: "Explain File".to_string(), prompt: "Please explain what this file does, its purpose, and how it fits into the overall project structure.".to_string(), icon: "file-text".to_string(), is_default: true, created_at: now, updated_at: now, }, QuickAction { id: "default-fix-error".to_string(), name: "Fix Error".to_string(), prompt: "I'm getting an error. Can you help me identify the cause and fix it?".to_string(), icon: "alert-circle".to_string(), is_default: true, created_at: now, updated_at: now, }, QuickAction { id: "default-write-tests".to_string(), name: "Write Tests".to_string(), prompt: "Please write comprehensive unit tests for the current code with good coverage.".to_string(), icon: "check-square".to_string(), is_default: true, created_at: now, updated_at: now, }, QuickAction { id: "default-refactor".to_string(), name: "Refactor".to_string(), prompt: "Please refactor this code to improve readability, maintainability, and performance.".to_string(), icon: "refresh-cw".to_string(), is_default: true, created_at: now, updated_at: now, }, ] } fn load_all_quick_actions(app: &AppHandle) -> Result, String> { let store = app .store("hikari-quick-actions.json") .map_err(|e| e.to_string())?; match store.get(QUICK_ACTIONS_STORE_KEY) { Some(value) => { let mut actions: Vec = serde_json::from_value(value.clone()).map_err(|e| e.to_string())?; let defaults = get_default_quick_actions(); for default in defaults { if !actions.iter().any(|a| a.id == default.id) { actions.push(default); } } Ok(actions) } None => Ok(get_default_quick_actions()), } } fn save_all_quick_actions(app: &AppHandle, actions: &[QuickAction]) -> Result<(), String> { let store = app .store("hikari-quick-actions.json") .map_err(|e| e.to_string())?; let value = serde_json::to_value(actions).map_err(|e| e.to_string())?; store.set(QUICK_ACTIONS_STORE_KEY, value); store.save().map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub async fn list_quick_actions(app: AppHandle) -> Result, String> { let mut actions = load_all_quick_actions(&app)?; actions.sort_by(|a, b| { let default_cmp = b.is_default.cmp(&a.is_default); if default_cmp == std::cmp::Ordering::Equal { a.name.cmp(&b.name) } else { default_cmp } }); Ok(actions) } #[tauri::command] pub async fn save_quick_action(app: AppHandle, action: QuickAction) -> Result<(), String> { let mut actions = load_all_quick_actions(&app)?; if let Some(existing) = actions.iter_mut().find(|a| a.id == action.id) { let mut updated = action; updated.is_default = existing.is_default; *existing = updated; } else { actions.push(action); } save_all_quick_actions(&app, &actions) } #[tauri::command] pub async fn delete_quick_action(app: AppHandle, action_id: String) -> Result<(), String> { let mut actions = load_all_quick_actions(&app)?; if actions .iter() .any(|a| a.id == action_id && a.is_default) { return Err("Cannot delete default quick actions".to_string()); } actions.retain(|a| a.id != action_id); save_all_quick_actions(&app, &actions) } #[tauri::command] pub async fn reset_default_quick_actions(app: AppHandle) -> Result<(), String> { let mut actions = load_all_quick_actions(&app)?; actions.retain(|a| !a.is_default); actions.extend(get_default_quick_actions()); save_all_quick_actions(&app, &actions) } #[cfg(test)] mod tests { use super::*; fn create_test_action(id: &str, name: &str, is_default: bool) -> QuickAction { QuickAction { id: id.to_string(), name: name.to_string(), prompt: "Test prompt".to_string(), icon: "star".to_string(), is_default, created_at: Utc::now(), updated_at: Utc::now(), } } #[test] fn test_default_quick_actions_exist() { let defaults = get_default_quick_actions(); assert!(!defaults.is_empty()); assert!(defaults.iter().all(|a| a.is_default)); } #[test] fn test_default_quick_actions_have_required_fields() { let defaults = get_default_quick_actions(); for action in defaults { assert!(!action.id.is_empty()); assert!(!action.name.is_empty()); assert!(!action.prompt.is_empty()); assert!(!action.icon.is_empty()); } } #[test] fn test_default_quick_actions_count() { let defaults = get_default_quick_actions(); // Should have 6 default actions assert_eq!(defaults.len(), 6); } #[test] fn test_default_quick_actions_have_unique_ids() { let defaults = get_default_quick_actions(); let mut ids: Vec<&String> = defaults.iter().map(|a| &a.id).collect(); ids.sort(); ids.dedup(); assert_eq!(ids.len(), defaults.len()); } #[test] fn test_default_quick_actions_ids_start_with_default() { let defaults = get_default_quick_actions(); assert!(defaults.iter().all(|a| a.id.starts_with("default-"))); } #[test] fn test_quick_action_serialization() { let action = create_test_action("test-1", "Test Action", false); let json = serde_json::to_string(&action).expect("Failed to serialize"); let parsed: QuickAction = serde_json::from_str(&json).expect("Failed to deserialize"); assert_eq!(parsed.id, action.id); assert_eq!(parsed.name, action.name); assert_eq!(parsed.prompt, action.prompt); assert_eq!(parsed.icon, action.icon); assert_eq!(parsed.is_default, action.is_default); } #[test] fn test_quick_action_clone() { let original = create_test_action("clone-test", "Clone Test", true); let cloned = original.clone(); assert_eq!(original.id, cloned.id); assert_eq!(original.name, cloned.name); assert_eq!(original.is_default, cloned.is_default); } #[test] #[allow(clippy::useless_vec)] fn test_quick_action_sorting_defaults_first() { let mut actions = vec![ create_test_action("custom-z", "Zebra", false), create_test_action("default-a", "Apple", true), create_test_action("custom-a", "Alpha", false), create_test_action("default-z", "Zulu", true), ]; // Sort by: defaults first, then alphabetically by name actions.sort_by(|a, b| { let default_cmp = b.is_default.cmp(&a.is_default); if default_cmp == std::cmp::Ordering::Equal { a.name.cmp(&b.name) } else { default_cmp } }); // Defaults should come first assert!(actions[0].is_default); assert!(actions[1].is_default); assert!(!actions[2].is_default); assert!(!actions[3].is_default); // Within defaults, alphabetically sorted assert_eq!(actions[0].name, "Apple"); assert_eq!(actions[1].name, "Zulu"); // Within non-defaults, alphabetically sorted assert_eq!(actions[2].name, "Alpha"); assert_eq!(actions[3].name, "Zebra"); } #[test] fn test_known_default_actions() { let defaults = get_default_quick_actions(); let ids: Vec<&str> = defaults.iter().map(|a| a.id.as_str()).collect(); assert!(ids.contains(&"default-review-pr")); assert!(ids.contains(&"default-run-tests")); assert!(ids.contains(&"default-explain-file")); assert!(ids.contains(&"default-fix-error")); assert!(ids.contains(&"default-write-tests")); assert!(ids.contains(&"default-refactor")); } #[test] fn test_default_action_icons() { let defaults = get_default_quick_actions(); let icons: Vec<&str> = defaults.iter().map(|a| a.icon.as_str()).collect(); assert!(icons.contains(&"git-pull-request")); assert!(icons.contains(&"play")); assert!(icons.contains(&"file-text")); assert!(icons.contains(&"alert-circle")); assert!(icons.contains(&"check-square")); assert!(icons.contains(&"refresh-cw")); } #[test] fn test_quick_action_prompts_not_empty() { let defaults = get_default_quick_actions(); for action in defaults { assert!( action.prompt.len() > 10, "Prompt should be meaningful: {}", action.name ); } } #[test] fn test_quick_action_timestamps() { let action = create_test_action("time-test", "Time Test", false); assert!(action.created_at <= action.updated_at); } #[test] fn test_default_actions_have_same_timestamps() { let defaults = get_default_quick_actions(); // All defaults are created at the same instant let first_created = defaults[0].created_at; let first_updated = defaults[0].updated_at; for action in &defaults { assert_eq!(action.created_at, first_created); assert_eq!(action.updated_at, first_updated); } } #[test] fn test_action_retain_non_default() { let mut actions = vec![ create_test_action("default-1", "Default 1", true), create_test_action("custom-1", "Custom 1", false), create_test_action("default-2", "Default 2", true), create_test_action("custom-2", "Custom 2", false), ]; // Mimics reset_default_quick_actions behavior (retain non-defaults) actions.retain(|a| !a.is_default); assert_eq!(actions.len(), 2); assert!(actions.iter().all(|a| !a.is_default)); } #[test] #[allow(clippy::useless_vec)] fn test_action_find_by_id() { let actions = vec![ create_test_action("action-1", "First", false), create_test_action("action-2", "Second", false), create_test_action("action-3", "Third", false), ]; let found = actions.iter().find(|a| a.id == "action-2"); assert!(found.is_some()); assert_eq!(found.unwrap().name, "Second"); let not_found = actions.iter().find(|a| a.id == "action-999"); assert!(not_found.is_none()); } }