Expand description
Optional system-tray / menu-bar icon — issue #2977 Workstream 1,
docs/specs/SPEC_TRAY_OPTIONAL_BACKGROUND_SERVICE_2026_09_04.md.
§Why this lives in the launcher, not the host
§2 of the spec puts tray ownership in agentmux-launcher deliberately: the
icon must survive host crashes and restarts, and the launcher is the
process that outlives them. It is also the process that already knows how
to ask for a new window (second_instance::forward_open_new_window).
§The event-loop problem (§7.5)
The spec originally assumed the tray would “coexist with whatever message
pump the launcher already runs for its splash”. There is no such pump on
Windows — the splash creates a real window but registers DefWindowProcW
and polls, which works only because it is layered, non-activating, and
takes no input. The launcher’s real loop is Tokio. So the Windows backend
here introduces a message pump on a dedicated thread; see windows.rs.
macOS is the opposite: AppKit insists the status item be created and
driven on the main thread, and the launcher’s main thread already pumps
NSApplication for the process lifetime. So the macOS backend does not
own a thread at all — it queues a request the main-thread pump services,
see macos.rs. Linux uses ksni, which is pure D-Bus and needs no
toolkit loop at all.
§What is shared
Everything downstream of “the user picked something” is platform-neutral
and lives here: the menu model, the reachability probe that drives the
icon’s state, the retrying forwarder to the host, and the action loop the
supervisor spawns. A new backend only has to turn native events into
TrayActions and call spawn_action_loop from its supervisor.
§When it runs
When AGENTMUX_TRAY or AGENTMUX_BACKGROUND_SERVICE is set
(presence-based, matching the AGENTMUX_DEV idiom). background_config
sets them from two separate settings: app:showtray (the icon, on by
default) and app:runinbackground (keep running after the last window
closes, opt-in). Background mode forces the icon (should_enable), and
the converse holds too: when the tray was requested but failed to start,
unavailable tells the host spawn to drop background mode, so there is
never a resident process without an icon. See
docs/reports/REPORT_TRAY_BACKGROUND_DEFAULT_ON_2026_09_28.md for why the
two are kept apart.
Modules§
- linux 🔒
- Linux tray backend — StatusNotifierItem over D-Bus via
ksni(SPEC_TRAY_OPTIONAL_BACKGROUND_SERVICE_2026_09_04.md§2, §7.6; notification hub perSPEC_OS_NOTIFICATIONS_SYSTEM_2026_09_24.md§4.2). - notify_
menu 🔒 - The tray as a notification hub —
SPEC_OS_NOTIFICATIONS_SYSTEM_2026_09_24.md§4.2.
Structs§
- Check
- A check item’s state.
- Menu
Item - A menu entry, resolved from the platform-neutral model below. Kept as data
(rather than built directly against
muda/ksnitypes) so the menu’s composition is unit-testable without a display server.
Enums§
- Tray
Action - What the user picked from the tray menu. Platform backends translate their native click/menu events into these; everything downstream is platform-neutral, so the handling logic is written and tested once.
Constants§
- LOGIN_
TRAY_ 🔒WAIT - How long a login start keeps trying for a tray host that is not up yet
(
SPEC_START_WITH_OS_2026_09_25.md§3.4). At login the Linux StatusNotifier watcher can register after the autostart entry runs; nobody is looking at a login start yet, and a late tray beats a window nobody asked for. - READY_
TIMEOUT 🔒 - How long a backend waits for its icon to actually exist before
spawnreports success. Every backend builds the icon on another thread (Windows: its pump thread; macOS: the AppKit main thread; Linux: the ksni thread), andstart_if_enabledmust not report success for an icon that was only requested:unavailabledepends on it (ReAgent P1s on #3785). - STATUS_
POLL - How often backends re-check whether the background service is actually reachable. Cheap (a loopback connect with a short timeout) and slow enough to be invisible; the icon is a status indicator, not a monitor.
Statics§
- TRAY_
UNAVAILABLE 🔒 - Set when the tray was requested but could not start. Read by the host spawn.
Functions§
- background_
service_ from_ env - Background-service mode, as the host reads it. The launcher needs its own read because it decides whether to show the icon before/independently of the host reporting in.
- forward_
tray_ cmd - Forward a tray action to the host, retrying while the failure is
Transient. - menu_
model - The tray menu, as data.
- ready_
timeout 🔒 - The Linux backend’s ready timeout: long enough to cover the retries a
login start makes there. Windows and macOS do not retry (their icon does
not depend on another process being up), so they keep
READY_TIMEOUTand a failed start still falls back fast. - service_
reachable - Is the background service actually reachable right now?
- should_
enable - Should the tray be started at all?
- spawn_
action_ loop - Drain user actions from a backend and forward each to the host.
- start_
if_ enabled - Start the tray if enabled, returning the receiver the caller polls for
user actions.
Nonewhen the tray is disabled or unsupported on this platform — callers treat that as “no tray”, never as an error. - tooltip
- Tooltip shown on hover: just the name and version. The running state is reported by the menu’s first item (“New Window” / “Start AgentMux”).
- tray_
opt_ in_ from_ env - Read the tray switch from the environment (
background_configsets it fromapp:showtray). Presence-based, matchingAGENTMUX_DEVandAGENTMUX_BACKGROUND_SERVICE. - unavailable
- The tray was requested but failed to start, so background mode must not be handed to the host: closing the last window would otherwise leave a resident process with no icon and no visible way to quit it.