From 2e2dff79f2c5549f79e61ff4d83e0cf51cb09548 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 16 Sep 2026 06:12:43 -0300 Subject: [PATCH] fix(cli): redraw Windows tray icon with dark outline + ship icon.ico (#13535) (#13797) Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging). --- bin/cli/tray/icon.ico | Bin 0 -> 1102 bytes bin/cli/tray/icon.png | Bin 713 -> 286 bytes .../fixes/13535-windows-tray-icon-contrast.md | 1 + electron/assets/tray-icon.png | Bin 713 -> 286 bytes tests/unit/tray-icon-contrast.test.ts | 104 ++++++++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 bin/cli/tray/icon.ico create mode 100644 changelog.d/fixes/13535-windows-tray-icon-contrast.md create mode 100644 tests/unit/tray-icon-contrast.test.ts diff --git a/bin/cli/tray/icon.ico b/bin/cli/tray/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f0891cfc79f7bbbfd680d2462c9c19de187e4ed1 GIT binary patch literal 1102 zcmZQzU}Run5D;Jh(h3Z#7#JAbfLKBT!oLAze*j_y1qfe`k%8d^5E~dk_!&U50@Yy4K$HKfP)Q4%76Z!2&6a*JR*x37?@vxFyq$hplqOEjHioZ zh(&L5!U9zRLBXH=hf=bC{Qv*oKE*DM+1176pYuV{q9>9IxYj+`)HuQBYa_>zW7$CY z_o}C_{TDc!;INKyuZW}qQ?F6iB}S_S^;QCono_AN#Lii;6Nj0%2!A-jVR2bAZhwoN*Sb2X3gRj7X(+nrR&0f&D*8DxI#>e{9 zGko&Ffr){0v!>2C|2^&e^c4S1LCpu|)U1pWUeL|JGmD{?<-tnM09S^0Sqw82vTm0} z@AO}1$7yjjHQD5^#-I7|zZp!vG3G>{eDaM^Wn0omLDwX0G4X%Dgsi6C%ZU4St~crR zAp)TTOu-BW;Lr(Y`q_>nbQq;Tp_9@0cBU_1lLJrdbAk7l^gJRe^qrGZ7P=d*UCGB} z_3-@6B~$B8Mmt*V*`~R$<$*=tWG04stq3Lt;ft|H&OBi&+I~1D`h@Apnk#SiYFsTk z+~888w8+h*{K6&nL&tvnHgZ^hKwYj_>~9r=+N0+tAI_XQ&L|$J#_X%LPg#Y?c<`bngVj8uyjs`B-#q{EM#@(+qIdd0x8giXxJF~xYZqST>at;r- zvTu3Uy5$Ie^MQThvpfPGS1-I%J$q>|Yt(nA?79Mn$8jE08+F(GJh)J(RPydz#^ao3 R6$}c03WG2ZMRSOM=`I8py;l)L7xpN{UxEn+iH#) zNWMspA^=EK=m19hTKk^eAvhDs2gxQDX;0Pbo4 zm>_D~c3Zhmu09Dk)n#Ke{V4pOU;6+w_5o~L$>jC{B(`Gl5k+Dv78m~d5;~RMXkPZt UjbiJ<00000Ne4wvM6N<$f{Lqc$^ZZW literal 713 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4zl0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+uenMVO6iP5s=4O;1O926a+~Cd6kb#fh_hC zPhVH|S4=WI;v&564Lv|1)e_f;l9a@fRIB8oR3OD*WME{bYha{nWD#O$WMyP*Wn`dj zU|?lnu<&+DCyIvL{FKbJO57SQwB0cUYS4h&P?DLOT3nKtTYy_n`{ci`fO@P!dWy@^ zt&;O|b5rw57!;g=WKm*{LSBAKssfjSe`!f-5tvg9Z(<)tQQ1;uzv_{Aj4P-(d%lIomxYTMtRN$8cX(FizM|QuuNY&x3Mu{!K23i(d%|RyHX`ne5^Vy;yj<6kV?!%74tKPCOF^mW^4$t zetbQAQH4&crn%0H<4#u!GIzopr08mZ^cmMzZ diff --git a/changelog.d/fixes/13535-windows-tray-icon-contrast.md b/changelog.d/fixes/13535-windows-tray-icon-contrast.md new file mode 100644 index 0000000000..8c4aa21333 --- /dev/null +++ b/changelog.d/fixes/13535-windows-tray-icon-contrast.md @@ -0,0 +1 @@ +- **fix(cli):** redraw the CLI/Electron system tray icon with a dark outline and ship a native multi-res `icon.ico` so it is no longer a pure-white, nearly invisible glyph on the Windows light-theme taskbar and hidden-icons flyout (#13535) — thanks @ProphetOfDoom-PoD diff --git a/electron/assets/tray-icon.png b/electron/assets/tray-icon.png index 4e4abe2b781251126f9206c28774816f554d57c6..508f949e162abace5490f51a05f5f5332216b071 100644 GIT binary patch delta 270 zcmV+p0rCFH1)c(s8Gi-<0047(dh`GQ010qNS#tmY1Lyz%1Ly&@ds1rv007WQL_t(o zh3%F>3WG2ZMRSOM=`I8py;l)L7xpN{UxEn+iH#) zNWMspA^=EK=m19hTKk^eAvhDs2gxQDX;0Pbo4 zm>_D~c3Zhmu09Dk)n#Ke{V4pOU;6+w_5o~L$>jC{B(`Gl5k+Dv78m~d5;~RMXkPZt UjbiJ<00000Ne4wvM6N<$f{Lqc$^ZZW literal 713 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4zl0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+uenMVO6iP5s=4O;1O926a+~Cd6kb#fh_hC zPhVH|S4=WI;v&564Lv|1)e_f;l9a@fRIB8oR3OD*WME{bYha{nWD#O$WMyP*Wn`dj zU|?lnu<&+DCyIvL{FKbJO57SQwB0cUYS4h&P?DLOT3nKtTYy_n`{ci`fO@P!dWy@^ zt&;O|b5rw57!;g=WKm*{LSBAKssfjSe`!f-5tvg9Z(<)tQQ1;uzv_{Aj4P-(d%lIomxYTMtRN$8cX(FizM|QuuNY&x3Mu{!K23i(d%|RyHX`ne5^Vy;yj<6kV?!%74tKPCOF^mW^4$t zetbQAQH4&crn%0H<4#u!GIzopr08mZ^cmMzZ diff --git a/tests/unit/tray-icon-contrast.test.ts b/tests/unit/tray-icon-contrast.test.ts new file mode 100644 index 0000000000..d9ebca19cf --- /dev/null +++ b/tests/unit/tray-icon-contrast.test.ts @@ -0,0 +1,104 @@ +// Regression guard for #13535: the Windows CLI system tray icon must contain a dark +// outline/stroke so it stays visible against the light-theme taskbar/hidden-icons +// background. Windows' NotifyIcon paints bitmap pixel colors literally — unlike macOS +// there is no "template image" auto-tinting — so a pure-white glyph with no outline is +// effectively invisible there. See bin/cli/tray/tray.ts::getIconPath() (prefers icon.ico +// on win32, falling back to icon.png) and electron/main.js (darwin-only setTemplateImage). +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; + +const repoRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); +const ICON_PNG_PATH = join(repoRoot, "bin", "cli", "tray", "icon.png"); +const ICON_ICO_PATH = join(repoRoot, "bin", "cli", "tray", "icon.ico"); +const ELECTRON_TRAY_ICON_PATH = join(repoRoot, "electron", "assets", "tray-icon.png"); + +// Windows 11 Fluent "hidden icons" overflow flyout / light-theme taskbar background. +const LIGHT_BG = { r: 243, g: 243, b: 243 }; +// A representative dark-theme taskbar background. +const DARK_BG = { r: 32, g: 32, b: 32 }; + +function relativeLuminance({ r, g, b }: { r: number; g: number; b: number }): number { + const lin = (c: number) => { + const s = c / 255; + return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); + }; + const [rl, gl, bl] = [lin(r), lin(g), lin(b)]; + return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl; +} + +function contrastRatio( + a: { r: number; g: number; b: number }, + b: { r: number; g: number; b: number } +): number { + const la = relativeLuminance(a); + const lb = relativeLuminance(b); + const [lighter, darker] = la >= lb ? [la, lb] : [lb, la]; + return (lighter + 0.05) / (darker + 0.05); +} + +async function assertHasVisibleContrastAgainstBothThemes(iconPath: string, label: string) { + const raw = readFileSync(iconPath); + const { data, info } = await sharp(raw).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); + assert.equal(info.channels, 4, `expected RGBA after ensureAlpha() for ${label}`); + + let opaqueCount = 0; + let nonWhiteOpaqueCount = 0; + let bestContrastOnLight = 0; + let bestContrastOnDark = 0; + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; + if (a < 128) continue; + opaqueCount++; + if (!(r === 255 && g === 255 && b === 255)) nonWhiteOpaqueCount++; + const ratioLight = contrastRatio({ r, g, b }, LIGHT_BG); + if (ratioLight > bestContrastOnLight) bestContrastOnLight = ratioLight; + const ratioDark = contrastRatio({ r, g, b }, DARK_BG); + if (ratioDark > bestContrastOnDark) bestContrastOnDark = ratioDark; + } + + assert.ok(opaqueCount > 0, `${label} has no opaque pixels at all — nothing would render`); + assert.ok( + nonWhiteOpaqueCount > 0, + `${label}'s glyph is pure-white-only (0 non-white opaque pixels out of ${opaqueCount}) — ` + + `no outline/stroke exists to provide contrast on a light background (bug #13535)` + ); + assert.ok( + bestContrastOnLight >= 3, + `${label}'s best pixel contrast against the Windows light tray background (#F3F3F3) is only ` + + `${bestContrastOnLight.toFixed(3)}:1, below the WCAG 3:1 UI-component minimum (bug #13535)` + ); + assert.ok( + bestContrastOnDark >= 3, + `${label}'s best pixel contrast against a dark tray background (#202020) is only ` + + `${bestContrastOnDark.toFixed(3)}:1, below the WCAG 3:1 UI-component minimum (bug #13535)` + ); +} + +test("icon.ico now ships next to icon.png so the win32 tray path uses the native .ico asset", () => { + assert.ok(existsSync(ICON_PNG_PATH), `expected ${ICON_PNG_PATH} to exist`); + assert.equal( + existsSync(ICON_ICO_PATH), + true, + "icon.ico is missing from bin/cli/tray/ — tray.ts::getIconPath() prefers it on win32 " + + "but silently falls back to icon.png when absent (bug #13535)" + ); +}); + +test("bin/cli/tray/icon.png has a dark outline visible on both light and dark Windows tray backgrounds", async () => { + await assertHasVisibleContrastAgainstBothThemes(ICON_PNG_PATH, "bin/cli/tray/icon.png"); +}); + +test("electron/assets/tray-icon.png has a dark outline visible on both light and dark tray backgrounds (Windows/Linux Electron tray, no template-image auto-tint)", async () => { + await assertHasVisibleContrastAgainstBothThemes( + ELECTRON_TRAY_ICON_PATH, + "electron/assets/tray-icon.png" + ); +});