mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat(a2ui): Implement A2UI backend with question handling and WebSocket support
- Added A2UITypes for defining question structures and answers. - Created A2UIWebSocketHandler for managing WebSocket connections and message handling. - Developed ask-question tool for interactive user questions via A2UI. - Introduced platformUtils for platform detection and shell command handling. - Centralized TypeScript types in index.ts for better organization. - Implemented compatibility checks for hook templates based on platform requirements.
This commit is contained in:
194
ccw/frontend/src/components/hook/EventGroup.tsx
Normal file
194
ccw/frontend/src/components/hook/EventGroup.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
// ========================================
|
||||
// Event Group Component
|
||||
// ========================================
|
||||
// Groups hooks by trigger event type
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Zap,
|
||||
Wrench,
|
||||
CheckCircle,
|
||||
StopCircle,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { HookCard, type HookCardData, type HookTriggerType } from './HookCard';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface EventGroupProps {
|
||||
eventType: HookTriggerType;
|
||||
hooks: HookCardData[];
|
||||
onHookToggle: (hookName: string, enabled: boolean) => void;
|
||||
onHookEdit: (hook: HookCardData) => void;
|
||||
onHookDelete: (hookName: string) => void;
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function getEventIcon(eventType: HookTriggerType) {
|
||||
switch (eventType) {
|
||||
case 'UserPromptSubmit':
|
||||
return Zap;
|
||||
case 'PreToolUse':
|
||||
return Wrench;
|
||||
case 'PostToolUse':
|
||||
return CheckCircle;
|
||||
case 'Stop':
|
||||
return StopCircle;
|
||||
}
|
||||
}
|
||||
|
||||
function getEventColor(eventType: HookTriggerType): string {
|
||||
switch (eventType) {
|
||||
case 'UserPromptSubmit':
|
||||
return 'text-amber-500 bg-amber-500/10';
|
||||
case 'PreToolUse':
|
||||
return 'text-blue-500 bg-blue-500/10';
|
||||
case 'PostToolUse':
|
||||
return 'text-green-500 bg-green-500/10';
|
||||
case 'Stop':
|
||||
return 'text-red-500 bg-red-500/10';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Component ==========
|
||||
|
||||
export function EventGroup({
|
||||
eventType,
|
||||
hooks,
|
||||
onHookToggle,
|
||||
onHookEdit,
|
||||
onHookDelete,
|
||||
}: EventGroupProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [expandedHooks, setExpandedHooks] = useState<Set<string>>(new Set());
|
||||
|
||||
const Icon = getEventIcon(eventType);
|
||||
const iconColorClass = getEventColor(eventType);
|
||||
|
||||
const enabledCount = hooks.filter((h) => h.enabled).length;
|
||||
const totalCount = hooks.length;
|
||||
|
||||
const handleToggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
const handleToggleHookExpand = (hookName: string) => {
|
||||
setExpandedHooks((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(hookName)) {
|
||||
next.delete(hookName);
|
||||
} else {
|
||||
next.add(hookName);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleExpandAll = () => {
|
||||
setExpandedHooks(new Set(hooks.map((h) => h.name)));
|
||||
};
|
||||
|
||||
const handleCollapseAll = () => {
|
||||
setExpandedHooks(new Set());
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
{/* Event Header */}
|
||||
<div
|
||||
className="p-4 cursor-pointer hover:bg-muted/50 transition-colors border-b border-border"
|
||||
onClick={handleToggleExpand}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn('p-2 rounded-lg', iconColorClass)}>
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-foreground">
|
||||
{formatMessage({ id: `cliHooks.trigger.${eventType}` })}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.stats.count' }, {
|
||||
enabled: enabledCount,
|
||||
total: totalCount
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{totalCount}
|
||||
</Badge>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-5 h-5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hooks List */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3 bg-muted/10">
|
||||
{totalCount === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p className="text-sm">{formatMessage({ id: 'cliHooks.empty.noHooksInEvent' })}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Expand/Collapse All */}
|
||||
{totalCount > 1 && (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={handleExpandAll}
|
||||
>
|
||||
{formatMessage({ id: 'cliHooks.actions.expandAll' })}
|
||||
</Button>
|
||||
<span className="text-muted-foreground text-xs">/</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={handleCollapseAll}
|
||||
>
|
||||
{formatMessage({ id: 'cliHooks.actions.collapseAll' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hook Cards */}
|
||||
<div className="space-y-2">
|
||||
{hooks.map((hook) => (
|
||||
<HookCard
|
||||
key={hook.name}
|
||||
hook={hook}
|
||||
isExpanded={expandedHooks.has(hook.name)}
|
||||
onToggleExpand={() => handleToggleHookExpand(hook.name)}
|
||||
onToggle={onHookToggle}
|
||||
onEdit={onHookEdit}
|
||||
onDelete={onHookDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventGroup;
|
||||
238
ccw/frontend/src/components/hook/HookCard.tsx
Normal file
238
ccw/frontend/src/components/hook/HookCard.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
// ========================================
|
||||
// Hook Card Component
|
||||
// ========================================
|
||||
// Individual hook display card with actions
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
GitFork,
|
||||
Power,
|
||||
PowerOff,
|
||||
Edit,
|
||||
Trash2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export type HookTriggerType = 'UserPromptSubmit' | 'PreToolUse' | 'PostToolUse' | 'Stop';
|
||||
|
||||
export interface HookCardData {
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
trigger: HookTriggerType;
|
||||
matcher?: string;
|
||||
command?: string;
|
||||
script?: string;
|
||||
}
|
||||
|
||||
export interface HookCardProps {
|
||||
hook: HookCardData;
|
||||
isExpanded: boolean;
|
||||
onToggleExpand: () => void;
|
||||
onToggle: (hookName: string, enabled: boolean) => void;
|
||||
onEdit: (hook: HookCardData) => void;
|
||||
onDelete: (hookName: string) => void;
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function getTriggerIcon(trigger: HookTriggerType) {
|
||||
switch (trigger) {
|
||||
case 'UserPromptSubmit':
|
||||
return '⚡';
|
||||
case 'PreToolUse':
|
||||
return '🔧';
|
||||
case 'PostToolUse':
|
||||
return '✅';
|
||||
case 'Stop':
|
||||
return '🛑';
|
||||
default:
|
||||
return '📌';
|
||||
}
|
||||
}
|
||||
|
||||
function getTriggerVariant(trigger: HookTriggerType): 'default' | 'secondary' | 'outline' {
|
||||
switch (trigger) {
|
||||
case 'UserPromptSubmit':
|
||||
return 'default';
|
||||
case 'PreToolUse':
|
||||
return 'secondary';
|
||||
case 'PostToolUse':
|
||||
return 'outline';
|
||||
case 'Stop':
|
||||
return 'secondary';
|
||||
default:
|
||||
return 'outline';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Component ==========
|
||||
|
||||
export function HookCard({
|
||||
hook,
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
onToggle,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: HookCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const handleToggle = () => {
|
||||
onToggle(hook.name, !hook.enabled);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
onEdit(hook);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (confirm(formatMessage({ id: 'cliHooks.actions.deleteConfirm' }, { hookName: hook.name }))) {
|
||||
onDelete(hook.name);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={cn('overflow-hidden', !hook.enabled && 'opacity-60')}>
|
||||
{/* Header */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className={cn(
|
||||
'p-2 rounded-lg flex-shrink-0',
|
||||
hook.enabled ? 'bg-primary/10' : 'bg-muted'
|
||||
)}>
|
||||
<GitFork className={cn(
|
||||
'w-4 h-4',
|
||||
hook.enabled ? 'text-primary' : 'text-muted-foreground'
|
||||
)} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium text-foreground truncate">
|
||||
{hook.name}
|
||||
</span>
|
||||
<Badge
|
||||
variant={getTriggerVariant(hook.trigger)}
|
||||
className="text-xs flex-shrink-0"
|
||||
>
|
||||
<span className="mr-1">{getTriggerIcon(hook.trigger)}</span>
|
||||
{formatMessage({ id: `cliHooks.trigger.${hook.trigger}` })}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={hook.enabled ? 'default' : 'secondary'}
|
||||
className="text-xs flex-shrink-0"
|
||||
>
|
||||
{hook.enabled
|
||||
? formatMessage({ id: 'common.status.enabled' })
|
||||
: formatMessage({ id: 'common.status.disabled' })
|
||||
}
|
||||
</Badge>
|
||||
</div>
|
||||
{hook.description && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5 truncate">
|
||||
{hook.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={handleToggle}
|
||||
title={hook.enabled
|
||||
? formatMessage({ id: 'cliHooks.actions.disable' })
|
||||
: formatMessage({ id: 'cliHooks.actions.enable' })
|
||||
}
|
||||
>
|
||||
{hook.enabled ? (
|
||||
<Power className="w-4 h-4 text-success" />
|
||||
) : (
|
||||
<PowerOff className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={handleEdit}
|
||||
title={formatMessage({ id: 'common.actions.edit' })}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={handleDelete}
|
||||
title={formatMessage({ id: 'common.actions.delete' })}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={onToggleExpand}
|
||||
title={isExpanded
|
||||
? formatMessage({ id: 'cliHooks.actions.collapse' })
|
||||
: formatMessage({ id: 'cliHooks.actions.expand' })
|
||||
}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-border bg-muted/30 p-4 space-y-3">
|
||||
{hook.description && (
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.form.description' })}
|
||||
</label>
|
||||
<p className="text-sm text-foreground mt-1">{hook.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.form.matcher' })}
|
||||
</label>
|
||||
<p className="text-sm text-foreground mt-1 font-mono bg-muted px-2 py-1 rounded">
|
||||
{hook.matcher || formatMessage({ id: 'cliHooks.allTools' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.form.command' })}
|
||||
</label>
|
||||
<p className="text-sm text-foreground mt-1 font-mono bg-muted px-2 py-1 rounded break-all max-h-32 overflow-y-auto">
|
||||
{hook.command || hook.script || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default HookCard;
|
||||
289
ccw/frontend/src/components/hook/HookFormDialog.tsx
Normal file
289
ccw/frontend/src/components/hook/HookFormDialog.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
// ========================================
|
||||
// Hook Form Dialog Component
|
||||
// ========================================
|
||||
// Dialog for creating and editing hooks
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/Dialog';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Textarea } from '@/components/ui/Textarea';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/Select';
|
||||
import type { HookCardData, HookTriggerType } from './HookCard';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export type HookFormMode = 'create' | 'edit';
|
||||
|
||||
export interface HookFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
trigger: HookTriggerType;
|
||||
matcher: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
export interface HookFormDialogProps {
|
||||
mode: HookFormMode;
|
||||
hook?: HookCardData;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (data: HookFormData) => Promise<void>;
|
||||
}
|
||||
|
||||
// ========== Helper: Form Validation ==========
|
||||
|
||||
interface FormErrors {
|
||||
name?: string;
|
||||
trigger?: string;
|
||||
command?: string;
|
||||
}
|
||||
|
||||
function validateForm(data: HookFormData): FormErrors {
|
||||
const errors: FormErrors = {};
|
||||
|
||||
if (!data.name.trim()) {
|
||||
errors.name = 'validation.nameRequired';
|
||||
} else if (!/^[a-zA-Z0-9_-]+$/.test(data.name)) {
|
||||
errors.name = 'validation.nameInvalid';
|
||||
}
|
||||
|
||||
if (!data.trigger) {
|
||||
errors.trigger = 'validation.triggerRequired';
|
||||
}
|
||||
|
||||
if (!data.command.trim()) {
|
||||
errors.command = 'validation.commandRequired';
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ========== Component ==========
|
||||
|
||||
export function HookFormDialog({
|
||||
mode,
|
||||
hook,
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
}: HookFormDialogProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [formData, setFormData] = useState<HookFormData>({
|
||||
name: '',
|
||||
description: '',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: '',
|
||||
command: '',
|
||||
});
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Reset form when dialog opens or hook changes
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (mode === 'edit' && hook) {
|
||||
setFormData({
|
||||
name: hook.name,
|
||||
description: hook.description || '',
|
||||
trigger: hook.trigger,
|
||||
matcher: hook.matcher || '',
|
||||
command: hook.command || hook.script || '',
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: '',
|
||||
command: '',
|
||||
});
|
||||
}
|
||||
setErrors({});
|
||||
}
|
||||
}, [open, mode, hook]);
|
||||
|
||||
const handleFieldChange = (
|
||||
field: keyof HookFormData,
|
||||
value: string
|
||||
) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
// Clear error for this field when user starts typing
|
||||
if (errors[field as keyof FormErrors]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Validate form
|
||||
const validationErrors = validateForm(formData);
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
setErrors(validationErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onSave(formData);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Failed to save hook:', error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const TRIGGER_OPTIONS: { value: HookTriggerType; label: string }[] = [
|
||||
{ value: 'UserPromptSubmit', label: 'cliHooks.trigger.UserPromptSubmit' },
|
||||
{ value: 'PreToolUse', label: 'cliHooks.trigger.PreToolUse' },
|
||||
{ value: 'PostToolUse', label: 'cliHooks.trigger.PostToolUse' },
|
||||
{ value: 'Stop', label: 'cliHooks.trigger.Stop' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{mode === 'create'
|
||||
? formatMessage({ id: 'cliHooks.dialog.createTitle' })
|
||||
: formatMessage({ id: 'cliHooks.dialog.editTitle' }, { hookName: hook?.name })
|
||||
}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label htmlFor="hook-name" className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.form.name' })} *
|
||||
</label>
|
||||
<Input
|
||||
id="hook-name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'cliHooks.form.namePlaceholder' })}
|
||||
className="mt-1"
|
||||
error={!!errors.name}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
{formatMessage({ id: `cliHooks.${errors.name}` })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label htmlFor="hook-description" className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.form.description' })}
|
||||
</label>
|
||||
<Textarea
|
||||
id="hook-description"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleFieldChange('description', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'cliHooks.form.descriptionPlaceholder' })}
|
||||
className="mt-1"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Trigger */}
|
||||
<div>
|
||||
<label htmlFor="hook-trigger" className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.form.trigger' })} *
|
||||
</label>
|
||||
<Select
|
||||
value={formData.trigger}
|
||||
onValueChange={(value) => handleFieldChange('trigger', value as HookTriggerType)}
|
||||
>
|
||||
<SelectTrigger className="mt-1" id="hook-trigger">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TRIGGER_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{formatMessage({ id: option.label })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.trigger && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
{formatMessage({ id: `cliHooks.${errors.trigger}` })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Matcher */}
|
||||
<div>
|
||||
<label htmlFor="hook-matcher" className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.form.matcher' })}
|
||||
</label>
|
||||
<Input
|
||||
id="hook-matcher"
|
||||
value={formData.matcher}
|
||||
onChange={(e) => handleFieldChange('matcher', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'cliHooks.form.matcherPlaceholder' })}
|
||||
className="mt-1 font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'cliHooks.form.matcherHelp' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Command */}
|
||||
<div>
|
||||
<label htmlFor="hook-command" className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.form.command' })} *
|
||||
</label>
|
||||
<Textarea
|
||||
id="hook-command"
|
||||
value={formData.command}
|
||||
onChange={(e) => handleFieldChange('command', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'cliHooks.form.commandPlaceholder' })}
|
||||
className="mt-1 font-mono text-sm"
|
||||
rows={4}
|
||||
error={!!errors.command}
|
||||
/>
|
||||
{errors.command && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
{formatMessage({ id: `cliHooks.${errors.command}` })}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'cliHooks.form.commandHelp' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={isSubmitting}>
|
||||
{formatMessage({ id: 'common.actions.cancel' })}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting
|
||||
? formatMessage({ id: 'common.actions.saving' })
|
||||
: formatMessage({ id: 'common.actions.save' })
|
||||
}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default HookFormDialog;
|
||||
268
ccw/frontend/src/components/hook/HookQuickTemplates.tsx
Normal file
268
ccw/frontend/src/components/hook/HookQuickTemplates.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
// ========================================
|
||||
// Hook Quick Templates Component
|
||||
// ========================================
|
||||
// Predefined hook templates for quick installation
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Bell,
|
||||
Database,
|
||||
Wrench,
|
||||
Check,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
/**
|
||||
* Template category type
|
||||
*/
|
||||
export type TemplateCategory = 'notification' | 'indexing' | 'automation';
|
||||
|
||||
/**
|
||||
* Hook template definition
|
||||
*/
|
||||
export interface HookTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: TemplateCategory;
|
||||
trigger: 'UserPromptSubmit' | 'PreToolUse' | 'PostToolUse' | 'Stop';
|
||||
command: string;
|
||||
args?: string[];
|
||||
matcher?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component props
|
||||
*/
|
||||
export interface HookQuickTemplatesProps {
|
||||
/** Callback when install button is clicked */
|
||||
onInstallTemplate: (templateId: string) => Promise<void>;
|
||||
/** Array of installed template IDs */
|
||||
installedTemplates: string[];
|
||||
/** Optional loading state */
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
// ========== Hook Templates ==========
|
||||
|
||||
/**
|
||||
* Predefined hook templates for quick installation
|
||||
*/
|
||||
export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
{
|
||||
id: 'ccw-notify',
|
||||
name: 'CCW Dashboard Notify',
|
||||
description: 'Send notifications to CCW dashboard when files are written',
|
||||
category: 'notification',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write',
|
||||
command: 'bash',
|
||||
args: [
|
||||
'-c',
|
||||
'INPUT=$(cat); FILE_PATH=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); [ -n "$FILE_PATH" ] && curl -s -X POST -H "Content-Type: application/json" -d "{\\"type\\":\\"file_written\\",\\"filePath\\":\\"$FILE_PATH\\"}" http://localhost:3456/api/hook || true'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'codexlens-update',
|
||||
name: 'CodexLens Auto-Update',
|
||||
description: 'Update CodexLens index when files are written or edited',
|
||||
category: 'indexing',
|
||||
trigger: 'Stop',
|
||||
command: 'bash',
|
||||
args: [
|
||||
'-c',
|
||||
'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); [ -d ".codexlens" ] && [ -n "$FILE" ] && (python -m codexlens update "$FILE" --json 2>/dev/null || ~/.codexlens/venv/bin/python -m codexlens update "$FILE" --json 2>/dev/null || true)'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'git-add',
|
||||
name: 'Auto Git Stage',
|
||||
description: 'Automatically stage written files to git',
|
||||
category: 'automation',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write',
|
||||
command: 'bash',
|
||||
args: [
|
||||
'-c',
|
||||
'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // empty"); [ -n "$FILE" ] && git add "$FILE" 2>/dev/null || true'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'lint-check',
|
||||
name: 'Auto ESLint',
|
||||
description: 'Run ESLint on JavaScript/TypeScript files after write',
|
||||
category: 'automation',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'Write',
|
||||
command: 'bash',
|
||||
args: [
|
||||
'-c',
|
||||
'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // empty"); if [[ "$FILE" =~ \\.(js|ts|jsx|tsx)$ ]]; then npx eslint "$FILE" --fix 2>/dev/null || true; fi'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'log-tool',
|
||||
name: 'Tool Usage Logger',
|
||||
description: 'Log all tool executions to a file for audit trail',
|
||||
category: 'automation',
|
||||
trigger: 'PostToolUse',
|
||||
command: 'bash',
|
||||
args: [
|
||||
'-c',
|
||||
'mkdir -p "$HOME/.claude"; INPUT=$(cat); TOOL=$(echo "$INPUT" | jq -r ".tool_name // empty" 2>/dev/null); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty" 2>/dev/null); echo "[$(date)] Tool: $TOOL, File: $FILE" >> "$HOME/.claude/tool-usage.log"'
|
||||
]
|
||||
}
|
||||
] as const;
|
||||
|
||||
// ========== Category Icons ==========
|
||||
|
||||
const CATEGORY_ICONS: Record<TemplateCategory, { icon: typeof Bell; color: string }> = {
|
||||
notification: { icon: Bell, color: 'text-blue-500' },
|
||||
indexing: { icon: Database, color: 'text-purple-500' },
|
||||
automation: { icon: Wrench, color: 'text-orange-500' }
|
||||
};
|
||||
|
||||
// ========== Category Names ==========
|
||||
|
||||
function getCategoryName(category: TemplateCategory, formatMessage: ReturnType<typeof useIntl>['formatMessage']): string {
|
||||
const names: Record<TemplateCategory, string> = {
|
||||
notification: formatMessage({ id: 'cliHooks.templates.categories.notification' }),
|
||||
indexing: formatMessage({ id: 'cliHooks.templates.categories.indexing' }),
|
||||
automation: formatMessage({ id: 'cliHooks.templates.categories.automation' })
|
||||
};
|
||||
return names[category];
|
||||
}
|
||||
|
||||
// ========== Main Component ==========
|
||||
|
||||
/**
|
||||
* HookQuickTemplates - Display predefined hook templates for quick installation
|
||||
*/
|
||||
export function HookQuickTemplates({
|
||||
onInstallTemplate,
|
||||
installedTemplates,
|
||||
isLoading = false
|
||||
}: HookQuickTemplatesProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
// Group templates by category
|
||||
const templatesByCategory = useMemo(() => {
|
||||
return HOOK_TEMPLATES.reduce((acc, template) => {
|
||||
if (!acc[template.category]) {
|
||||
acc[template.category] = [];
|
||||
}
|
||||
acc[template.category].push(template);
|
||||
return acc;
|
||||
}, {} as Record<TemplateCategory, HookTemplate[]>);
|
||||
}, []);
|
||||
|
||||
// Define category order
|
||||
const categoryOrder: TemplateCategory[] = ['notification', 'indexing', 'automation'];
|
||||
|
||||
const handleInstall = async (templateId: string) => {
|
||||
await onInstallTemplate(templateId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.templates.title' })}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.templates.description' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
{categoryOrder.map((category) => {
|
||||
const templates = templatesByCategory[category];
|
||||
if (!templates || templates.length === 0) return null;
|
||||
|
||||
const { icon: CategoryIcon, color } = CATEGORY_ICONS[category];
|
||||
|
||||
return (
|
||||
<div key={category} className="space-y-3">
|
||||
{/* Category Header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<CategoryIcon className={cn('w-4 h-4', color)} />
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
{getCategoryName(category, formatMessage)}
|
||||
</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{templates.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Template Cards */}
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{templates.map((template) => {
|
||||
const isInstalled = installedTemplates.includes(template.id);
|
||||
const isInstalling = isLoading && !isInstalled;
|
||||
|
||||
return (
|
||||
<Card key={template.id} className="p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
{/* Template Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: `cliHooks.templates.templates.${template.id}.name` })}
|
||||
</h4>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{formatMessage({ id: `cliHooks.trigger.${template.trigger}` })}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: `cliHooks.templates.templates.${template.id}.description` })}
|
||||
</p>
|
||||
{template.matcher && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<span className="font-mono bg-muted px-1 rounded">
|
||||
{template.matcher}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Install Button */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isInstalled ? 'outline' : 'default'}
|
||||
disabled={isInstalled || isInstalling}
|
||||
onClick={() => handleInstall(template.id)}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isInstalled ? (
|
||||
<>
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
{formatMessage({ id: 'cliHooks.templates.actions.installed' })}
|
||||
</>
|
||||
) : (
|
||||
formatMessage({ id: 'cliHooks.templates.actions.install' })
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HookQuickTemplates;
|
||||
842
ccw/frontend/src/components/hook/HookWizard.tsx
Normal file
842
ccw/frontend/src/components/hook/HookWizard.tsx
Normal file
@@ -0,0 +1,842 @@
|
||||
// ========================================
|
||||
// Hook Wizard Component
|
||||
// ========================================
|
||||
// Multi-step wizard for creating common hook patterns
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/Dialog';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Textarea } from '@/components/ui/Textarea';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/Select';
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
X,
|
||||
Brain,
|
||||
Shield,
|
||||
Sparkles,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { fetchSkills, type Skill, createHook } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
detect,
|
||||
getShell,
|
||||
getShellName,
|
||||
checkCompatibility,
|
||||
getPlatformName,
|
||||
adjustCommandForPlatform,
|
||||
DEFAULT_PLATFORM_REQUIREMENTS,
|
||||
type Platform,
|
||||
} from '@/utils/platformUtils';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
/**
|
||||
* Supported wizard types
|
||||
*/
|
||||
export type WizardType = 'memory-update' | 'danger-protection' | 'skill-context';
|
||||
|
||||
/**
|
||||
* Wizard step number
|
||||
*/
|
||||
type WizardStep = 1 | 2 | 3;
|
||||
|
||||
/**
|
||||
* Component props
|
||||
*/
|
||||
export interface HookWizardProps {
|
||||
/** Type of wizard to launch */
|
||||
wizardType: WizardType;
|
||||
/** Whether the dialog is open */
|
||||
open: boolean;
|
||||
/** Callback when dialog is closed */
|
||||
onClose: () => void;
|
||||
/** Callback when wizard completes with hook configuration */
|
||||
onComplete: (hookConfig: {
|
||||
name: string;
|
||||
description: string;
|
||||
trigger: string;
|
||||
matcher?: string;
|
||||
command: string;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory update wizard configuration
|
||||
*/
|
||||
interface MemoryUpdateConfig {
|
||||
claudePath: string;
|
||||
updateFrequency: 'session-end' | 'hourly' | 'daily';
|
||||
sections: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Danger protection wizard configuration
|
||||
*/
|
||||
interface DangerProtectionConfig {
|
||||
keywords: string;
|
||||
confirmationMessage: string;
|
||||
allowBypass: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill context wizard configuration
|
||||
*/
|
||||
interface SkillContextConfig {
|
||||
keywordSkillPairs: Array<{ keyword: string; skill: string }>;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
* Wizard configuration union type
|
||||
*/
|
||||
type WizardConfig = MemoryUpdateConfig | DangerProtectionConfig | SkillContextConfig;
|
||||
|
||||
// ========== Wizard Definitions ==========
|
||||
|
||||
/**
|
||||
* Wizard metadata for each type
|
||||
*/
|
||||
const WIZARD_METADATA = {
|
||||
'memory-update': {
|
||||
title: 'cliHooks.wizards.memoryUpdate.title',
|
||||
description: 'cliHooks.wizards.memoryUpdate.description',
|
||||
icon: Brain,
|
||||
trigger: 'Stop' as const,
|
||||
platformRequirements: DEFAULT_PLATFORM_REQUIREMENTS['memory-update'],
|
||||
},
|
||||
'danger-protection': {
|
||||
title: 'cliHooks.wizards.dangerProtection.title',
|
||||
description: 'cliHooks.wizards.dangerProtection.description',
|
||||
icon: Shield,
|
||||
trigger: 'UserPromptSubmit' as const,
|
||||
platformRequirements: DEFAULT_PLATFORM_REQUIREMENTS['danger-protection'],
|
||||
},
|
||||
'skill-context': {
|
||||
title: 'cliHooks.wizards.skillContext.title',
|
||||
description: 'cliHooks.wizards.skillContext.description',
|
||||
icon: Sparkles,
|
||||
trigger: 'UserPromptSubmit' as const,
|
||||
platformRequirements: DEFAULT_PLATFORM_REQUIREMENTS['skill-context'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
/**
|
||||
* Get wizard icon component
|
||||
*/
|
||||
function getWizardIcon(type: WizardType) {
|
||||
return WIZARD_METADATA[type].icon;
|
||||
}
|
||||
|
||||
// ========== Main Component ==========
|
||||
|
||||
export function HookWizard({
|
||||
wizardType,
|
||||
open,
|
||||
onClose,
|
||||
onComplete,
|
||||
}: HookWizardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [currentStep, setCurrentStep] = useState<WizardStep>(1);
|
||||
const [detectedPlatform, setDetectedPlatform] = useState<Platform>('linux');
|
||||
|
||||
// Fetch available skills for skill-context wizard
|
||||
const { data: skillsData, isLoading: skillsLoading } = useQuery({
|
||||
queryKey: ['skills'],
|
||||
queryFn: fetchSkills,
|
||||
enabled: open && wizardType === 'skill-context',
|
||||
});
|
||||
|
||||
// Mutation for creating hook
|
||||
const createMutation = useMutation({
|
||||
mutationFn: createHook,
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
setCurrentStep(1);
|
||||
},
|
||||
});
|
||||
|
||||
// Detect platform on mount
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDetectedPlatform(detect());
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Wizard configuration state
|
||||
const [memoryConfig, setMemoryConfig] = useState<MemoryUpdateConfig>({
|
||||
claudePath: '.claude/CLAUDE.md',
|
||||
updateFrequency: 'session-end',
|
||||
sections: ['all'],
|
||||
});
|
||||
|
||||
const [dangerConfig, setDangerConfig] = useState<DangerProtectionConfig>({
|
||||
keywords: 'delete\nrm\nformat\ndrop\ntruncate\nshutdown',
|
||||
confirmationMessage: 'Are you sure you want to perform this action: {action}?',
|
||||
allowBypass: true,
|
||||
});
|
||||
|
||||
const [skillConfig, setSkillConfig] = useState<SkillContextConfig>({
|
||||
keywordSkillPairs: [{ keyword: '', skill: '' }],
|
||||
priority: 'medium',
|
||||
});
|
||||
|
||||
// Check platform compatibility
|
||||
const wizardMetadata = WIZARD_METADATA[wizardType];
|
||||
const compatibilityCheck = checkCompatibility(
|
||||
wizardMetadata.platformRequirements,
|
||||
detectedPlatform
|
||||
);
|
||||
|
||||
// Handlers
|
||||
const handleNext = () => {
|
||||
if (currentStep < 3) {
|
||||
setCurrentStep((prev) => (prev + 1) as WizardStep);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep((prev) => (prev - 1) as WizardStep);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setCurrentStep(1);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleComplete = async () => {
|
||||
let hookConfig: {
|
||||
name: string;
|
||||
description: string;
|
||||
trigger: string;
|
||||
matcher?: string;
|
||||
command: string;
|
||||
};
|
||||
|
||||
const wizardName = formatMessage({ id: WIZARD_METADATA[wizardType].title });
|
||||
|
||||
switch (wizardType) {
|
||||
case 'memory-update':
|
||||
hookConfig = {
|
||||
name: `memory-update-${Date.now()}`,
|
||||
description: `${wizardName}: Update ${memoryConfig.claudePath} on ${memoryConfig.updateFrequency}`,
|
||||
trigger: wizardMetadata.trigger,
|
||||
command: buildMemoryUpdateCommand(memoryConfig, detectedPlatform),
|
||||
};
|
||||
break;
|
||||
|
||||
case 'danger-protection':
|
||||
hookConfig = {
|
||||
name: `danger-protection-${Date.now()}`,
|
||||
description: `${wizardName}: Confirm dangerous operations`,
|
||||
trigger: wizardMetadata.trigger,
|
||||
matcher: buildDangerMatcher(dangerConfig),
|
||||
command: buildDangerProtectionCommand(dangerConfig, detectedPlatform),
|
||||
};
|
||||
break;
|
||||
|
||||
case 'skill-context':
|
||||
hookConfig = {
|
||||
name: `skill-context-${Date.now()}`,
|
||||
description: `${wizardName}: Load SKILL based on keywords`,
|
||||
trigger: wizardMetadata.trigger,
|
||||
matcher: buildSkillMatcher(skillConfig),
|
||||
command: buildSkillContextCommand(skillConfig, detectedPlatform),
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
await createMutation.mutateAsync(hookConfig);
|
||||
};
|
||||
|
||||
// Step renderers
|
||||
const renderStep1 = () => {
|
||||
const WizardIcon = getWizardIcon(wizardType);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Introduction */}
|
||||
<div className="flex items-center gap-3 pb-4 border-b">
|
||||
<div className="p-3 rounded-lg bg-primary/10">
|
||||
<WizardIcon className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{formatMessage({ id: WIZARD_METADATA[wizardType].title })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: WIZARD_METADATA[wizardType].description })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Platform Detection */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle className={cn(
|
||||
'w-5 h-5',
|
||||
compatibilityCheck.compatible ? 'text-green-500' : 'text-destructive'
|
||||
)} />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.platform.detected' })}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{getPlatformName(detectedPlatform)} ({getShellName(getShell(detectedPlatform))})
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={compatibilityCheck.compatible ? 'default' : 'destructive'}>
|
||||
{compatibilityCheck.compatible
|
||||
? formatMessage({ id: 'cliHooks.wizards.platform.compatible' })
|
||||
: formatMessage({ id: 'cliHooks.wizards.platform.incompatible' })
|
||||
}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Compatibility Issues */}
|
||||
{!compatibilityCheck.compatible && compatibilityCheck.issues.length > 0 && (
|
||||
<div className="mt-3 flex items-start gap-2 p-3 bg-destructive/10 rounded-lg">
|
||||
<AlertTriangle className="w-4 h-4 text-destructive shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{formatMessage({ id: 'cliHooks.wizards.platform.compatibilityError' })}
|
||||
</p>
|
||||
<ul className="mt-1 space-y-1">
|
||||
{compatibilityCheck.issues.map((issue, i) => (
|
||||
<li key={i} className="text-xs text-destructive/80">
|
||||
{issue}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Compatibility Warnings */}
|
||||
{compatibilityCheck.warnings.length > 0 && (
|
||||
<div className="mt-3 flex items-start gap-2 p-3 bg-yellow-500/10 rounded-lg">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-600 shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-yellow-600">
|
||||
{formatMessage({ id: 'cliHooks.wizards.platform.compatibilityWarning' })}
|
||||
</p>
|
||||
<ul className="mt-1 space-y-1">
|
||||
{compatibilityCheck.warnings.map((warning, i) => (
|
||||
<li key={i} className="text-xs text-yellow-600/80">
|
||||
{warning}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Trigger Event */}
|
||||
<Card className="p-4">
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
{formatMessage({ id: 'cliHooks.wizards.steps.triggerEvent' })}
|
||||
</p>
|
||||
<Badge variant="secondary">
|
||||
{formatMessage({ id: `cliHooks.trigger.${wizardMetadata.trigger}` })}
|
||||
</Badge>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStep2 = () => {
|
||||
switch (wizardType) {
|
||||
case 'memory-update':
|
||||
return renderMemoryUpdateConfig();
|
||||
case 'danger-protection':
|
||||
return renderDangerProtectionConfig();
|
||||
case 'skill-context':
|
||||
return renderSkillContextConfig();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderStep3 = () => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-center pb-4 border-b">
|
||||
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-3" />
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.steps.review.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.steps.review.description' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<Card className="p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.steps.review.hookType' })}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: WIZARD_METADATA[wizardType].title })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.steps.review.trigger' })}
|
||||
</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{formatMessage({ id: `cliHooks.trigger.${wizardMetadata.trigger}` })}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.steps.review.platform' })}
|
||||
</span>
|
||||
<span className="text-sm text-foreground">
|
||||
{getPlatformName(detectedPlatform)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Configuration Summary */}
|
||||
{renderConfigSummary()}
|
||||
</Card>
|
||||
|
||||
{/* Command Preview */}
|
||||
<Card className="p-4">
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
{formatMessage({ id: 'cliHooks.wizards.steps.review.commandPreview' })}
|
||||
</p>
|
||||
<pre className="text-xs font-mono bg-muted p-3 rounded-lg overflow-x-auto">
|
||||
{getPreviewCommand()}
|
||||
</pre>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Configuration renderers
|
||||
const renderMemoryUpdateConfig = () => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.memoryUpdate.claudePath' })}
|
||||
</label>
|
||||
<Input
|
||||
value={memoryConfig.claudePath}
|
||||
onChange={(e) => setMemoryConfig({ ...memoryConfig, claudePath: e.target.value })}
|
||||
placeholder=".claude/CLAUDE.md"
|
||||
className="mt-1 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.memoryUpdate.updateFrequency' })}
|
||||
</label>
|
||||
<Select
|
||||
value={memoryConfig.updateFrequency}
|
||||
onValueChange={(value: any) => setMemoryConfig({ ...memoryConfig, updateFrequency: value })}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="session-end">
|
||||
{formatMessage({ id: 'cliHooks.wizards.memoryUpdate.frequency.sessionEnd' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="hourly">
|
||||
{formatMessage({ id: 'cliHooks.wizards.memoryUpdate.frequency.hourly' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="daily">
|
||||
{formatMessage({ id: 'cliHooks.wizards.memoryUpdate.frequency.daily' })}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderDangerProtectionConfig = () => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.dangerProtection.keywords' })}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'cliHooks.wizards.dangerProtection.keywordsHelp' })}
|
||||
</p>
|
||||
<Textarea
|
||||
value={dangerConfig.keywords}
|
||||
onChange={(e) => setDangerConfig({ ...dangerConfig, keywords: e.target.value })}
|
||||
placeholder="delete\nrm\nformat"
|
||||
className="mt-1 font-mono text-sm"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.dangerProtection.confirmationMessage' })}
|
||||
</label>
|
||||
<Input
|
||||
value={dangerConfig.confirmationMessage}
|
||||
onChange={(e) => setDangerConfig({ ...dangerConfig, confirmationMessage: e.target.value })}
|
||||
placeholder="Are you sure you want to {action}?"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allow-bypass"
|
||||
checked={dangerConfig.allowBypass}
|
||||
onChange={(e) => setDangerConfig({ ...dangerConfig, allowBypass: e.target.checked })}
|
||||
className="rounded"
|
||||
/>
|
||||
<label htmlFor="allow-bypass" className="text-sm text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.dangerProtection.allowBypass' })}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSkillContextConfig = () => {
|
||||
const skills = skillsData?.skills ?? [];
|
||||
|
||||
const addPair = () => {
|
||||
setSkillConfig({
|
||||
...skillConfig,
|
||||
keywordSkillPairs: [...skillConfig.keywordSkillPairs, { keyword: '', skill: '' }],
|
||||
});
|
||||
};
|
||||
|
||||
const removePair = (index: number) => {
|
||||
setSkillConfig({
|
||||
...skillConfig,
|
||||
keywordSkillPairs: skillConfig.keywordSkillPairs.filter((_, i) => i !== index),
|
||||
});
|
||||
};
|
||||
|
||||
const updatePair = (index: number, field: 'keyword' | 'skill', value: string) => {
|
||||
const newPairs = [...skillConfig.keywordSkillPairs];
|
||||
newPairs[index][field] = value;
|
||||
setSkillConfig({ ...skillConfig, keywordSkillPairs: newPairs });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{skillsLoading ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.skillContext.loadingSkills' })}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{skillConfig.keywordSkillPairs.map((pair, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<Input
|
||||
value={pair.keyword}
|
||||
onChange={(e) => updatePair(index, 'keyword', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'cliHooks.wizards.skillContext.keywordPlaceholder' })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Select value={pair.skill} onValueChange={(value) => updatePair(index, 'skill', value)}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder={formatMessage({ id: 'cliHooks.wizards.skillContext.selectSkill' })} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{skills.map((skill) => (
|
||||
<SelectItem key={skill.name} value={skill.name}>
|
||||
{skill.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{skillConfig.keywordSkillPairs.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removePair(index)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addPair} className="w-full">
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'cliHooks.wizards.skillContext.addPair' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.skillContext.priority' })}
|
||||
</label>
|
||||
<Select
|
||||
value={skillConfig.priority}
|
||||
onValueChange={(value: any) => setSkillConfig({ ...skillConfig, priority: value })}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="high">
|
||||
{formatMessage({ id: 'cliHooks.wizards.skillContext.priorityHigh' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="medium">
|
||||
{formatMessage({ id: 'cliHooks.wizards.skillContext.priorityMedium' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="low">
|
||||
{formatMessage({ id: 'cliHooks.wizards.skillContext.priorityLow' })}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Config summary for review step
|
||||
const renderConfigSummary = () => {
|
||||
switch (wizardType) {
|
||||
case 'memory-update':
|
||||
return (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.memoryUpdate.claudePath' })}
|
||||
</span>
|
||||
<span className="font-mono">{memoryConfig.claudePath}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.memoryUpdate.updateFrequency' })}
|
||||
</span>
|
||||
<span>
|
||||
{formatMessage({ id: `cliHooks.wizards.memoryUpdate.frequency.${memoryConfig.updateFrequency}` })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'danger-protection':
|
||||
return (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.dangerProtection.keywords' })}:
|
||||
</span>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{dangerConfig.keywords.split('\n').filter(Boolean).map((kw, i) => (
|
||||
<Badge key={i} variant="secondary" className="text-xs">
|
||||
{kw.trim()}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.dangerProtection.allowBypass' })}
|
||||
</span>
|
||||
<span>{dangerConfig.allowBypass ? 'Yes' : 'No'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'skill-context':
|
||||
return (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.skillContext.keywordMappings' })}:
|
||||
</span>
|
||||
<div className="mt-1 space-y-1">
|
||||
{skillConfig.keywordSkillPairs
|
||||
.filter((p) => p.keyword && p.skill)
|
||||
.map((pair, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-xs">
|
||||
<Badge variant="outline">{pair.keyword}</Badge>
|
||||
<span className="text-muted-foreground">{'->'}</span>
|
||||
<Badge variant="secondary">{pair.skill}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.skillContext.priority' })}
|
||||
</span>
|
||||
<span>
|
||||
{formatMessage({ id: `cliHooks.wizards.skillContext.priority${skillConfig.priority.charAt(0).toUpperCase()}${skillConfig.priority.slice(1)}` })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Get command preview for review step
|
||||
const getPreviewCommand = (): string => {
|
||||
switch (wizardType) {
|
||||
case 'memory-update':
|
||||
return buildMemoryUpdateCommand(memoryConfig, detectedPlatform);
|
||||
case 'danger-protection':
|
||||
return buildDangerProtectionCommand(dangerConfig, detectedPlatform);
|
||||
case 'skill-context':
|
||||
return buildSkillContextCommand(skillConfig, detectedPlatform);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// Navigation buttons
|
||||
const renderNavigation = () => (
|
||||
<DialogFooter className="gap-2">
|
||||
{currentStep > 1 && (
|
||||
<Button variant="outline" onClick={handlePrevious} disabled={createMutation.isPending}>
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'cliHooks.wizards.navigation.previous' })}
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < 3 ? (
|
||||
<Button onClick={handleNext} disabled={!compatibilityCheck.compatible}>
|
||||
{formatMessage({ id: 'cliHooks.wizards.navigation.next' })}
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleComplete} disabled={createMutation.isPending}>
|
||||
{createMutation.isPending
|
||||
? formatMessage({ id: 'cliHooks.wizards.navigation.creating' })
|
||||
: formatMessage({ id: 'cliHooks.wizards.navigation.create' })
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" onClick={handleClose} disabled={createMutation.isPending}>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
);
|
||||
|
||||
// Step indicator
|
||||
const renderStepIndicator = () => (
|
||||
<div className="flex items-center justify-center gap-2 pb-4">
|
||||
{[1, 2, 3].map((step) => (
|
||||
<div key={step} className="flex items-center">
|
||||
<div
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium',
|
||||
currentStep === step
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: currentStep > step
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{currentStep > step ? <CheckCircle className="w-4 h-4" /> : step}
|
||||
</div>
|
||||
{step < 3 && (
|
||||
<div
|
||||
className={cn(
|
||||
'w-8 h-0.5 mx-1',
|
||||
currentStep > step ? 'bg-green-500' : 'bg-muted'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{formatMessage({ id: 'cliHooks.wizards.title' })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{renderStepIndicator()}
|
||||
|
||||
<div className="min-h-[300px]">
|
||||
{currentStep === 1 && renderStep1()}
|
||||
{currentStep === 2 && renderStep2()}
|
||||
{currentStep === 3 && renderStep3()}
|
||||
</div>
|
||||
|
||||
{renderNavigation()}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default HookWizard;
|
||||
|
||||
// ========== Command Builders ==========
|
||||
|
||||
function buildMemoryUpdateCommand(config: MemoryUpdateConfig, platform: Platform): string {
|
||||
const shellCmd = getShellCommand(getShell(platform));
|
||||
const command = `echo "Updating ${config.claudePath} at ${config.updateFrequency}"`;
|
||||
return JSON.stringify([...shellCmd, command]);
|
||||
}
|
||||
|
||||
function buildDangerMatcher(config: DangerProtectionConfig): string {
|
||||
const keywords = config.keywords.split('\n').filter(Boolean).join('|');
|
||||
return `(${keywords})`;
|
||||
}
|
||||
|
||||
function buildDangerProtectionCommand(config: DangerProtectionConfig, platform: Platform): string {
|
||||
const shellCmd = getShellCommand(getShell(platform));
|
||||
const command = `echo "Checking for dangerous operations: ${config.keywords.split('\n').filter(Boolean).join(', ')}"`;
|
||||
return JSON.stringify([...shellCmd, command]);
|
||||
}
|
||||
|
||||
function buildSkillMatcher(config: SkillContextConfig): string {
|
||||
const keywords = config.keywordSkillPairs
|
||||
.filter((p) => p.keyword)
|
||||
.map((p) => p.keyword)
|
||||
.join('|');
|
||||
return `(${keywords})`;
|
||||
}
|
||||
|
||||
function buildSkillContextCommand(config: SkillContextConfig, platform: Platform): string {
|
||||
const pairs = config.keywordSkillPairs.filter((p) => p.keyword && p.skill);
|
||||
const command = `echo "Loading SKILL based on keywords: ${pairs.map((p) => p.keyword).join(', ')}"`;
|
||||
return JSON.stringify([...getShellCommand(getShell(platform)), command]);
|
||||
}
|
||||
19
ccw/frontend/src/components/hook/index.ts
Normal file
19
ccw/frontend/src/components/hook/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// ========================================
|
||||
// Hook Components Barrel Export
|
||||
// ========================================
|
||||
|
||||
export { HookCard } from './HookCard';
|
||||
export type { HookCardProps, HookCardData, HookTriggerType } from './HookCard';
|
||||
|
||||
export { EventGroup } from './EventGroup';
|
||||
export type { EventGroupProps } from './EventGroup';
|
||||
|
||||
export { HookFormDialog } from './HookFormDialog';
|
||||
export type { HookFormDialogProps, HookFormMode, HookFormData } from './HookFormDialog';
|
||||
|
||||
export { HookQuickTemplates } from './HookQuickTemplates';
|
||||
export type { HookQuickTemplatesProps, HookTemplate, TemplateCategory } from './HookQuickTemplates';
|
||||
export { HOOK_TEMPLATES } from './HookQuickTemplates';
|
||||
|
||||
export { HookWizard } from './HookWizard';
|
||||
export type { HookWizardProps, WizardType } from './HookWizard';
|
||||
Reference in New Issue
Block a user