feat: add "Icon color" submenu for Linux tray (Auto/Black/White)#491
feat: add "Icon color" submenu for Linux tray (Auto/Black/White)#491IliyaBrook wants to merge 6 commits intoaaddrick:mainfrom
Conversation
Adds a user-selectable "Icon color" submenu (Auto / Black / White)
between "Show App" and "Quit" on the Linux tray context menu. Fixes
the long-standing issue where the tray icon colour doesn't adapt to
the panel on setups where Electron's nativeTheme.shouldUseDarkColors
misdetects — most commonly KDE Plasma with a dark panel on a light
desktop theme. Label names describe the icon's colour (not the theme
it targets) to avoid the confusion the auto-detection naming caused.
- Auto (default) preserves pre-existing behaviour.
- Black / White are explicit overrides that win over the detector.
Supporting pieces:
- scripts/tray-icon-settings.js — persistence under
~/.config/Claude/linux-tray.json, with migration for users who
picked an earlier build's "dark"/"light" values.
- scripts/frame-fix-wrapper.js — Proxy on nativeTheme so
shouldUseDarkColors reflects the user's choice. Relaunch on
change uses spawn($APPIMAGE) for AppImage builds (app.relaunch()
crashes with SIGTRAP once the squashfs mount is torn down), and
app.relaunch() otherwise.
- scripts/patches/tray.sh:
* patch_tray_icon_submenu — injects the Menu.buildFromTemplate
entry anchored on the Quit i18n ID (dKX0bpR+a2), which is
more stable across releases than minified function names.
* patch_tray_inplace_update — fast-path for the tray rebuild
function that reuses the existing StatusNotifierItem on
theme change (setImage + setContextMenu) instead of
destroy+recreate. Fixes the pre-existing KDE duplicate-tray
bug where the old SNI was still registered when the new one
appeared.
- tests/tray-icon-settings.test.js — 19 unit tests (node --test).
- docs/learnings/tray-icon-theme.md — design notes, KDE Connect
reference, and troubleshooting.
Co-Authored-By: Claude <[email protected]>
Adds a user-selectable "Icon color" submenu (Auto / Black / White)
between "Show App" and "Quit" on the Linux tray context menu. Fixes
the long-standing issue where the tray icon colour doesn't adapt to
the panel on setups where Electron's nativeTheme.shouldUseDarkColors
misdetects — most commonly KDE Plasma with a dark panel on a light
desktop theme. Label names describe the icon's colour (not the theme
it targets) to avoid the confusion the auto-detection naming caused.
- Auto (default) preserves pre-existing behaviour.
- Black / White are explicit overrides that win over the detector.
Supporting pieces:
- scripts/tray-icon-settings.js — persistence under
~/.config/Claude/linux-tray.json, with migration for users who
picked an earlier build's "dark"/"light" values.
- scripts/frame-fix-wrapper.js — Proxy on nativeTheme so
shouldUseDarkColors reflects the user's choice. Relaunch on
change uses spawn($APPIMAGE) for AppImage builds (app.relaunch()
crashes with SIGTRAP once the squashfs mount is torn down), and
app.relaunch() otherwise.
- scripts/patches/tray.sh:
* patch_tray_icon_submenu — injects the Menu.buildFromTemplate
entry anchored on the Quit i18n ID (dKX0bpR+a2), which is
more stable across releases than minified function names.
* patch_tray_inplace_update — fast-path for the tray rebuild
function that reuses the existing StatusNotifierItem on
theme change (setImage + setContextMenu) instead of
destroy+recreate. Fixes the pre-existing KDE duplicate-tray
bug where the old SNI was still registered when the new one
appeared.
- tests/tray-icon-settings.test.js — 19 unit tests (node --test).
- docs/learnings/tray-icon-theme.md — design notes, KDE Connect
reference, and troubleshooting.
Co-Authored-By: Claude <[email protected]>
… features/change-icon-color # Conflicts: # docs/learnings/tray-icon-theme.md
aaddrick
left a comment
There was a problem hiding this comment.
Hey @IliyaBrook — thanks for the detailed writeup, and the learnings doc in particular is great. Leaving this as a COMMENT because there's a blocker, a repro question, and one scope thing that isn't really yours to answer.
1. Hardcoded minified locals in patch_tray_inplace_update
T, E, and M are extracted dynamically, but t (the icon-path arg) and e (the menuBarEnabled local) are literal in the injected string. Per CLAUDE.md these names change between releases, and the idempotency grep keys on (t)) so a future drift wouldn't re-trigger the patch cleanly. Both are extractable with the same pattern the rest of tray.sh uses:
# the path arg is the first formal parameter of the tray function
path_arg=$(grep -oP \
"async function ${tray_func}\(\K\w+(?=\))" "$index_js")
# the menuBarEnabled local — same shape as patch_menu_bar_default
menu_bar_local=$(grep -oP \
'const \K\w+(?=\s*=\s*\w+\("menuBarEnabled"\))' "$index_js" | head -1)Pass them in next to TRAY_VAR / EL_VAR / MENU_FUNC and interpolate.
2. Duplicate tray icon on OS theme change — got a repro?
I've been running KDE Plasma 6 daily and haven't hit the duplicate SNI you're describing. patch_tray_menu_handler already sleeps 250 ms after tray.destroy() for this race (see scripts/patches/tray.sh:56-59), and your learnings doc mentions that delay, so I want to rule out that the existing mitigation is already covering it on most setups before we add a second path.
Happy to believe there's a config where 250 ms isn't enough — Plasma DBus timing drifts a lot between Wayland / X11 and compositor versions — but I haven't run the build with these changes and triggered the race myself, so right now this is static analysis. If you can share KDE / Plasma version, trigger steps, and a screenshot of the two icons, I can confirm on my end. Either way the in-place update is cleaner than destroy + delay + recreate, so I'd just rather land it as "strictly cleaner than the existing mitigation" than "fixes a bug" unless we've got the repro — keeps the commit message and learnings doc honest for whoever hunts for it later.
3. Scope
Separate from anything above: I'm not sold on this repo taking on net-new features versus sticking to Linux compat patches and packaging. Not a code comment — the engineering is solid — it's a precedent question for me and the other maintainers. I've opened #492 for that conversation. Nothing for you to do here; just flagging why I'm not hitting approve.
Non-blocking
scripts/tray-icon-settings.js—_save()is a singlewriteFileSync, so a kill mid-write can truncate._load()falls back toDEFAULT_MODEgracefully so it's soft-fail, but a.tmp+renameSynccloses the window if you're already in there.- The submenu-strip regex works today because no radio label contains
]. Worth a comment if someone adds a label with nested brackets. .gitignorepicked up.idea— benign but unrelated.
Thanks again for the writeup.
Written by Claude Opus 4.7 via Claude Code
…extraction Expands `patch_tray_inplace_update` in `tray.sh` to dynamically extract key variables (icon path, menuBarEnabled flag) and improve idempotency checks. Refines fast-path logic to update the tray icon in place, avoiding duplicate registrations on KDE Plasma.
- scripts/tray-icon-settings.js: _save() now writes to <path>.tmp and renameSync's on top of the real file, so a kill mid-write can't truncate the settings file. The failure path also unlinks the stray .tmp if the write itself threw. - tests/tray-icon-settings.test.js: add coverage asserting the parent directory has only the final file after a save cycle (no leftover .tmp). - scripts/patches/tray.sh: document the [^\]]*? assumption in the submenu-strip regex — relies on no radio label containing a literal ']'. All addressed from @aaddrick's PR aaddrick#491 review. Co-Authored-By: Claude <[email protected]>
|
Thanks for the thorough review — addressing each point below. 1. Blocker: hardcoded
|
| Distro | Fedora Linux 43 (KDE Plasma Desktop Edition) |
| Plasma | 6.6.4 |
xdg-desktop-portal-kde |
6.6.4 |
| Session | Wayland (XDG_SESSION_TYPE=wayland) |
| Kernel | 6.19.12-200.fc43.x86_64 |
Steps (on pristine main, no PR changes)
git clone https://github.com/aaddrick/claude-desktop-debian.git
cd claude-desktop-debian
./build.sh --build appimage --clean no
./claude-desktop-*-amd64.AppImageThen in System Settings → Colors → Plasma Style, switch the Plasma style to a different variant (e.g. Breeze Light ↔ Breeze Dark) while the app is running.
Expected: the tray icon updates in place with the new colour.
Actual on main: the old StatusNotifierItem stays registered and the new one appears next to it — two Claude icons side by side. Neither survives logout on its own (both are gone on next login, but they persist for the session).
Video — reproduced on main clean cloned into new dir
duplicate_tray_icon_bug_change_theme.mp4
Video — same steps after this PR
duplicate_tray_icon_after_fix.webm
Happy to believe the 250 ms delay in patch_tray_menu_handler is sufficient on some setups — it genuinely might be timing-dependent on compositor version / portal implementation / hardware speed — but on Plasma 6.6.4 + Wayland + Fedora 43 it isn't, and the in-place update sidesteps the race regardless of where the timing line falls.
I'm fine either way on the wording in the commit message / learnings doc. If you'd still prefer "strictly cleaner than destroy + delay + recreate" over "fixes a bug" so the phrasing holds for future readers on whatever setup, I'll happily reword — the videos establish the behaviour on record either way.
3. Scope
Understood — parked, awaiting the outcome of #492. No scope expansion from my side beyond this PR.
4. Non-blocking nits — addressed
All three in 6458f81:
- Atomic write in
_save()— now writes to<path>.tmp+renameSyncso a kill mid-write can't truncate the file. Failure path unlinks the stray.tmp. Added a test asserting no leftover.tmpafter a save cycle. - Submenu-strip regex — added an
NB: [^\]]*? assumes no radio label contains ']'comment next to the regex with a pointer to what would need to change if a future label contains one (switch to a balanced-brace match). .gitignorepicking up.idea— kept on this branch as8fd1945. Happy to drop it and ship separately``` if you'd rather have the feature PR contain only feature commits — one word and I'll force-push without it.
Thanks again for the detailed feedback and for keeping the bar high on minifier-resilience.
|
One more note, in case the scope discussion in #492 tilts against user-facing settings: The "Icon color" submenu is the only fix I was able to find for the invisible-tray-icon problem on KDE. The learnings doc walks through the three auto-detection approaches I tried — If #492 concludes "no net-new features in this repo," I'd be glad to split Either way, happy to follow whichever shape makes this easier to land for you. |
|
By the way! Just recently I discovered that this icon duplication bug also occurs when, in Claude Desktop itself, we open the dropdown next to the username, click the Appearance button, and change the Theme value in that section 🤔 here's the video: duplicate_icon_change_theme_in_app.webm |
Summary
Adds a user-selectable Icon color submenu (Auto / Black / White) to the Linux tray context menu, sitting between Show App and Quit. This gives users explicit control over the tray icon colour for the setups where automatic detection falls short.
As a bonus, the same patch fixes a pre-existing minor bug: changing the desktop theme while Claude is already running used to leave a duplicate tray icon behind.
Why a feature, not just a fix?
Electron's
nativeTheme.shouldUseDarkColorsreports the desktop colour scheme, not the actual panel colour — Chromium reads the xdg-desktop-portalorg.freedesktop.appearancesetting and nothing else. On a lot of real-world setups those two diverge, and the tray icon ends up invisible.The one that prompted this PR is the out-of-the-box Fedora Plasma KDE 6 experience with the default Fedora theme: the panel is black by design, but the global colour scheme is light, so Electron picks the black tray icon and it disappears into the panel. Other affected scenarios include:
xdg-desktop-portal-kdemapping doesn't line up with the panel's rendered colour (the portal only translates the Breeze family — third-party schemes return "no preference")Rather than try to paper over every edge case of the detection heuristic, this PR does what apps in similar spots have settled on: hand the choice back to the user while keeping auto-detection as the default.
How it works for the user
Right-click the tray icon → pick one of:
nativeTheme.shouldUseDarkColorspicks the icon — identical to pre-PR behaviourTrayIconTemplate.png) for light panelsTrayIconTemplate-Dark.png) for dark panelsThe radio labels describe the icon's own colour, so "White" means you'll see a white icon — no mental inversion required. The selection is persisted to
~/.config/Claude/linux-tray.jsonand survives restarts.Bonus: duplicate tray icon on OS theme change
Unrelated to the feature, this PR also fixes a long-standing annoyance: if you switched the KDE colour scheme while Claude was open, you'd see two tray icons side by side — the old one plus the new one. The pre-existing tray rebuild logic destroys the current
StatusNotifierItemand immediately registers a new one; on KDE Plasma the new SNI can appear before Plasma has processed the unregister signal for the old one, and both