From 2c8956e634e30810417eeefa665ace4678c0a38e Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Thu, 10 Sep 2026 17:24:34 +0100 Subject: [PATCH] fix(maestro): reset pending state when kill dialog transitions to force mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The onConfirm handler in Sidebar resolves (not rejects) when the daemon refuses an unforced kill — it mutates state to switch to force mode and returns. That left `pending` stuck at true in the KillDialog, so the "Force kill" button rendered disabled. Co-Authored-By: Claude Opus 4.6 --- .../components/dialogs/KillDialog.test.tsx | 29 +++++++++++++++++++ web/src/components/dialogs/KillDialog.tsx | 5 ++++ 2 files changed, 34 insertions(+) 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);