authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-11 20:19:22-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-11 20:19:22-03:00
log095d57552c4579ce33a6560a76b7c643b92f00f9
tree839b3a02943508b4f0aa3295ab64ef387b976325
parent0eaa18c12936dffde078b277be91cbafa5758cb9
signature Signed by PGP key B5690EEEBB952194

fix(desktop): hold-to-quit no longer strands the quit (#11016)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

2 files changed, 27 insertions(+), 10 deletions(-)

apps/desktop/src/window/QuitHold.test.ts+24-3
...@@ -178,14 +178,35 @@ describe("makeQuitShortcutHandler", () => {...@@ -178,14 +178,35 @@ describe("makeQuitShortcutHandler", () => {
178 await harness.holdFor(QUIT_HOLD_DURATION_MS + 200);178 await harness.holdFor(QUIT_HOLD_DURATION_MS + 200);
179 await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false }));179 await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false }));
180 harness.preventDefault.mockClear();180 harness.preventDefault.mockClear();
181 await harness.send(makeInput({ meta: false, isAutoRepeat: true }));181 // Repeats without the modifier prove Q is still down, so they hold the
182 expect(harness.preventDefault).toHaveBeenCalledTimes(1);182 // quit back for as long as they keep arriving.
183 vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS * 2);183 await harness.holdFor(QUIT_HOLD_RELEASE_GRACE_MS * 2, { meta: false });
184 expect(harness.preventDefault).toHaveBeenCalled();
184 expect(harness.quit).not.toHaveBeenCalled();185 expect(harness.quit).not.toHaveBeenCalled();
185 await harness.send(makeInput({ type: "keyUp", meta: false }));186 await harness.send(makeInput({ type: "keyUp", meta: false }));
186 expect(harness.quit).toHaveBeenCalledTimes(1);187 expect(harness.quit).toHaveBeenCalledTimes(1);
187 });188 });
188189
190 it("commits a concealed hold when the last Q repeat is never released", async () => {
191 // macOS can drop the final Q keyUp. The quit must land on its own once
192 // repeats stop, rather than sitting armed until an unrelated key arrives.
193 const harness = makeHarness();
194 await harness.send(makeInput({}));
195 await harness.holdFor(QUIT_HOLD_DURATION_MS + 200);
196 await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false }));
197 await harness.send(makeInput({ meta: false, isAutoRepeat: true }));
198
199 vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS);
200 expect(harness.quit).toHaveBeenCalledTimes(1);
201
202 // A lone Cmd tap afterwards must not quit a second time.
203 harness.quit.mockClear();
204 await harness.send(makeInput({ key: "Meta" }));
205 await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false }));
206 vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS * 4);
207 expect(harness.quit).not.toHaveBeenCalled();
208 });
209
189 it("does not quit when the hold stops before the duration", async () => {210 it("does not quit when the hold stops before the duration", async () => {
190 const harness = makeHarness();211 const harness = makeHarness();
191 await harness.send(makeInput({}));212 await harness.send(makeInput({}));
apps/desktop/src/window/QuitHold.ts+3-7
...@@ -125,13 +125,9 @@ export function makeQuitShortcutHandler(...@@ -125,13 +125,9 @@ export function makeQuitShortcutHandler(
125 }125 }
126 if (quitOnRelease) {126 if (quitOnRelease) {
127 event.preventDefault();127 event.preventDefault();
128 if (key === "q") {128 // A Q keydown proves the key is still down whether or not the modifier
129 if (modifierDown) {129 // is still held, so it only pushes the quiet period back.
130 quitAfterQuietPeriod();130 if (key === "q") quitAfterQuietPeriod();
131 } else {
132 clearWatchdog();
133 }
134 }
135 return;131 return;
136 }132 }
137133