feat(FN-1952): move archived tasks to cold database
This commit is contained in:
@@ -37,8 +37,8 @@ import { applyPresetToSelection, generateUniquePresetId } from "../utils/modelPr
|
||||
* - global-models: Default/fallback models and thinking level (global)
|
||||
* - project-models: Planning & validator models, model presets, and AI summarization (project)
|
||||
* - general: Task prefix configuration (project)
|
||||
* - scheduling: Concurrency, poll interval, file overlap serialization, and step execution
|
||||
* settings (runStepsInNewSessions, maxParallelSteps) (project)
|
||||
* - scheduling: Concurrency, poll interval, file overlap serialization, task auto-archive,
|
||||
* and step execution settings (runStepsInNewSessions, maxParallelSteps) (project)
|
||||
* - worktrees: Worktree limits, init commands, recycling (project)
|
||||
* - commands: Test and build command configuration (project)
|
||||
* - merge: Auto-merge settings (project)
|
||||
@@ -79,6 +79,9 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
|
||||
{ id: "plugins", label: "Plugins", scope: "project" },
|
||||
];
|
||||
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
const AUTO_ARCHIVE_DEFAULT_AFTER_DAYS = 2;
|
||||
|
||||
export type SectionId = SettingsSection["id"];
|
||||
|
||||
interface SettingsModalProps {
|
||||
@@ -110,7 +113,21 @@ export function SettingsModal({
|
||||
onColorThemeChange,
|
||||
onReopenOnboarding,
|
||||
}: SettingsModalProps) {
|
||||
const [form, setForm] = useState<Settings & { worktreeInitCommand?: string }>({ maxConcurrent: 2, maxTriageConcurrent: 2, maxWorktrees: 4, pollIntervalMs: 15000, groupOverlappingFiles: true, autoMerge: true, mergeStrategy: "direct", recycleWorktrees: false, worktreeNaming: "random", includeTaskIdInCommit: true, worktreeInitCommand: "", ntfyEnabled: false, ntfyTopic: undefined });
|
||||
const [form, setForm] = useState<Settings & { worktreeInitCommand?: string }>({
|
||||
maxConcurrent: 2,
|
||||
maxTriageConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
groupOverlappingFiles: true,
|
||||
autoMerge: true,
|
||||
mergeStrategy: "direct",
|
||||
recycleWorktrees: false,
|
||||
worktreeNaming: "random",
|
||||
includeTaskIdInCommit: true,
|
||||
worktreeInitCommand: "",
|
||||
ntfyEnabled: false,
|
||||
ntfyTopic: undefined,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
// Track initial values to detect explicit clears for null-as-delete semantics
|
||||
const [initialValues, setInitialValues] = useState<Settings | null>(null);
|
||||
@@ -1547,7 +1564,7 @@ export function SettingsModal({
|
||||
<input
|
||||
id="globalMaxConcurrent"
|
||||
type="number"
|
||||
min={1}
|
||||
min={0}
|
||||
max={10000}
|
||||
value={globalMaxConcurrent ?? ""}
|
||||
onChange={(e) => {
|
||||
@@ -1647,6 +1664,62 @@ export function SettingsModal({
|
||||
/>
|
||||
<small>Maximum age in hours before a specification is considered stale. Default: 6 hours.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="autoArchiveDoneTasksEnabled" className="checkbox-label">
|
||||
<input
|
||||
id="autoArchiveDoneTasksEnabled"
|
||||
type="checkbox"
|
||||
checked={form.autoArchiveDoneTasksEnabled ?? true}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
autoArchiveDoneTasksEnabled: e.target.checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
Enable automatic task archiving
|
||||
</label>
|
||||
<small>Completed tasks older than the threshold are moved out of the active task database.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="autoArchiveDoneAfterMs">Archive Completed Tasks After (days)</label>
|
||||
<input
|
||||
id="autoArchiveDoneAfterMs"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={form.autoArchiveDoneAfterMs !== undefined ? Math.round(form.autoArchiveDoneAfterMs / MS_PER_DAY) : AUTO_ARCHIVE_DEFAULT_AFTER_DAYS}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
const num = Number(val);
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
autoArchiveDoneAfterMs: val === "" ? undefined : num * MS_PER_DAY,
|
||||
}));
|
||||
}}
|
||||
disabled={form.autoArchiveDoneTasksEnabled === false}
|
||||
/>
|
||||
<small>Number of days a task can stay in Done before it is archived. Default: 2 days (48 hours).</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="archiveAgentLogMode">Archive Agent Log</label>
|
||||
<select
|
||||
id="archiveAgentLogMode"
|
||||
value={form.archiveAgentLogMode ?? "compact"}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
archiveAgentLogMode: e.target.value as "none" | "compact" | "full",
|
||||
}))
|
||||
}
|
||||
disabled={form.autoArchiveDoneTasksEnabled === false}
|
||||
>
|
||||
<option value="compact">Compact summary and recent entries</option>
|
||||
<option value="none">Do not archive agent logs</option>
|
||||
<option value="full">Full agent log</option>
|
||||
</select>
|
||||
<small>Compact mode keeps archive size low while preserving recent agent activity for context.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="maxStuckKills">Max Stuck Retries</label>
|
||||
<input
|
||||
|
||||
@@ -5,7 +5,13 @@ import userEvent from "@testing-library/user-event";
|
||||
import { SettingsModal } from "../SettingsModal";
|
||||
import type { Settings, ThemeMode, ColorTheme } from "@fusion/core";
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
type SettingsWithAutoArchive = Settings & {
|
||||
autoArchiveDoneTasksEnabled?: boolean;
|
||||
autoArchiveDoneAfterMs?: number;
|
||||
archiveAgentLogMode?: "none" | "compact" | "full";
|
||||
};
|
||||
|
||||
const defaultSettings: SettingsWithAutoArchive = {
|
||||
maxConcurrent: 2,
|
||||
maxTriageConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
@@ -2879,6 +2885,77 @@ describe("SettingsModal", () => {
|
||||
expect(thresholdInput.getAttribute("min")).toBe("0");
|
||||
});
|
||||
|
||||
it("shows auto-archive fields in Scheduling section", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Scheduling"));
|
||||
const checkbox = screen.getByLabelText("Enable automatic task archiving");
|
||||
expect(checkbox).toBeTruthy();
|
||||
expect(checkbox.getAttribute("type")).toBe("checkbox");
|
||||
|
||||
const ageInput = screen.getByLabelText("Archive Completed Tasks After (days)");
|
||||
expect(ageInput).toBeTruthy();
|
||||
expect(ageInput.getAttribute("type")).toBe("number");
|
||||
expect(ageInput.getAttribute("min")).toBe("1");
|
||||
|
||||
const logMode = screen.getByLabelText("Archive Agent Log") as HTMLSelectElement;
|
||||
expect(logMode).toBeTruthy();
|
||||
expect(logMode.value).toBe("compact");
|
||||
});
|
||||
|
||||
it("auto-archive age input shows default days and disables when archiving is off", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Scheduling"));
|
||||
const ageInput = screen.getByLabelText("Archive Completed Tasks After (days)") as HTMLInputElement;
|
||||
expect(ageInput.value).toBe("2");
|
||||
|
||||
const checkbox = screen.getByLabelText("Enable automatic task archiving") as HTMLInputElement;
|
||||
const logMode = screen.getByLabelText("Archive Agent Log") as HTMLSelectElement;
|
||||
fireEvent.click(checkbox);
|
||||
expect(checkbox.checked).toBe(false);
|
||||
expect(ageInput).toBeDisabled();
|
||||
expect(logMode).toBeDisabled();
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
expect(checkbox.checked).toBe(true);
|
||||
expect(ageInput).not.toBeDisabled();
|
||||
expect(logMode).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("auto-archive age renders from milliseconds and converts days back to milliseconds on save", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
autoArchiveDoneTasksEnabled: true,
|
||||
autoArchiveDoneAfterMs: 5 * 24 * 60 * 60 * 1000,
|
||||
archiveAgentLogMode: "compact",
|
||||
} as SettingsWithAutoArchive);
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Scheduling"));
|
||||
const checkbox = screen.getByLabelText("Enable automatic task archiving") as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(true);
|
||||
|
||||
const ageInput = screen.getByLabelText("Archive Completed Tasks After (days)") as HTMLInputElement;
|
||||
expect(ageInput.value).toBe("5");
|
||||
|
||||
fireEvent.change(ageInput, { target: { value: "7" } });
|
||||
expect(ageInput.value).toBe("7");
|
||||
fireEvent.change(screen.getByLabelText("Archive Agent Log"), { target: { value: "none" } });
|
||||
|
||||
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.autoArchiveDoneTasksEnabled).toBe(true);
|
||||
expect(payload.autoArchiveDoneAfterMs).toBe(7 * 24 * 60 * 60 * 1000);
|
||||
expect(payload.archiveAgentLogMode).toBe("none");
|
||||
});
|
||||
|
||||
it("Specification Staleness threshold input is disabled when toggle is off", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
Reference in New Issue
Block a user