// ======================================== // Hook Manager Page // ======================================== // Full CRUD page for managing CLI hooks import { useState, useMemo } from 'react'; import { useIntl } from 'react-intl'; import { GitFork, Plus, Search, RefreshCw, Zap, Wrench, CheckCircle, StopCircle, Wand2, Brain, Shield, Sparkles, } from 'lucide-react'; import { useMutation } from '@tanstack/react-query'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { EventGroup, HookFormDialog, HookQuickTemplates, HookWizard, type HookCardData, type HookFormData, type HookTriggerType, HOOK_TEMPLATES, type WizardType } from '@/components/hook'; import { useHooks, useToggleHook } from '@/hooks'; import { installHookTemplate, createHook } from '@/lib/api'; import { cn } from '@/lib/utils'; // ========== Types ========== interface HooksByTrigger { UserPromptSubmit: HookCardData[]; PreToolUse: HookCardData[]; PostToolUse: HookCardData[]; Stop: HookCardData[]; } // ========== Helper Functions ========== function isHookTriggerType(value: string): value is HookTriggerType { return ['UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'].includes(value); } function toHookCardData(hook: { name: string; description?: string; enabled: boolean; trigger: string; matcher?: string; command?: string; script?: string }): HookCardData | null { if (!isHookTriggerType(hook.trigger)) { return null; } return { name: hook.name, description: hook.description, enabled: hook.enabled, trigger: hook.trigger, matcher: hook.matcher, command: hook.command || hook.script, }; } function groupHooksByTrigger(hooks: HookCardData[]): HooksByTrigger { return { UserPromptSubmit: hooks.filter((h) => h.trigger === 'UserPromptSubmit'), PreToolUse: hooks.filter((h) => h.trigger === 'PreToolUse'), PostToolUse: hooks.filter((h) => h.trigger === 'PostToolUse'), Stop: hooks.filter((h) => h.trigger === 'Stop'), }; } function getTriggerStats(hooksByTrigger: HooksByTrigger) { return { UserPromptSubmit: { total: hooksByTrigger.UserPromptSubmit.length, enabled: hooksByTrigger.UserPromptSubmit.filter((h) => h.enabled).length, }, PreToolUse: { total: hooksByTrigger.PreToolUse.length, enabled: hooksByTrigger.PreToolUse.filter((h) => h.enabled).length, }, PostToolUse: { total: hooksByTrigger.PostToolUse.length, enabled: hooksByTrigger.PostToolUse.filter((h) => h.enabled).length, }, Stop: { total: hooksByTrigger.Stop.length, enabled: hooksByTrigger.Stop.filter((h) => h.enabled).length, }, }; } // ========== Main Page Component ========== export function HookManagerPage() { const { formatMessage } = useIntl(); const [searchQuery, setSearchQuery] = useState(''); const [dialogOpen, setDialogOpen] = useState(false); const [dialogMode, setDialogMode] = useState<'create' | 'edit'>('create'); const [editingHook, setEditingHook] = useState(); const [wizardOpen, setWizardOpen] = useState(false); const [wizardType, setWizardType] = useState('memory-update'); const { hooks, enabledCount, totalCount, isLoading, refetch } = useHooks(); const { toggleHook } = useToggleHook(); // Convert hooks to HookCardData and filter by search query const filteredHooks = useMemo(() => { const validHooks = hooks.map(toHookCardData).filter((h): h is HookCardData => h !== null); if (!searchQuery.trim()) return validHooks; const query = searchQuery.toLowerCase(); return validHooks.filter( (h) => h.name.toLowerCase().includes(query) || (h.description && h.description.toLowerCase().includes(query)) || h.trigger.toLowerCase().includes(query) || (h.command && h.command.toLowerCase().includes(query)) ); }, [hooks, searchQuery]); // Group hooks by trigger type const hooksByTrigger = useMemo(() => groupHooksByTrigger(filteredHooks), [filteredHooks]); // Get stats for each trigger type const triggerStats = useMemo(() => getTriggerStats(hooksByTrigger), [hooksByTrigger]); // Handlers const handleAddClick = () => { setDialogMode('create'); setEditingHook(undefined); setDialogOpen(true); }; const handleEditClick = (hook: HookCardData) => { setDialogMode('edit'); setEditingHook(hook); setDialogOpen(true); }; const handleDeleteClick = async (hookName: string) => { // This will be implemented when delete API is added console.log('Delete hook:', hookName); }; const handleSave = async (data: HookFormData) => { // This will be implemented when create/update APIs are added console.log('Save hook:', data); await refetch(); }; // ========== Wizard Handlers ========== const wizardTypes: Array<{ type: WizardType; icon: typeof Brain; label: string; description: string }> = [ { type: 'memory-update', icon: Brain, label: formatMessage({ id: 'cliHooks.wizards.memoryUpdate.title' }), description: formatMessage({ id: 'cliHooks.wizards.memoryUpdate.shortDescription' }), }, { type: 'danger-protection', icon: Shield, label: formatMessage({ id: 'cliHooks.wizards.dangerProtection.title' }), description: formatMessage({ id: 'cliHooks.wizards.dangerProtection.shortDescription' }), }, { type: 'skill-context', icon: Sparkles, label: formatMessage({ id: 'cliHooks.wizards.skillContext.title' }), description: formatMessage({ id: 'cliHooks.wizards.skillContext.shortDescription' }), }, ]; const handleLaunchWizard = (type: WizardType) => { setWizardType(type); setWizardOpen(true); }; const handleWizardComplete = async (hookConfig: { name: string; description: string; trigger: string; matcher?: string; command: string; }) => { await createHook(hookConfig); await refetch(); }; // ========== Quick Templates Logic ========== // Determine which templates are already installed const installedTemplates = useMemo(() => { return HOOK_TEMPLATES.filter(template => { return hooks.some(hook => { // Check if hook name contains template ID return hook.name.includes(template.id) || (hook.command && hook.command.includes(template.command)); }); }).map(t => t.id); }, [hooks]); // Mutation for installing templates const installMutation = useMutation({ mutationFn: async (templateId: string) => { return await installHookTemplate(templateId); }, onSuccess: () => { refetch(); }, }); const handleInstallTemplate = async (templateId: string) => { await installMutation.mutateAsync(templateId); }; const TRIGGER_TYPES: Array<{ type: HookTriggerType; icon: typeof Zap }> = [ { type: 'UserPromptSubmit', icon: Zap }, { type: 'PreToolUse', icon: Wrench }, { type: 'PostToolUse', icon: CheckCircle }, { type: 'Stop', icon: StopCircle }, ]; return (
{/* Page Header */}

