import { useState, useEffect, useCallback } from "react"; import type { Settings } from "@hai/core"; import { fetchSettings, updateSettings } from "../api"; import type { ToastType } from "../hooks/useToast"; interface SettingsModalProps { onClose: () => void; addToast: (message: string, type?: ToastType) => void; } export function SettingsModal({ onClose, addToast }: SettingsModalProps) { const [form, setForm] = useState({ maxConcurrent: 2, maxWorktrees: 4, pollIntervalMs: 15000, groupOverlappingFiles: false, autoMerge: false }); const [loading, setLoading] = useState(true); useEffect(() => { fetchSettings() .then((s) => { setForm(s); setLoading(false); }) .catch((err) => { addToast(err.message, "error"); setLoading(false); }); }, [addToast]); useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [onClose]); const handleOverlayClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget) onClose(); }, [onClose], ); const handleSave = useCallback(async () => { try { await updateSettings(form); addToast("Settings saved", "success"); onClose(); } catch (err: any) { addToast(err.message, "error"); } }, [form, onClose, addToast]); return (

Settings

{loading ? (
Loading…
) : (
setForm((f) => ({ ...f, maxConcurrent: Number(e.target.value) })) } />
setForm((f) => ({ ...f, maxWorktrees: Number(e.target.value) })) } /> Limits total git worktrees including in-review tasks
setForm((f) => ({ ...f, pollIntervalMs: Number(e.target.value) })) } />
When enabled, tasks that modify the same files are queued serially to avoid merge conflicts
)}
); }