From 095d57552c4579ce33a6560a76b7c643b92f00f9 Mon Sep 17 00:00:00 2001 From: maria Date: Fri, 11 Sep 2026 20:19:22 -0300 Subject: [PATCH] fix(desktop): hold-to-quit no longer strands the quit (#11016) Co-authored-by: Claude Opus 5 (1M context) --- apps/desktop/src/window/QuitHold.test.ts | 27 +++++++++++++++++++++--- apps/desktop/src/window/QuitHold.ts | 10 +++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/window/QuitHold.test.ts b/apps/desktop/src/window/QuitHold.test.ts index 58809d8eb14d6901465a6dd61071e0400bee43fe..47f3ec5298ec2247714264266490b015d544a7fb 100644 --- a/apps/desktop/src/window/QuitHold.test.ts +++ b/apps/desktop/src/window/QuitHold.test.ts @@ -178,14 +178,35 @@ describe("makeQuitShortcutHandler", () => { await harness.holdFor(QUIT_HOLD_DURATION_MS + 200); await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false })); harness.preventDefault.mockClear(); - await harness.send(makeInput({ meta: false, isAutoRepeat: true })); - expect(harness.preventDefault).toHaveBeenCalledTimes(1); - vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS * 2); + // Repeats without the modifier prove Q is still down, so they hold the + // quit back for as long as they keep arriving. + await harness.holdFor(QUIT_HOLD_RELEASE_GRACE_MS * 2, { meta: false }); + expect(harness.preventDefault).toHaveBeenCalled(); expect(harness.quit).not.toHaveBeenCalled(); await harness.send(makeInput({ type: "keyUp", meta: false })); expect(harness.quit).toHaveBeenCalledTimes(1); }); + it("commits a concealed hold when the last Q repeat is never released", async () => { + // macOS can drop the final Q keyUp. The quit must land on its own once + // repeats stop, rather than sitting armed until an unrelated key arrives. + const harness = makeHarness(); + await harness.send(makeInput({})); + await harness.holdFor(QUIT_HOLD_DURATION_MS + 200); + await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false })); + await harness.send(makeInput({ meta: false, isAutoRepeat: true })); + + vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS); + expect(harness.quit).toHaveBeenCalledTimes(1); + + // A lone Cmd tap afterwards must not quit a second time. + harness.quit.mockClear(); + await harness.send(makeInput({ key: "Meta" })); + await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false })); + vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS * 4); + expect(harness.quit).not.toHaveBeenCalled(); + }); + it("does not quit when the hold stops before the duration", async () => { const harness = makeHarness(); await harness.send(makeInput({})); diff --git a/apps/desktop/src/window/QuitHold.ts b/apps/desktop/src/window/QuitHold.ts index a995184ddd707234ef1a62ebebdb6bb09cb84f60..f656183f137563865f474af07795e516fa6e9071 100644 --- a/apps/desktop/src/window/QuitHold.ts +++ b/apps/desktop/src/window/QuitHold.ts @@ -125,13 +125,9 @@ export function makeQuitShortcutHandler( } if (quitOnRelease) { event.preventDefault(); - if (key === "q") { - if (modifierDown) { - quitAfterQuietPeriod(); - } else { - clearWatchdog(); - } - } + // A Q keydown proves the key is still down whether or not the modifier + // is still held, so it only pushes the quiet period back. + if (key === "q") quitAfterQuietPeriod(); return; } -- 2.54.0