{formatMessage({ id: 'cliHooks.title' })}

{formatMessage({ id: 'cliHooks.description' })}

{/* Stats Cards */}
{TRIGGER_TYPES.map(({ type, icon: Icon }) => { const stats = triggerStats[type]; return (

{formatMessage({ id: `cliHooks.trigger.${type}` })}

{stats.enabled}/{stats.total}

); })}
{/* Search and Global Stats */}
setSearchQuery(e.target.value)} className="pl-9" />
{formatMessage({ id: 'cliHooks.stats.total' }, { count: totalCount })} {formatMessage({ id: 'cliHooks.stats.enabled' }, { count: enabledCount })}
{/* Quick Templates */} {/* Wizard Launchers */}

{formatMessage({ id: 'cliHooks.wizards.sectionTitle' })}

{formatMessage({ id: 'cliHooks.wizards.sectionDescription' })}

{wizardTypes.map(({ type, icon: Icon, label, description }) => ( handleLaunchWizard(type)}>

{label}

{description}

))}
{/* Event Groups */}
{TRIGGER_TYPES.map(({ type }) => ( toggleHook(hookName, enabled)} onHookEdit={handleEditClick} onHookDelete={handleDeleteClick} /> ))}
{/* Empty State */} {!isLoading && filteredHooks.length === 0 && (

{formatMessage({ id: 'cliHooks.empty.title' })}

{formatMessage({ id: 'cliHooks.empty.description' })}

)} {/* Form Dialog */} setDialogOpen(false)} onSave={handleSave} /> {/* Hook Wizard */} setWizardOpen(false)} onComplete={handleWizardComplete} />
); } export default HookManagerPage;