feat(HAI-049): add recycleWorktrees checkbox to Settings modal

- Add recycleWorktrees toggle to the Worktrees section in SettingsModal
- Wire checkbox state to settings store
- Add unit tests for recycleWorktrees setting behavior
This commit is contained in:
Dustin Byrne
2026-03-25 23:22:54 -04:00
parent d557e90a5b
commit b0d3492530
2 changed files with 42 additions and 1 deletions

View File

@@ -26,7 +26,7 @@ interface SettingsModalProps {
}
export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
const [form, setForm] = useState<Settings & { worktreeInitCommand?: string }>({ maxConcurrent: 2, maxWorktrees: 4, pollIntervalMs: 15000, groupOverlappingFiles: false, autoMerge: false, worktreeInitCommand: "" });
const [form, setForm] = useState<Settings & { worktreeInitCommand?: string }>({ maxConcurrent: 2, maxWorktrees: 4, pollIntervalMs: 15000, groupOverlappingFiles: false, autoMerge: false, recycleWorktrees: false, worktreeInitCommand: "" });
const [loading, setLoading] = useState(true);
const [activeSection, setActiveSection] = useState<SectionId>(SETTINGS_SECTIONS[0].id);
@@ -150,6 +150,20 @@ export function SettingsModal({ onClose, addToast }: SettingsModalProps) {
/>
<small>Shell command to run in each new worktree after creation</small>
</div>
<div className="form-group">
<label htmlFor="recycleWorktrees" style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input
id="recycleWorktrees"
type="checkbox"
checked={form.recycleWorktrees}
onChange={(e) =>
setForm((f) => ({ ...f, recycleWorktrees: e.target.checked }))
}
/>
Recycle worktrees
</label>
<small>When enabled, completed task worktrees are returned to an idle pool instead of being deleted, preserving build caches for faster startup</small>
</div>
</>
);
case "commands":

View File

@@ -9,6 +9,7 @@ const defaultSettings: Settings = {
pollIntervalMs: 15000,
groupOverlappingFiles: false,
autoMerge: false,
recycleWorktrees: false,
worktreeInitCommand: "",
testCommand: "",
buildCommand: "",
@@ -75,6 +76,7 @@ describe("SettingsModal", () => {
fireEvent.click(screen.getByText("Worktrees"));
expect(screen.getByLabelText("Max Worktrees")).toBeTruthy();
expect(screen.getByLabelText("Worktree Init Command")).toBeTruthy();
expect(screen.getByText("Recycle worktrees")).toBeTruthy();
// Commands
fireEvent.click(screen.getByText("Commands"));
@@ -86,6 +88,31 @@ describe("SettingsModal", () => {
expect(screen.getByText("Auto-merge completed tasks")).toBeTruthy();
});
it("shows Recycle worktrees checkbox in Worktrees section", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Worktrees"));
const checkbox = screen.getByLabelText("Recycle worktrees");
expect(checkbox).toBeTruthy();
expect(checkbox.getAttribute("type")).toBe("checkbox");
});
it("toggling recycleWorktrees checkbox sends true in save payload", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Worktrees"));
const checkbox = screen.getByLabelText("Recycle worktrees");
fireEvent.click(checkbox);
fireEvent.click(screen.getByText("Save"));
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
const payload = (updateSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(payload.recycleWorktrees).toBe(true);
});
it("groupOverlappingFiles input has type checkbox", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());