diff --git a/web/src/components/dialogs/KillDialog.test.tsx b/web/src/components/dialogs/KillDialog.test.tsx index 5046a27..aed5dd1 100644 --- a/web/src/components/dialogs/KillDialog.test.tsx +++ b/web/src/components/dialogs/KillDialog.test.tsx @@ -71,4 +71,33 @@ describe("KillDialog", () => { await waitFor(() => expect(kill.disabled).toBe(false)); expect(screen.getByText("Failed to kill: daemon unreachable")).not.toBeNull(); }); + + it("resets pending when force changes, so the force button is clickable", async () => { + // Simulate the Sidebar flow: onConfirm resolves (parent switches to + // force mode via rerender), but pending was set to true before the call. + const onConfirm = vi.fn(() => Promise.resolve()); + const { rerender } = render( + , + ); + + const kill = screen.getByRole("button", { name: "Kill" }) as HTMLButtonElement; + fireEvent.click(kill); + expect(kill.disabled).toBe(true); + + // Parent re-renders with force=true (the daemon refused the unforced kill). + await waitFor(() => expect(onConfirm).toHaveBeenCalledOnce()); + rerender( + , + ); + + const forceBtn = screen.getByRole("button", { name: "Force kill" }) as HTMLButtonElement; + expect(forceBtn.disabled).toBe(false); + }); }); diff --git a/web/src/components/dialogs/KillDialog.tsx b/web/src/components/dialogs/KillDialog.tsx index 6674480..3494c2d 100644 --- a/web/src/components/dialogs/KillDialog.tsx +++ b/web/src/components/dialogs/KillDialog.tsx @@ -34,6 +34,11 @@ export function KillDialog(props: KillDialogProps) { // asked for the kill, so Enter confirms it. useEffect(() => confirmRef.current?.focus(), []); + // When the dialog transitions from normal → force mode, the previous + // onConfirm resolved without error (it mutated parent state and returned), + // so `pending` is still true. Reset it so the force button is clickable. + useEffect(() => setPending(false), [force]); + const handleConfirm = () => { setPending(true); setError(null);