#529

fix: preserve tray image appearance on update

Openege0zcanwants to merge intofrom

Summary

  • preserve a tray's configured template mode and dimensions when setImage() replaces its native image
  • update the existing native tray item instead of forcing applications to hide and recreate it
  • add an interactive Kitchen reproduction and focused source-contract coverage

Problem and root cause

Creating a tray and updating its image did not use equivalent native paths.

Tray creation receives template, width, and height. On macOS, it creates an NSImage, applies setTemplate(...), sets the configured size, and assigns the image to the NSStatusItem.

The image-update path previously received only the replacement image path. It created a fresh NSImage and assigned it directly, without restoring template mode or the configured dimensions. This is reproducible even when setImage() is called with the same file used at creation.

For a white macOS template asset, the visible result is immediate: the icon is rendered correctly in black when the tray is created, then becomes white and nearly invisible against the light menu bar after setImage().

This also explains a downstream hide/show workaround. Recreating the tray routes the image back through the complete creation path, so template mode and size are restored. However, recreation also produces a new NSStatusItem; macOS treats it as a new menu-bar item and discards the position arranged by the user. The image-appearance bug is therefore the upstream defect, while the position jump is a consequence of working around it by recreating the tray.

The discrepancy is visible in the native implementation:

  • create: load image → apply template mode → apply configured size → assign
  • update before this PR: load image → assign

Fix

ElectrobunCore already stores is_template, width, and height in TrayState. This change reuses those values during every image update and passes them through the internal native ABI.

  • macOS: apply template mode and configured size to the replacement NSImage, then assign it to the existing NSStatusItem.
  • Windows: load the replacement icon using the tray's configured width and height instead of LR_DEFAULTSIZE.
  • Linux: accept the consistent internal ABI while retaining existing AppIndicator behavior; template mode and dimensions remain ignored at creation and update time as they are today.

The existing tray object is never removed or recreated, so its identity and user-arranged position are preserved. The public Tray.setImage(path) API is unchanged.

Minimal reproduction

  1. Run Kitchen and open Tray (Interactive) → Tray playground.
  2. Create a tray with the supplied template icon.
  3. On macOS, move it to a clearly visible menu-bar position.
  4. Click Update Image. The reproduction deliberately supplies the same asset, isolating replacement-image behavior.
  5. The button and tray title advance to Image update #1, confirming that the update path ran.

On upstream main, the icon changes from the normal black template rendering to a white, nearly invisible image. With this PR, its appearance, dimensions, and position remain unchanged.

Visual evidence

The screenshots use the same Kitchen asset and the same arranged menu-bar position:

Before update (upstream): black template icon beside My Tray.

After update (upstream): the counter advances to Image update #1, but the icon becomes white/ghosted.

After update (this PR): the counter advances to Image update #1 and the icon remains a black template image in the same position.

Validation

  • bun test package/src/shared/tray-image-update-contract.test.ts — 3 passed
  • full hutch dev native macOS build and Kitchen launch from upstream main plus only the reproduction UI: issue reproduced
  • full hutch dev native macOS build and Kitchen launch from this PR branch: image appearance and arranged position preserved
  • isolated downstream macOS harness confirmed that hide/show recreation resets menu-bar placement
  • git diff --check

A full Windows/Linux native build was not available locally. The focused contract test covers the platform-specific update semantics, and the Linux implementation remains behaviorally unchanged.