From 02cc8bd0d5ed7fe597239966147c508a293f0529 Mon Sep 17 00:00:00 2001 From: Hikari Date: Sun, 25 Jan 2026 16:25:59 -0800 Subject: [PATCH] feat: add quick actions panel for common prompts - Add Rust backend for managing quick actions with persistent storage - Create QuickActionsPanel component with edit/delete functionality - Add quickActions store for frontend state management - Move Actions and Snippets buttons to input controls row - Include 6 default quick actions: Review PR, Run Tests, Explain File, Fix Error, Write Tests, and Refactor - Support custom quick action creation and management Closes #15 --- src-tauri/src/lib.rs | 6 + src-tauri/src/quick_actions.rs | 191 +++++++++ src/lib/components/InputBar.svelte | 138 +++++- src/lib/components/QuickActionsPanel.svelte | 451 ++++++++++++++++++++ src/lib/stores/quickActions.ts | 118 +++++ 5 files changed, 881 insertions(+), 23 deletions(-) create mode 100644 src-tauri/src/quick_actions.rs create mode 100644 src/lib/components/QuickActionsPanel.svelte create mode 100644 src/lib/stores/quickActions.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 16ab2df..fc3e0b3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ mod commands; mod config; mod git; mod notifications; +mod quick_actions; mod sessions; mod snippets; mod stats; @@ -20,6 +21,7 @@ use commands::load_saved_achievements; use commands::*; use git::*; use notifications::*; +use quick_actions::*; use sessions::*; use snippets::*; use tauri::Manager; @@ -120,6 +122,10 @@ pub fn run() { delete_snippet, get_snippet_categories, reset_default_snippets, + list_quick_actions, + save_quick_action, + delete_quick_action, + reset_default_quick_actions, git_status, git_diff, git_branches, diff --git a/src-tauri/src/quick_actions.rs b/src-tauri/src/quick_actions.rs new file mode 100644 index 0000000..92be5c5 --- /dev/null +++ b/src-tauri/src/quick_actions.rs @@ -0,0 +1,191 @@ +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::*; + + #[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()); + } + } +} diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 5df9435..2b6b099 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -26,6 +26,7 @@ } from "$lib/commands/slashCommands"; import AttachmentPreview from "$lib/components/AttachmentPreview.svelte"; import SnippetLibraryPanel from "$lib/components/SnippetLibraryPanel.svelte"; + import QuickActionsPanel from "$lib/components/QuickActionsPanel.svelte"; import type { Attachment } from "$lib/types/messages"; const INPUT_HISTORY_KEY = "hikari-input-history"; @@ -41,6 +42,7 @@ let attachments = $state([]); let isDragging = $state(false); let showSnippetLibrary = $state(false); + let showQuickActions = $state(false); // Input history state let inputHistory = $state([]); @@ -629,6 +631,42 @@ User: ${formattedMessage}`; userHasTyped = true; } + async function handleQuickAction(prompt: string): Promise { + // Quick actions send the prompt directly + if (!isConnected || isSubmitting) return; + + // Add to history + addToHistory(prompt); + historyIndex = -1; + tempInput = ""; + userHasTyped = false; + + isSubmitting = true; + + // Reset notification state for new user message + handleNewUserMessage(); + + claudeStore.addLine("user", prompt); + characterState.setState("thinking"); + + try { + const conversationId = get(claudeStore.activeConversationId); + if (!conversationId) { + throw new Error("No active conversation"); + } + await invoke("send_prompt", { + conversationId, + message: prompt, + }); + } catch (error) { + console.error("Failed to send quick action:", error); + claudeStore.addLine("error", `Failed to send: ${error}`); + characterState.setTemporaryState("error", 3000); + } finally { + isSubmitting = false; + } + } + function handleKeyDown(event: KeyboardEvent) { // Handle command menu navigation if (showCommandMenu && matchingCommands.length > 0) { @@ -705,6 +743,50 @@ User: ${formattedMessage}`;
+ +
@@ -735,29 +817,6 @@ User: ${formattedMessage}`;
-