(null);
// 加载配置
const loadConfig = useCallback(async (showToast = false) => {
try {
const result = await getHooksConfig();
if (result.success) {
setConfig(result.data);
if (showToast) {
toast.success('Configuration refreshed');
}
} else {
toast.error(result.error || 'Failed to load configuration');
}
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to load configuration');
}
}, []);
// 初始加载
useEffect(() => {
setLoading(true);
loadConfig().finally(() => setLoading(false));
}, [loadConfig]);
// 刷新
const handleRefresh = async () => {
setRefreshing(true);
await loadConfig(true);
setRefreshing(false);
};
// 保存配置
const saveConfig = async (newConfig: HookConfig) => {
setSaving(true);
try {
const result = await updateHooksConfig(newConfig);
if (result.success) {
setConfig(result.data);
toast.success('Configuration saved');
return true;
} else {
toast.error(result.error || 'Failed to save configuration');
return false;
}
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to save configuration');
return false;
} finally {
setSaving(false);
}
};
// 切换展开
const toggleExpanded = (type: HookType) => {
const newExpanded = new Set(expandedTypes);
if (newExpanded.has(type)) {
newExpanded.delete(type);
} else {
newExpanded.add(type);
}
setExpandedTypes(newExpanded);
};
// 测试命令
const handleTestCommand = async (cmd: ShellCommandConfig, id: string) => {
setTestingCommand(id);
try {
const result = await testHookCommand(cmd);
if (result.success && result.data) {
if (result.data.success) {
toast.success(
Command succeeded
Exit code: {result.data.exitCode} ({result.data.duration}ms)
{result.data.stdout && (
{result.data.stdout.slice(0, 500)}
)}
);
} else {
toast.error(
Command failed
Exit code: {result.data.exitCode} ({result.data.duration}ms)
{result.data.stderr && (
{result.data.stderr.slice(0, 500)}
)}
);
}
} else {
toast.error(result.error || 'Test failed');
}
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Test failed');
} finally {
setTestingCommand(null);
}
};
// 删除文件钩子的某个 pattern
const handleDeleteFileHook = async (type: HookType, pattern: string) => {
if (type === 'session_completed') return;
const fileHooks = config[type] as FileHookConfig | undefined;
if (!fileHooks) return;
const newFileHooks = { ...fileHooks };
delete newFileHooks[pattern];
const newConfig = { ...config, [type]: newFileHooks };
await saveConfig(newConfig);
};
// 删除 session_completed 的某个命令
const handleDeleteSessionHook = async (index: number) => {
const commands = config.session_completed || [];
const newCommands = commands.filter((_, i) => i !== index);
const newConfig = { ...config, session_completed: newCommands };
await saveConfig(newConfig);
};
// 打开编辑器 - 文件钩子
const openFileHookEditor = (type: HookType, pattern?: string, commands?: ShellCommandConfig[]) => {
setEditingHook({
type,
pattern,
command: commands?.[0],
isNew: !pattern,
});
};
// 打开编辑器 - Session 钩子
const openSessionHookEditor = (command?: ShellCommandConfig, index?: number) => {
setEditingHook({
type: 'session_completed',
pattern: index !== undefined ? String(index) : undefined,
command,
isNew: index === undefined,
});
};
// 保存编辑的钩子
const handleSaveHook = async (type: HookType, pattern: string, commands: ShellCommandConfig[]) => {
let newConfig: HookConfig;
if (type === 'session_completed') {
// Session hook
const existingCommands = [...(config.session_completed || [])];
const index = editingHook?.pattern !== undefined ? parseInt(editingHook.pattern) : -1;
if (index >= 0 && index < existingCommands.length) {
// 更新现有命令
existingCommands[index] = commands[0];
} else {
// 添加新命令
existingCommands.push(commands[0]);
}
newConfig = { ...config, session_completed: existingCommands };
} else {
// File hook
const fileHooks = { ...(config[type] as FileHookConfig || {}) };
// 如果是重命名 pattern
if (editingHook?.pattern && editingHook.pattern !== pattern) {
delete fileHooks[editingHook.pattern];
}
fileHooks[pattern] = commands;
newConfig = { ...config, [type]: fileHooks };
}
const success = await saveConfig(newConfig);
if (success) {
setEditingHook(null);
}
};
// 统计
const totalHooks = Object.values(config).reduce((sum, hooks) => {
if (Array.isArray(hooks)) {
return sum + hooks.length;
}
if (hooks && typeof hooks === 'object') {
return sum + Object.keys(hooks).length;
}
return sum;
}, 0);
// Loading 骨架屏
const LoadingSkeleton = () => (
{[1, 2, 3, 4].map((i) => (
))}
);
// 渲染文件钩子内容
const renderFileHooks = (type: HookType) => {
const hooks = config[type] as FileHookConfig | undefined;
const patterns = Object.keys(hooks || {});
if (patterns.length === 0) {
return (
No hooks configured
);
}
return (
{patterns.map((pattern) => {
const commands = hooks![pattern];
const cmdId = `${type}-${pattern}`;
return (
{pattern}
{commands.map((cmd, idx) => (
{cmd.command.join(' ')}
))}
);
})}
);
};
// 渲染 session hooks
const renderSessionHooks = () => {
const commands = config.session_completed || [];
if (commands.length === 0) {
return (
No hooks configured
);
}
return (
{commands.map((cmd, idx) => {
const cmdId = `session-${idx}`;
return (
{cmd.command.join(' ')}
);
})}
);
};
return (
<>
e.stopPropagation()}
className={cn(
'bg-surface-subtle max-h-[90vh] overflow-hidden flex flex-col',
responsive
? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
: 'rounded-lg w-full max-w-2xl mx-4'
)}
>
{/* Header */}
{responsive && (
)}
Hooks Configuration
{totalHooks} hooks configured
{/* Content */}
{loading ? (
) : (
{HOOK_TYPES.map(({ type, label, icon, description }) => {
const isExpanded = expandedTypes.has(type);
const isFileHook = type !== 'session_completed';
const hookCount = isFileHook
? Object.keys((config[type] as FileHookConfig) || {}).length
: (config.session_completed || []).length;
return (
{/* Type Header */}
toggleExpanded(type)}
>
{icon}
{label}
{hookCount > 0 && (
{hookCount}
)}
{description}
{/* Expanded Content */}
{isExpanded && (
{isFileHook ? renderFileHooks(type) : renderSessionHooks()}
)}
);
})}
)}
{/* Footer */}
Commands are executed in your project directory. Use caution with destructive operations.
{/* Hook Editor Modal */}
{editingHook && (
setEditingHook(null)}
onTest={handleTestCommand}
saving={saving}
responsive={responsive}
/>
)}
>
);
}