@@ -1215,9 +1215,10 @@ test("subagent nickname metadata descriptor follows upstream metadata bundle nam
12151215
12161216function trayBundleFixture() {
12171217 return [
1218- "async function fae(e){let t=await pae(e.buildFlavor,e.appBrand,e.repoRoot),r=new n.Tray(t.defaultIcon);r.setToolTip(n.app.getName());let i=new pb(r);return i}",
1219- "async function pae(e,t,r){if(process.platform===`darwin`)return null;if(process.platform===`linux`){let e=n.nativeImage.createFromPath(`tray.png`);return{defaultIcon:e,chronicleRunningIcon:null}}return null}",
1220- "var pb=class{trayMenuThreads={runningThreads:[],unreadThreads:[],pinnedThreads:[],recentThreads:[],usageLimits:[]};constructor(e={on(){},setContextMenu(){}}){this.tray=e;if(process.platform===`linux`){this.tray.on(`click`,()=>{}),this.updatePersistentTrayMenu();return}}getNativeTrayMenuItems(){return[]}updatePersistentTrayMenu(){process.platform===`linux`&&this.tray.setContextMenu(n.Menu.buildFromTemplate(this.getNativeTrayMenuItems()))}}",
1218+ "async function gj(e){let t=e;if(typeof t.whenReady!=`function`)return process.platform!==`linux`;try{return await t.whenReady(),!0}catch{return!1}}function _j(e){let t=e;return typeof t.isReady==`function`?t.isReady():process.platform!==`linux`}",
1219+ "async function fae(e){let t=await pae(e.buildFlavor,e.appBrand,e.repoRoot),r=new c.Tray(t.defaultIcon);r.setToolTip(c.app.getName());let i=new pb(r);return!await i.waitForReady()?(i.destroy(),null):i}",
1220+ "async function pae(e,t,n){if(process.platform===`darwin`)return null;if(process.platform===`linux`){let r=`${fv(e,t)}.png`,i=c.nativeImage.createFromPath(c.app.isPackaged?(0,u.join)(process.resourcesPath,r):(0,u.join)(n,`electron`,`src`,`icons`,r));if(i.isEmpty())throw Error(`Linux tray application icon is unavailable`);return{defaultIcon:i.resize({width:V9,height:V9,quality:`best`}),chronicleRunningIcon:null}}return null}",
1221+ "var pb=class{trayMenuThreads={runningThreads:[],unreadThreads:[],pinnedThreads:[],recentThreads:[],usageLimits:[]};constructor(e={on(){},setContextMenu(){}}){this.tray=e;if(process.platform===`linux`){this.tray.on(`click`,()=>{}),this.updatePersistentTrayMenu();return}}destroy(){this.tray.destroy()}isReady(){return _j(this.tray)}waitForReady(){return gj(this.tray)}getNativeTrayMenuItems(){return[]}updatePersistentTrayMenu(){process.platform===`linux`&&this.tray.setContextMenu(c.Menu.buildFromTemplate(this.getNativeTrayMenuItems()))}}",
12211222 "v&&k.on(`close`,e=>{this.persistPrimaryWindowBounds(k);let t=this.getPrimaryWindows().some(e=>e!==k);if((process.platform===`win32`||process.platform===`linux`)&&!this.isAppQuitting&&this.options.canHideLastWindowToTray?.()===!0&&!t){e.preventDefault(),k.hide();return}if(process.platform===`darwin`&&!this.isAppQuitting&&!t){e.preventDefault(),k.hide()}});",
12221223 "let oe=async()=>{try{await fae({appBrand:a.U(),buildFlavor:b,repoRoot:j.repoRoot})}catch(e){v.reportNonFatal(e)}};(E||process.platform===`linux`)&&oe();",
12231224 ].join("");
@@ -2403,18 +2404,19 @@ test("adds the Linux quit guard for the current interleaved bundler prelude", ()
24032404
24042405test("destroys the registered Linux tray before the app exits", () => {
24052406 const source = `${currentMainBundlePrefix}${trayBundleFixture()}`;
2407+ const iconPathExpression = "process.resourcesPath+`/../content/webview/assets/app-test.png`";
24062408 const patched = applyPatchTwice(
24072409 applyLinuxTrayPatch,
24082410 applyLinuxQuitGuardPatch(source),
2409- null ,
2411+ iconPathExpression ,
24102412 );
24112413
24122414 assert.match(patched, /codexLinuxRegisterTray=e=>\(codexLinuxTray=e,e\)/);
24132415 assert.match(patched, /codexLinuxDestroyTray=\(\)=>\{if\(process\.platform!==`linux`\)return;/);
24142416 assert.match(patched, /codexLinuxTray=null;try\{e\?\.destroy\(\)\}catch\{\}/);
24152417 assert.match(patched, /codexLinuxMarkQuitInProgress=\(\)=>\{codexLinuxQuitInProgress=!0,codexLinuxDestroyTray\(\)\}/);
24162418 assert.match(patched, /c\.app\.on\(`before-quit`,\(\)=>codexLinuxDestroyTray\(\)\)/);
2417- assert.match(patched, /r=typeof codexLinuxRegisterTray===`function`\?codexLinuxRegisterTray\(new n \.Tray\(t\.defaultIcon\)\):new n \.Tray\(t\.defaultIcon\)/);
2419+ assert.match(patched, /r=typeof codexLinuxRegisterTray===`function`\?codexLinuxRegisterTray\(new c \.Tray\(t\.defaultIcon\)\):new c \.Tray\(t\.defaultIcon\)/);
24182420 assert.doesNotMatch(patched, /codexLinuxTrayQuitDelayMs/);
24192421
24202422 const helperStart = patched.indexOf("let codexLinuxTray=null");
@@ -2427,6 +2429,104 @@ test("destroys the registered Linux tray before the app exits", () => {
24272429 assert.equal(runDestroy({ platform: "linux" }), 1);
24282430});
24292431
2432+ test("accepts stock Electron tray readiness and falls back to the Linux app icon", async () => {
2433+ const iconPathExpression = "process.resourcesPath+`/../content/webview/assets/app-test.png`";
2434+ const source = `${currentMainBundlePrefix}${trayBundleFixture()}`;
2435+ const patched = applyPatchTwice(applyLinuxTrayPatch, source, iconPathExpression);
2436+
2437+ assert.match(
2438+ patched,
2439+ /if\(typeof t\.whenReady!=`function`\)return!0;try\{return await t\.whenReady\(\),!0\}catch\{return!1\}/,
2440+ );
2441+ assert.match(
2442+ patched,
2443+ /return typeof t\.isReady==`function`\?t\.isReady\(\):!0/,
2444+ );
2445+ assert.match(
2446+ patched,
2447+ new RegExp(
2448+ `let __codexLinuxTrayFallbackIcon=c\\.nativeImage\\.createFromPath\\(${escapeRegExp(iconPathExpression)}\\)`,
2449+ ),
2450+ );
2451+ assert.match(
2452+ patched,
2453+ /if\(!__codexLinuxTrayFallbackIcon\.isEmpty\(\)\)i=__codexLinuxTrayFallbackIcon/,
2454+ );
2455+
2456+ const readinessHelpers = patched.match(
2457+ /async function gj\(e\)\{let t=e;[^]*?\}function _j\(e\)\{let t=e;[^}]+\}/,
2458+ )?.[0];
2459+ assert.ok(readinessHelpers);
2460+ const context = { process: { platform: "linux" }, result: null };
2461+ await vm.runInNewContext(
2462+ `${readinessHelpers};result=(async()=>({stockWait:await gj({}),stockReady:_j({}),nativeWait:await gj({whenReady:async()=>{}}),nativeReady:_j({isReady:()=>!1}),failedWait:await gj({whenReady:async()=>{throw Error(\`not ready\`)}})}))()`,
2463+ context,
2464+ );
2465+ assert.deepEqual(
2466+ JSON.parse(JSON.stringify(await context.result)),
2467+ {
2468+ stockWait: true,
2469+ stockReady: true,
2470+ nativeWait: true,
2471+ nativeReady: false,
2472+ failedWait: false,
2473+ },
2474+ );
2475+
2476+ const iconLoaderStart = patched.indexOf("async function pae(");
2477+ const iconLoaderEnd = patched.indexOf("var pb=class", iconLoaderStart);
2478+ assert.notEqual(iconLoaderStart, -1);
2479+ assert.notEqual(iconLoaderEnd, -1);
2480+ const iconLoaderSource = patched.slice(iconLoaderStart, iconLoaderEnd);
2481+ const iconCalls = [];
2482+ const iconContext = {
2483+ process: { platform: "linux", resourcesPath: "/resources" },
2484+ upstreamEmpty: true,
2485+ iconCalls,
2486+ c: {
2487+ app: { isPackaged: true },
2488+ nativeImage: {
2489+ createFromPath(iconPath) {
2490+ iconCalls.push(iconPath);
2491+ const fallback = iconPath.includes("content/webview/assets/app-test.png");
2492+ return {
2493+ isEmpty: () => fallback ? false : iconContext.upstreamEmpty,
2494+ resize: () => ({ source: fallback ? "fallback" : "upstream" }),
2495+ };
2496+ },
2497+ },
2498+ },
2499+ u: path,
2500+ fv: () => "icon-chatgpt",
2501+ V9: 16,
2502+ result: null,
2503+ };
2504+ await vm.runInNewContext(
2505+ `${iconLoaderSource};result=pae(\`prod\`,\`chatgpt\`,\`/repo\`)`,
2506+ iconContext,
2507+ );
2508+ assert.deepEqual(JSON.parse(JSON.stringify(await iconContext.result)), {
2509+ defaultIcon: { source: "fallback" },
2510+ chronicleRunningIcon: null,
2511+ });
2512+ assert.deepEqual(iconCalls, [
2513+ "/resources/icon-chatgpt.png",
2514+ "/resources/../content/webview/assets/app-test.png",
2515+ ]);
2516+
2517+ iconContext.upstreamEmpty = false;
2518+ iconCalls.length = 0;
2519+ await vm.runInNewContext(
2520+ `${iconLoaderSource};result=pae(\`prod\`,\`chatgpt\`,\`/repo\`)`,
2521+ iconContext,
2522+ );
2523+ assert.deepEqual(JSON.parse(JSON.stringify(await iconContext.result)), {
2524+ defaultIcon: { source: "upstream" },
2525+ chronicleRunningIcon: null,
2526+ });
2527+ assert.deepEqual(iconCalls, ["/resources/icon-chatgpt.png"]);
2528+ });
2529+
24302530test("bypasses the upstream before-quit confirmation after a Linux explicit quit", () => {
24312531 const source = `${currentMainBundlePrefix}${beforeQuitConfirmationBundleFixture()}`;
24322532 const patched = applyPatchTwice(
0 commit comments