Skip to content

app: Add a setting to disable the system tray icon#5754

Open
VlatkoMilisav wants to merge 3 commits into
kubernetes-sigs:mainfrom
VlatkoMilisav:feat/optional-system-tray
Open

app: Add a setting to disable the system tray icon#5754
VlatkoMilisav wants to merge 3 commits into
kubernetes-sigs:mainfrom
VlatkoMilisav:feat/optional-system-tray

Conversation

@VlatkoMilisav
Copy link
Copy Markdown

Summary

This PR makes the desktop system tray icon optional by adding a persisted "Show system tray icon" setting, addressing the request that the tray (added in #4030 / #4452) is currently created unconditionally with no opt-out.

Related Issue

Fixes #5753

Changes

  • Added isTrayIconEnabled / setTrayIconEnabled in app/electron/tray.ts, persisted via the existing settings.json (key enableSystemTray, defaults to true so current behavior is unchanged)
  • createHeadlampTray now returns early when the tray is disabled
  • app/electron/main.ts: extracted tray options into a shared builder and added applyTrayIconSetting, which creates or removes the tray at runtime (no restart needed); added request-tray-icon / set-tray-icon IPC handlers
  • app/electron/preload.ts: whitelisted the new IPC channels
  • Added a "Show system tray icon" toggle to General Settings, shown only in the desktop app (isElectron()), wired through the desktop API
  • Ran npm run i18n to add the new translation key

Steps to Test

  1. Run the desktop app (make run-app or npm start in app/) on macOS/Linux/Windows
  2. Observe the tray icon appears in the system tray/menu bar (default behavior unchanged)
  3. Go to Settings → General → toggle off "Show system tray icon"
  4. Observe the tray icon disappears immediately, without restarting
  5. Toggle it back on and observe the tray icon reappears
  6. Restart the app and confirm the chosen setting persists

Screenshots (if applicable)

Notes for the Reviewer

  • This touches the i18n layer (new translation key across locale files) - only translation.json was changed; the parser's incidental glossary.json reformatting was reverted.
  • The setting is desktop-only and defaults to enabled, so web and existing desktop users see no behavior change.
  • Happy to add tests or adjust the setting key/label/placement if you'd prefer a different approach.

@k8s-ci-robot k8s-ci-robot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label May 19, 2026
@k8s-ci-robot k8s-ci-robot requested review from kahirokunn and sniok May 19, 2026 09:16
@k8s-ci-robot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: VlatkoMilisav
Once this PR has been reviewed and has the lgtm label, please assign sniok for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot
Copy link
Copy Markdown
Contributor

Welcome @VlatkoMilisav!

It looks like this is your first PR to kubernetes-sigs/headlamp 🎉. Please refer to our pull request process documentation to help your PR have a smooth ride to approval.

You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. Here is the bot commands documentation.

You can also check if kubernetes-sigs/headlamp has its own contribution guidelines.

You may want to refer to our testing guide if you run into trouble with your tests not passing.

If you are having difficulty getting your pull request seen, please follow the recommended escalation practices. Also, for tips and tricks in the contribution process you may want to read the Kubernetes contributor cheat sheet. We want to make sure your contribution gets all the attention it needs!

Thank you, and welcome to Kubernetes. 😃

@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented May 19, 2026

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: VlatkoMilisav / name: VlatkoMilisav (90c9153)

@k8s-ci-robot k8s-ci-robot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels May 19, 2026
@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. and removed cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels May 19, 2026
@VlatkoMilisav VlatkoMilisav marked this pull request as ready for review May 19, 2026 09:32
@k8s-ci-robot k8s-ci-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label May 19, 2026
@yolossn yolossn requested a review from Copilot May 19, 2026 12:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an Electron-only, persisted preference to enable/disable the system tray icon at runtime (no restart), keeping the default behavior unchanged (tray enabled unless explicitly disabled).

Changes:

  • Persist a new settings.json flag (enableSystemTray, defaulting to enabled) and gate tray creation on it.
  • Add IPC plumbing to query/update the setting from the renderer and create/destroy the tray live.
  • Add a desktop-only “Show system tray icon” toggle in General Settings and introduce the new i18n key across locales.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
app/electron/main.ts Adds IPC handlers and runtime apply logic to create/destroy the tray based on the persisted setting.
app/electron/preload.ts Whitelists new IPC channels for requesting/updating tray icon state.
app/electron/tray.ts Adds persisted getters/setters for the tray preference and skips tray creation when disabled.
frontend/src/components/App/Settings/Settings.tsx Adds an Electron-only toggle wired to IPC to show/hide the tray icon.
frontend/src/i18n/locales/en/translation.json Adds “Show system tray icon” translation key (source string).
frontend/src/i18n/locales/de/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/es/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/fr/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/hi/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/it/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/ja/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/ko/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/pt/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/ru/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/ta/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/zh/translation.json Adds “Show system tray icon” translation key.
frontend/src/i18n/locales/zh-tw/translation.json Adds “Show system tray icon” translation key.
Comments suppressed due to low confidence (1)

app/electron/preload.ts:63

  • desktopApi.receive registers a wrapper function with ipcRenderer.on(...), but the renderer later calls desktopApi.removeListener(channel, originalHandler). Since the wrapper reference is different, removeListener cannot actually unsubscribe, leading to leaked listeners (and duplicated callbacks on remount). Consider storing a mapping from original handler -> wrapped handler (and using it in removeListener), or have receive return an explicit unsubscribe function.
    if (validChannels.includes(channel)) {
      // Deliberately strip event as it includes `sender`
      ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
  },

Comment thread frontend/src/components/App/Settings/Settings.tsx
Comment thread app/electron/main.ts Outdated
Copy link
Copy Markdown
Contributor

@illume illume left a comment

Choose a reason for hiding this comment

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

Thanks for working on this.

The commit messages could use some tidying up to match our contribution guidelines. We use Linux kernel style — the contributing guide has the details, and git log shows good examples.

Commits that need attention
  • Add setting to disable the system tray icon — Missing area: description prefix — e.g. frontend: HomeButton: Fix so it navigates to home or backend: config: Add enable-dynamic-clusters flag.
Commit guidelines
  • Use atomic commits focused on a single change.
  • Use the title format <area>: <Description of changes> — description must start with a capital letter.
  • Keep the title under 72 characters (soft requirement).
  • Explain the intention and why the change is needed.
  • Make commit titles meaningful and describe what changed.
  • Do not add code that a later commit rewrites; squash or reorder commits instead.
  • Do not include Fixes #NN in commit messages.

Good examples:

  • frontend: HomeButton: Fix so it navigates to home
  • backend: config: Add enable-dynamic-clusters flag

Would you mind addressing the open Copilot review comments? Please mark each comment as resolved after addressing it.

Looks like the frontend snapshots are out of date — cd frontend && npm run test -- -u will update them.

How to update snapshots

Run cd frontend && npm run test -- -u to regenerate all snapshots. Review the diff to make sure the visual changes are intentional, then commit the updated snapshot files.

@VlatkoMilisav VlatkoMilisav force-pushed the feat/optional-system-tray branch from 90c9153 to 49d7eaa Compare May 19, 2026 14:42
@VlatkoMilisav
Copy link
Copy Markdown
Author

Thanks for the review! Updated:

  • Commit message: reworded to app: Add setting to disable the system tray icon (kernel style, area prefix, no Fixes #NN), squashed to a single atomic commit.
  • Copilot comments: both addressed and resolved.
    • Listener leak: receive in the preload now returns an unsubscribe function that removes the exact wrapped listener; the Settings effect uses it for cleanup.
    • Payload coercion: set-tray-icon now ignores non-boolean payloads instead of !!-coercing.

On the snapshot check: the failing snapshots in CI are Table > WithGlobalFilter, GRPCRoute/DetailsView > Basic and JobSet/List > Items. This PR only touches the Electron tray code and the General Settings page (the new toggle is gated behind isElectron(), so it doesn't render in the storyshot env), and none of those three snapshots relate to it - they pass locally on this branch and look like pre-existing drift on main rather than something this change introduced. Running npm run test -- -u locally rewrites ~120 unrelated storyshots, so I've kept them out to keep the PR atomic. Happy to update them here if you'd prefer, or address separately - let me know which you'd like.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

app/electron/preload.ts:71

  • receive registers a wrapped listener with ipcRenderer.on(...), but removeListener still forwards the original func directly to ipcRenderer.removeListener(...), so callers that use window.desktopApi.removeListener(channel, originalFunc) will not actually unsubscribe. This can lead to accumulating IPC listeners (e.g. existing frontend code relies on removeListener for the plugin-manager channel). Consider either (a) tracking a mapping from original handler -> wrapped handler per channel and using it in removeListener, or (b) removing/deprecating removeListener in favor of the unsubscribe function returned from receive and updating call sites accordingly.
      const wrapped = (event: unknown, ...args: unknown[]) => func(...args);
      ipcRenderer.on(channel, wrapped);
      // Return an unsubscribe function so callers can clean up the exact
      // wrapped listener (removeListener with the original func cannot).
      return () => ipcRenderer.removeListener(channel, wrapped);
    }
  },

  removeListener: (channel: string, func: (...args: unknown[]) => void) => {
    ipcRenderer.removeListener(channel, func);
  },

Comment thread app/electron/tray.ts Outdated
Comment thread app/electron/tray.ts Outdated
@VlatkoMilisav VlatkoMilisav force-pushed the feat/optional-system-tray branch from 49d7eaa to 2522a09 Compare May 19, 2026 16:02
@VlatkoMilisav
Copy link
Copy Markdown
Author

Addressed Copilot's follow-up round too (both resolved):

  • Hardened isTrayIconEnabled/setTrayIconEnabled with an asSettingsObject() guard so a hand-edited settings.json with valid-but-non-object JSON can't throw.
  • Added app/electron/tray.test.ts covering default-unset, persistence both ways, unrelated-settings preservation, and the non-object fallback. The two helpers take an optional settingsPath (defaults unchanged) for testability.

Still folded into the single atomic commit.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

@VlatkoMilisav VlatkoMilisav force-pushed the feat/optional-system-tray branch from 2522a09 to 24471be Compare May 20, 2026 09:53
@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label May 20, 2026
@VlatkoMilisav
Copy link
Copy Markdown
Author

Rebased onto current main. The frontend-i18n-check failure was caused by the branch being stale - upstream added the ar, he, and ur locale dirs (RTL support, #5695) after this PR was opened. CI's parser saw the merged tree and wanted to add the "Show system tray icon" key to those new locales, but my committed state didn't have them yet. After the rebase, npm run i18n -- --fail-on-update exits 0 locally - only the three new locales' translation.json files needed the empty-value entry. Still one atomic commit.

The system tray icon was created unconditionally on launch with no
way to opt out. Add a persisted "Show system tray icon" preference
(default on, desktop only) that creates or removes the tray at
runtime without requiring a restart.
@VlatkoMilisav VlatkoMilisav force-pushed the feat/optional-system-tray branch from 24471be to a839014 Compare May 20, 2026 10:59
@k8s-ci-robot k8s-ci-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label May 20, 2026
@illume illume requested a review from Copilot May 21, 2026 08:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

app/electron/preload.ts:71

  • desktopApi.receive now registers a wrapped listener (to strip the Electron event) and returns an unsubscribe, but desktopApi.removeListener still calls ipcRenderer.removeListener(channel, func) with the original callback. That won’t remove listeners added via receive, so existing renderer code that pairs receive(..., cb) with removeListener(..., cb) (e.g. the plugin-manager flow) will leak listeners and can break request/response logic. Consider tracking original->wrapped callbacks per channel so removeListener can remove the wrapped listener, or deprecate removeListener and update all call sites to use the unsubscribe returned from receive.
      // Deliberately strip event as it includes `sender`
      const wrapped = (event: unknown, ...args: unknown[]) => func(...args);
      ipcRenderer.on(channel, wrapped);
      // Return an unsubscribe function so callers can clean up the exact
      // wrapped listener (removeListener with the original func cannot).
      return () => ipcRenderer.removeListener(channel, wrapped);
    }
  },

  removeListener: (channel: string, func: (...args: unknown[]) => void) => {
    ipcRenderer.removeListener(channel, func);
  },

@illume illume requested a review from Copilot May 22, 2026 06:05
Copy link
Copy Markdown
Contributor

@illume illume left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution.

it looks like there's a merge-main commit in this PR — could you rebase onto main instead?

Why this matters

Merge commits from main make the PR history harder to review. Please rebase your branch on top of the latest main instead, then update the PR with the rebased commits.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Comment thread app/electron/preload.ts
Comment on lines 60 to +65
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
const wrapped = (event: unknown, ...args: unknown[]) => func(...args);
ipcRenderer.on(channel, wrapped);
// Return an unsubscribe function so callers can clean up the exact
// wrapped listener (removeListener with the original func cannot).
return () => ipcRenderer.removeListener(channel, wrapped);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

app: Add a setting to disable the macOS menu bar tray icon

4 participants