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
This commit is contained in:
@@ -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<String, String> {
|
||||
pub async fn slash_command_delete(command_id: String, project_path: Option<String>) -> Result<String, String> {
|
||||
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)
|
||||
|
@@ -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<ClaudeCodeSessionProps> = ({
|
||||
const [timelineVersion, setTimelineVersion] = useState(0);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showForkDialog, setShowForkDialog] = useState(false);
|
||||
const [showSlashCommandsSettings, setShowSlashCommandsSettings] = useState(false);
|
||||
const [forkCheckpointId, setForkCheckpointId] = useState<string | null>(null);
|
||||
const [forkSessionName, setForkSessionName] = useState("");
|
||||
|
||||
@@ -995,6 +998,17 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
Hooks
|
||||
</Button>
|
||||
)}
|
||||
{projectPath && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowSlashCommandsSettings(true)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Command className="h-4 w-4 mr-2" />
|
||||
Commands
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{showSettings && (
|
||||
<CheckpointSettings
|
||||
@@ -1386,6 +1400,23 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* Slash Commands Settings Dialog */}
|
||||
{showSlashCommandsSettings && (
|
||||
<Dialog open={showSlashCommandsSettings} onOpenChange={setShowSlashCommandsSettings}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Slash Commands</DialogTitle>
|
||||
<DialogDescription>
|
||||
Manage project-specific slash commands for {projectPath}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<SlashCommandsManager projectPath={projectPath} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -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<ProjectSettingsProps> = ({
|
||||
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<ProjectSettingsProps> = ({
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-muted-foreground" />
|
||||
<h2 className="text-xl font-semibold">Hooks</h2>
|
||||
<h2 className="text-xl font-semibold">Project Settings</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,6 +108,10 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
<div className="p-6">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="mb-6">
|
||||
<TabsTrigger value="commands" className="gap-2">
|
||||
<Command className="h-4 w-4" />
|
||||
Slash Commands
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="project" className="gap-2">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
Project Hooks
|
||||
@@ -116,6 +122,26 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="commands" className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Project Slash Commands</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Custom commands that are specific to this project. These commands are stored in
|
||||
<code className="mx-1 px-2 py-1 bg-muted rounded text-xs">.claude/slash-commands/</code>
|
||||
and can be committed to version control.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SlashCommandsManager
|
||||
projectPath={project.path}
|
||||
scopeFilter="project"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="project" className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
|
@@ -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<SlashCommandsManagerProps> = ({
|
||||
projectPath,
|
||||
className,
|
||||
scopeFilter = 'all',
|
||||
}) => {
|
||||
const [commands, setCommands] = useState<SlashCommand[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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<Set<string>>(new Set());
|
||||
|
||||
// Edit dialog state
|
||||
@@ -109,6 +111,11 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
|
||||
scope: 'user'
|
||||
});
|
||||
|
||||
// Delete confirmation dialog state
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [commandToDelete, setCommandToDelete] = useState<SlashCommand | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// Load commands on mount
|
||||
useEffect(() => {
|
||||
loadCommands();
|
||||
@@ -136,7 +143,7 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
|
||||
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<SlashCommandsManagerProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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<SlashCommandsManagerProps> = ({
|
||||
|
||||
// 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<SlashCommandsManagerProps> = ({
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Slash Commands</h3>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{scopeFilter === 'project' ? 'Project Slash Commands' : 'Slash Commands'}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Create custom commands to streamline your workflow
|
||||
{scopeFilter === 'project'
|
||||
? 'Create custom commands for this project'
|
||||
: 'Create custom commands to streamline your workflow'}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleCreateNew} size="sm" className="gap-2">
|
||||
@@ -286,16 +322,18 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={selectedScope} onValueChange={(value: any) => setSelectedScope(value)}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Commands</SelectItem>
|
||||
<SelectItem value="project">Project</SelectItem>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{scopeFilter === 'all' && (
|
||||
<Select value={selectedScope} onValueChange={(value: any) => setSelectedScope(value)}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Commands</SelectItem>
|
||||
<SelectItem value="project">Project</SelectItem>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
@@ -316,11 +354,17 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
|
||||
<div className="text-center">
|
||||
<Command className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{searchQuery ? "No commands found" : "No commands created yet"}
|
||||
{searchQuery
|
||||
? "No commands found"
|
||||
: scopeFilter === 'project'
|
||||
? "No project commands created yet"
|
||||
: "No commands created yet"}
|
||||
</p>
|
||||
{!searchQuery && (
|
||||
<Button onClick={handleCreateNew} variant="outline" size="sm" className="mt-4">
|
||||
Create your first command
|
||||
{scopeFilter === 'project'
|
||||
? "Create your first project command"
|
||||
: "Create your first command"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -414,7 +458,7 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(command)}
|
||||
onClick={() => handleDeleteClick(command)}
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
@@ -465,24 +509,28 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
|
||||
<Select
|
||||
value={commandForm.scope}
|
||||
onValueChange={(value: 'project' | 'user') => setCommandForm(prev => ({ ...prev, scope: value }))}
|
||||
disabled={!projectPath && commandForm.scope === 'project'}
|
||||
disabled={scopeFilter !== 'all' || (!projectPath && commandForm.scope === 'project')}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
User (Global)
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="project" disabled={!projectPath}>
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
Project
|
||||
</div>
|
||||
</SelectItem>
|
||||
{(scopeFilter === 'all' || scopeFilter === 'user') && (
|
||||
<SelectItem value="user">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
User (Global)
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
{(scopeFilter === 'all' || scopeFilter === 'project') && (
|
||||
<SelectItem value="project" disabled={!projectPath}>
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
Project
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -619,6 +667,53 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Command</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<p>Are you sure you want to delete this command?</p>
|
||||
{commandToDelete && (
|
||||
<div className="p-3 bg-muted rounded-md">
|
||||
<code className="text-sm font-mono">{commandToDelete.full_command}</code>
|
||||
{commandToDelete.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{commandToDelete.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This action cannot be undone. The command file will be permanently deleted.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={cancelDelete} disabled={deleting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmDelete}
|
||||
disabled={deleting}
|
||||
>
|
||||
{deleting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -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<string> {
|
||||
async slashCommandDelete(commandId: string, projectPath?: string): Promise<string> {
|
||||
try {
|
||||
return await invoke<string>("slash_command_delete", { commandId });
|
||||
return await invoke<string>("slash_command_delete", { commandId, projectPath });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete slash command:", error);
|
||||
throw error;
|
||||
|
Reference in New Issue
Block a user