- Add autoMerge boolean to Settings model with default false - Add auto-merge toggle UI to the In Review column header - Implement server-side auto-merge behavior in dashboard command - Fix pre-existing build errors in types, executor, and dashboard server - Update README with auto-merge documentation
133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
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<Settings>({ 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 (
|
|
<div className="modal-overlay open" onClick={handleOverlayClick}>
|
|
<div className="modal">
|
|
<div className="modal-header">
|
|
<h3>Settings</h3>
|
|
<button className="modal-close" onClick={onClose}>
|
|
×
|
|
</button>
|
|
</div>
|
|
{loading ? (
|
|
<div style={{ padding: "20px", textAlign: "center" }}>Loading…</div>
|
|
) : (
|
|
<div className="settings-form">
|
|
<div className="form-group">
|
|
<label htmlFor="maxConcurrent">Max Concurrent Tasks</label>
|
|
<input
|
|
id="maxConcurrent"
|
|
type="number"
|
|
min={1}
|
|
max={10}
|
|
value={form.maxConcurrent}
|
|
onChange={(e) =>
|
|
setForm((f) => ({ ...f, maxConcurrent: Number(e.target.value) }))
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="maxWorktrees">Max Worktrees</label>
|
|
<input
|
|
id="maxWorktrees"
|
|
type="number"
|
|
min={1}
|
|
max={20}
|
|
value={form.maxWorktrees}
|
|
onChange={(e) =>
|
|
setForm((f) => ({ ...f, maxWorktrees: Number(e.target.value) }))
|
|
}
|
|
/>
|
|
<small>Limits total git worktrees including in-review tasks</small>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="pollIntervalMs">Poll Interval (ms)</label>
|
|
<input
|
|
id="pollIntervalMs"
|
|
type="number"
|
|
min={5000}
|
|
step={1000}
|
|
value={form.pollIntervalMs}
|
|
onChange={(e) =>
|
|
setForm((f) => ({ ...f, pollIntervalMs: Number(e.target.value) }))
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="groupOverlappingFiles" style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
|
<input
|
|
id="groupOverlappingFiles"
|
|
type="checkbox"
|
|
checked={form.groupOverlappingFiles}
|
|
onChange={(e) =>
|
|
setForm((f) => ({ ...f, groupOverlappingFiles: e.target.checked }))
|
|
}
|
|
/>
|
|
Serialize tasks with overlapping files
|
|
</label>
|
|
<small>When enabled, tasks that modify the same files are queued serially to avoid merge conflicts</small>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="modal-actions">
|
|
<button className="btn btn-sm" onClick={onClose}>
|
|
Cancel
|
|
</button>
|
|
<button className="btn btn-primary btn-sm" onClick={handleSave} disabled={loading}>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|