Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions scripts/patch-linux-window-ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
Expand Down
48 changes: 48 additions & 0 deletions scripts/patches/main-process/quit-lifecycle.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use strict";

const { findMatchingBrace, requireName } = require("../shared.js");

function applyLinuxQuitGuardPatch(currentSource) {
let patchedSource = currentSource;

Expand Down Expand Up @@ -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, () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an auto review done by revuto.


Scoping note (please confirm against the real bundle): the replacement targets any {role:quit} token anywhere inside the getNativeTrayMenuItems(){...} body. The PR's stated intent ("Scope the replacement to tray menu construction so helper uses of Menu.buildFromTemplate([{ role: \"quit\" }]) keep working") is satisfied only when the label-helper that calls Menu.buildFromTemplate([{role:quit}]) is defined outside this method. The test at line 1617 places mH as a sibling function, which passes — but if upstream ever inlines such a helper (or any other Menu.buildFromTemplate([{role:quit}]) call) inside getNativeTrayMenuItems, this regex would rewrite that template entry into a {label:Quit,click:...} item too, changing the helper's label-extraction result. Worth an assertion or a tighter anchor (e.g. requiring the token to be a top-level array element of the returned menu) if that shape is plausible in the bundle.

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;

Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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) &&
Expand Down
Loading