feat(KB-612): convert stuck task timeout from milliseconds to minutes
- Convert stuck task timeout input from milliseconds to minutes for better UX\n- Add changeset documenting the UX improvement\n- Update SettingsModal with minutes-to-ms conversion logic\n- Fix nav item count test to include Backups section\n- Update timeout tests for minutes input format
This commit is contained in:
@@ -793,20 +793,20 @@ export function SettingsModal({
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="taskStuckTimeoutMs">Stuck Task Timeout (ms)</label>
|
||||
<label htmlFor="taskStuckTimeoutMs">Stuck Task Timeout (minutes)</label>
|
||||
<input
|
||||
id="taskStuckTimeoutMs"
|
||||
type="number"
|
||||
min={0}
|
||||
step={60000}
|
||||
value={form.taskStuckTimeoutMs || ""}
|
||||
min={1}
|
||||
step={1}
|
||||
value={form.taskStuckTimeoutMs ? Math.round(form.taskStuckTimeoutMs / 60000) : ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
const num = Number(val);
|
||||
setForm((f) => ({ ...f, taskStuckTimeoutMs: val && num > 0 ? num : undefined }));
|
||||
setForm((f) => ({ ...f, taskStuckTimeoutMs: val && num > 0 ? num * 60000 : undefined }));
|
||||
}}
|
||||
/>
|
||||
<small>Timeout in milliseconds for detecting stuck tasks. When a task's agent session shows no activity for longer than this duration, the task is terminated and retried. Set to 0 to disable. Suggested: 600000 (10 minutes).</small>
|
||||
<small>Timeout in minutes for detecting stuck tasks. When a task's agent session shows no activity for longer than this duration, the task is terminated and retried. Leave empty to disable. Suggested: 10.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="groupOverlappingFiles" className="checkbox-label">
|
||||
|
||||
@@ -833,14 +833,14 @@ describe("SettingsModal", () => {
|
||||
expect(layout!.querySelector(".settings-content")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has .settings-sidebar with 10 .settings-nav-item buttons for all sections", async () => {
|
||||
it("has .settings-sidebar with 11 .settings-nav-item buttons for all sections", async () => {
|
||||
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
const sidebar = container.querySelector(".settings-sidebar");
|
||||
expect(sidebar).toBeTruthy();
|
||||
const navItems = sidebar!.querySelectorAll(".settings-nav-item");
|
||||
expect(navItems.length).toBe(10);
|
||||
expect(navItems.length).toBe(11);
|
||||
|
||||
// Labels include scope emoji indicators (🌐 for global, 📁 for project)
|
||||
const labels = Array.from(navItems).map((el) => el.textContent);
|
||||
@@ -853,6 +853,7 @@ describe("SettingsModal", () => {
|
||||
"📁Worktrees",
|
||||
"📁Commands",
|
||||
"📁Merge",
|
||||
"📁Backups",
|
||||
"🌐Notifications",
|
||||
"Authentication",
|
||||
]);
|
||||
@@ -1327,11 +1328,11 @@ describe("SettingsModal", () => {
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Scheduling"));
|
||||
const input = screen.getByLabelText("Stuck Task Timeout (ms)");
|
||||
const input = screen.getByLabelText("Stuck Task Timeout (minutes)");
|
||||
expect(input).toBeTruthy();
|
||||
expect(input.getAttribute("type")).toBe("number");
|
||||
expect(input.getAttribute("min")).toBe("0");
|
||||
expect(input.getAttribute("step")).toBe("60000");
|
||||
expect(input.getAttribute("min")).toBe("1");
|
||||
expect(input.getAttribute("step")).toBe("1");
|
||||
});
|
||||
|
||||
it("Stuck Task Timeout field saves correctly when set to a value", async () => {
|
||||
@@ -1339,9 +1340,9 @@ describe("SettingsModal", () => {
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Scheduling"));
|
||||
const input = screen.getByLabelText("Stuck Task Timeout (ms)") as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "600000" } });
|
||||
expect(input.value).toBe("600000");
|
||||
const input = screen.getByLabelText("Stuck Task Timeout (minutes)") as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "10" } });
|
||||
expect(input.value).toBe("10");
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
|
||||
@@ -1350,7 +1351,7 @@ describe("SettingsModal", () => {
|
||||
expect(payload.taskStuckTimeoutMs).toBe(600000);
|
||||
});
|
||||
|
||||
it("Stuck Task Timeout field submits undefined when set to 0 or empty (disabled)", async () => {
|
||||
it("Stuck Task Timeout field submits undefined when set to empty (disabled)", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
taskStuckTimeoutMs: 600000,
|
||||
@@ -1360,7 +1361,7 @@ describe("SettingsModal", () => {
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Scheduling"));
|
||||
const input = screen.getByLabelText("Stuck Task Timeout (ms)") as HTMLInputElement;
|
||||
const input = screen.getByLabelText("Stuck Task Timeout (minutes)") as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
@@ -1375,7 +1376,21 @@ describe("SettingsModal", () => {
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Scheduling"));
|
||||
expect(screen.getByText(/Timeout in milliseconds for detecting stuck tasks/)).toBeTruthy();
|
||||
expect(screen.getByText(/Timeout in minutes for detecting stuck tasks/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Stuck Task Timeout field displays correct minute value from milliseconds setting", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
taskStuckTimeoutMs: 600000,
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Scheduling"));
|
||||
const input = screen.getByLabelText("Stuck Task Timeout (minutes)") as HTMLInputElement;
|
||||
expect(input.value).toBe("10");
|
||||
});
|
||||
|
||||
it("scope banners render for global and project sections", async () => {
|
||||
|
||||
Reference in New Issue
Block a user