From cee71343f5ddf98822bff36caf785601132d6158 Mon Sep 17 00:00:00 2001 From: Vivek R <123vivekr@gmail.com> Date: Mon, 7 Jul 2025 23:56:09 +0530 Subject: [PATCH] feat: enhance project-specific slash commands management - Add scopeFilter prop to SlashCommandsManager for filtering by scope - Replace browser confirm() with proper delete confirmation dialog - Fix slash_command_delete to handle project commands with project_path param - Add Slash Commands tab to ProjectSettings as the default tab - Add Commands button to ClaudeCodeSession for quick access - Improve error handling and user feedback for delete operations - Better UI text when showing project-specific commands only --- src-tauri/src/commands/slash_commands.rs | 20 ++- src/components/ClaudeCodeSession.tsx | 33 ++++- src/components/ProjectSettings.tsx | 32 ++++- src/components/SlashCommandsManager.tsx | 169 ++++++++++++++++++----- src/lib/api.ts | 5 +- 5 files changed, 213 insertions(+), 46 deletions(-) diff --git a/src-tauri/src/commands/slash_commands.rs b/src-tauri/src/commands/slash_commands.rs index 82bf71e..dbf12e6 100644 --- a/src-tauri/src/commands/slash_commands.rs +++ b/src-tauri/src/commands/slash_commands.rs @@ -416,11 +416,25 @@ pub async fn slash_command_save( /// Delete a slash command #[tauri::command] -pub async fn slash_command_delete(command_id: String) -> Result { +pub async fn slash_command_delete(command_id: String, project_path: Option) -> Result { info!("Deleting slash command: {}", command_id); - // Get the command to find its file path - let command = slash_command_get(command_id.clone()).await?; + // First, we need to determine if this is a project command by parsing the ID + let is_project_command = command_id.starts_with("project-"); + + // If it's a project command and we don't have a project path, error out + if is_project_command && project_path.is_none() { + return Err("Project path required to delete project commands".to_string()); + } + + // List all commands (including project commands if applicable) + let commands = slash_commands_list(project_path).await?; + + // Find the command by ID + let command = commands + .into_iter() + .find(|cmd| cmd.id == command_id) + .ok_or_else(|| format!("Command not found: {}", command_id))?; // Delete the file fs::remove_file(&command.file_path) diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index 2c8e8b5..2dc9a9b 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -10,7 +10,8 @@ import { Settings, ChevronUp, X, - Hash + Hash, + Command } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -25,6 +26,7 @@ import { FloatingPromptInput, type FloatingPromptInputRef } from "./FloatingProm import { ErrorBoundary } from "./ErrorBoundary"; import { TimelineNavigator } from "./TimelineNavigator"; import { CheckpointSettings } from "./CheckpointSettings"; +import { SlashCommandsManager } from "./SlashCommandsManager"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { SplitPane } from "@/components/ui/split-pane"; @@ -87,6 +89,7 @@ export const ClaudeCodeSession: React.FC = ({ const [timelineVersion, setTimelineVersion] = useState(0); const [showSettings, setShowSettings] = useState(false); const [showForkDialog, setShowForkDialog] = useState(false); + const [showSlashCommandsSettings, setShowSlashCommandsSettings] = useState(false); const [forkCheckpointId, setForkCheckpointId] = useState(null); const [forkSessionName, setForkSessionName] = useState(""); @@ -995,6 +998,17 @@ export const ClaudeCodeSession: React.FC = ({ Hooks )} + {projectPath && ( + + )}
{showSettings && ( = ({ )} + + {/* Slash Commands Settings Dialog */} + {showSlashCommandsSettings && ( + + + + Slash Commands + + Manage project-specific slash commands for {projectPath} + + +
+ +
+
+
+ )}
); }; diff --git a/src/components/ProjectSettings.tsx b/src/components/ProjectSettings.tsx index 3383a67..bbc8283 100644 --- a/src/components/ProjectSettings.tsx +++ b/src/components/ProjectSettings.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react'; import { HooksEditor } from '@/components/HooksEditor'; +import { SlashCommandsManager } from '@/components/SlashCommandsManager'; import { api } from '@/lib/api'; import { AlertTriangle, @@ -11,7 +12,8 @@ import { Settings, FolderOpen, GitBranch, - Shield + Shield, + Command } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; @@ -31,7 +33,7 @@ export const ProjectSettings: React.FC = ({ onBack, className }) => { - const [activeTab, setActiveTab] = useState('project'); + const [activeTab, setActiveTab] = useState('commands'); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); // Other hooks settings @@ -88,7 +90,7 @@ export const ProjectSettings: React.FC = ({
-

Hooks

+

Project Settings

@@ -106,6 +108,10 @@ export const ProjectSettings: React.FC = ({
+ + + Slash Commands + Project Hooks @@ -116,6 +122,26 @@ export const ProjectSettings: React.FC = ({ + + +
+
+

Project Slash Commands

+

+ Custom commands that are specific to this project. These commands are stored in + .claude/slash-commands/ + and can be committed to version control. +

+
+ + +
+
+
+
diff --git a/src/components/SlashCommandsManager.tsx b/src/components/SlashCommandsManager.tsx index 62e1646..ef46cf1 100644 --- a/src/components/SlashCommandsManager.tsx +++ b/src/components/SlashCommandsManager.tsx @@ -33,6 +33,7 @@ import { COMMON_TOOL_MATCHERS } from "@/types/hooks"; interface SlashCommandsManagerProps { projectPath?: string; className?: string; + scopeFilter?: 'project' | 'user' | 'all'; } interface CommandForm { @@ -88,13 +89,14 @@ const getCommandIcon = (command: SlashCommand) => { export const SlashCommandsManager: React.FC = ({ projectPath, className, + scopeFilter = 'all', }) => { const [commands, setCommands] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(""); - const [selectedScope, setSelectedScope] = useState<'all' | 'project' | 'user'>('all'); + const [selectedScope, setSelectedScope] = useState<'all' | 'project' | 'user'>(scopeFilter === 'all' ? 'all' : scopeFilter as 'project' | 'user'); const [expandedCommands, setExpandedCommands] = useState>(new Set()); // Edit dialog state @@ -109,6 +111,11 @@ export const SlashCommandsManager: React.FC = ({ scope: 'user' }); + // Delete confirmation dialog state + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [commandToDelete, setCommandToDelete] = useState(null); + const [deleting, setDeleting] = useState(false); + // Load commands on mount useEffect(() => { loadCommands(); @@ -136,7 +143,7 @@ export const SlashCommandsManager: React.FC = ({ content: "", description: "", allowedTools: [], - scope: projectPath ? 'project' : 'user' + scope: scopeFilter !== 'all' ? scopeFilter : (projectPath ? 'project' : 'user') }); setEditDialogOpen(true); }; @@ -179,20 +186,35 @@ export const SlashCommandsManager: React.FC = ({ } }; - const handleDelete = async (command: SlashCommand) => { - if (!confirm(`Delete command "${command.full_command}"?`)) { - return; - } + const handleDeleteClick = (command: SlashCommand) => { + setCommandToDelete(command); + setDeleteDialogOpen(true); + }; + + const confirmDelete = async () => { + if (!commandToDelete) return; try { - await api.slashCommandDelete(command.id); + setDeleting(true); + setError(null); + await api.slashCommandDelete(commandToDelete.id, projectPath); + setDeleteDialogOpen(false); + setCommandToDelete(null); await loadCommands(); } catch (err) { console.error("Failed to delete command:", err); - setError("Failed to delete command"); + const errorMessage = err instanceof Error ? err.message : "Failed to delete command"; + setError(errorMessage); + } finally { + setDeleting(false); } }; + const cancelDelete = () => { + setDeleteDialogOpen(false); + setCommandToDelete(null); + }; + const toggleExpanded = (commandId: string) => { setExpandedCommands(prev => { const next = new Set(prev); @@ -226,6 +248,16 @@ export const SlashCommandsManager: React.FC = ({ // Filter commands const filteredCommands = commands.filter(cmd => { + // Hide default commands + if (cmd.scope === 'default') { + return false; + } + + // Apply scopeFilter if set to specific scope + if (scopeFilter !== 'all' && cmd.scope !== scopeFilter) { + return false; + } + // Scope filter if (selectedScope !== 'all' && cmd.scope !== selectedScope) { return false; @@ -262,9 +294,13 @@ export const SlashCommandsManager: React.FC = ({ {/* Header */}
-

Slash Commands

+

+ {scopeFilter === 'project' ? 'Project Slash Commands' : 'Slash Commands'} +

- Create custom commands to streamline your workflow + {scopeFilter === 'project' + ? 'Create custom commands for this project' + : 'Create custom commands to streamline your workflow'}

- + {scopeFilter === 'all' && ( + + )}
{/* Error Message */} @@ -316,11 +354,17 @@ export const SlashCommandsManager: React.FC = ({

- {searchQuery ? "No commands found" : "No commands created yet"} + {searchQuery + ? "No commands found" + : scopeFilter === 'project' + ? "No project commands created yet" + : "No commands created yet"}

{!searchQuery && ( )}
@@ -414,7 +458,7 @@ export const SlashCommandsManager: React.FC = ({ + + + + ); -}; \ No newline at end of file +}; diff --git a/src/lib/api.ts b/src/lib/api.ts index 9adbc25..f7ff198 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1825,11 +1825,12 @@ export const api = { /** * Deletes a slash command * @param commandId - Unique identifier of the command to delete + * @param projectPath - Optional project path for deleting project commands * @returns Promise resolving to deletion message */ - async slashCommandDelete(commandId: string): Promise { + async slashCommandDelete(commandId: string, projectPath?: string): Promise { try { - return await invoke("slash_command_delete", { commandId }); + return await invoke("slash_command_delete", { commandId, projectPath }); } catch (error) { console.error("Failed to delete slash command:", error); throw error;