diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index b9a1e382..02e94389 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -1625,6 +1625,18 @@ test("supports explicit tray quit patching when upstream renames the quit label ); }); +test("replaces role-only tray quit items with an explicit Linux quit handler", () => { + const source = + "let n=require(`electron`);var q=class{getNativeTrayMenuItems(){return[{label:`New Chat`,click:()=>{}},{type:`separator`},{role:`quit`}]}};"; + const patched = applyPatchTwice(applyLinuxExplicitTrayQuitPatch, source); + + assert.doesNotMatch(patched, /\{role:`quit`\}/); + assert.match( + patched, + /\{label:`Quit`,click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\)\}\}/, + ); +}); + test("supports explicit IPC quit patching when minified aliases drift", () => { const source = "let x=require(`electron`);var q=class{getNativeTrayMenuItems(){return[{label:rB(this.appName),click:()=>{x.app.quit()}}]}};if(m.type===`quit-app`){x.app.quit();return}"; diff --git a/scripts/patches/main-process/quit-lifecycle.js b/scripts/patches/main-process/quit-lifecycle.js index edb5dc91..6335a1a3 100644 --- a/scripts/patches/main-process/quit-lifecycle.js +++ b/scripts/patches/main-process/quit-lifecycle.js @@ -1,5 +1,7 @@ "use strict"; +const { findMatchingBrace, requireName } = require("../shared.js"); + function applyLinuxQuitGuardPatch(currentSource) { let patchedSource = currentSource; @@ -45,6 +47,44 @@ function linuxExplicitQuitExpression() { return "typeof codexLinuxPrepareForExplicitQuit===`function`?codexLinuxPrepareForExplicitQuit():typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),"; } +function patchRoleOnlyTrayQuitItems(source, electronVar, quitMarkerExpression) { + let patchedSource = source; + let changed = false; + const methodNeedle = "getNativeTrayMenuItems(){"; + const roleOnlyTrayQuitRegex = /\{role:`quit`\}/g; + let cursor = 0; + + while (cursor < patchedSource.length) { + const methodIndex = patchedSource.indexOf(methodNeedle, cursor); + if (methodIndex === -1) { + break; + } + + const openIndex = methodIndex + methodNeedle.length - 1; + const closeIndex = findMatchingBrace(patchedSource, openIndex); + if (closeIndex === -1) { + break; + } + + const body = patchedSource.slice(openIndex + 1, closeIndex); + const patchedBody = body.replace(roleOnlyTrayQuitRegex, () => { + changed = true; + return `{label:\`Quit\`,click:()=>{${quitMarkerExpression}${electronVar}.app.quit()}}`; + }); + if (patchedBody !== body) { + patchedSource = + patchedSource.slice(0, openIndex + 1) + + patchedBody + + patchedSource.slice(closeIndex); + cursor = openIndex + 1 + patchedBody.length + 1; + } else { + cursor = closeIndex + 1; + } + } + + return { patchedSource, changed }; +} + function applyLinuxWillQuitDrainTimeoutPatch(currentSource) { let patchedSource = currentSource; @@ -128,6 +168,7 @@ function applyLinuxExplicitTrayQuitPatch(currentSource) { let patchedSource = currentSource; const quitMarkerExpression = linuxExplicitQuitExpression(); + const electronVar = requireName(currentSource, "electron") ?? "n"; const trayQuitNeedle = "{label:rB(this.appName),click:()=>{n.app.quit()}}"; const trayQuitPatch = @@ -157,6 +198,13 @@ function applyLinuxExplicitTrayQuitPatch(currentSource) { return `{label:${labelExpression},click:()=>{${quitMarkerExpression}${electronVar}.app.quit()}}`; }, ); + const roleOnlyTrayQuitPatch = patchRoleOnlyTrayQuitItems( + patchedSource, + electronVar, + quitMarkerExpression, + ); + patchedSource = roleOnlyTrayQuitPatch.patchedSource; + patchedAny ||= roleOnlyTrayQuitPatch.changed; if ( !patchedAny && !patchedTrayQuitRegex.test(patchedSource) &&