Skip to main content

Module tray

Module tray 

Source
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 per SPEC_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.
MenuItem
A menu entry, resolved from the platform-neutral model below. Kept as data (rather than built directly against muda/ksni types) so the menu’s composition is unit-testable without a display server.

Enums§

TrayAction
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 spawn reports success. Every backend builds the icon on another thread (Windows: its pump thread; macOS: the AppKit main thread; Linux: the ksni thread), and start_if_enabled must not report success for an icon that was only requested: unavailable depends 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_TIMEOUT and 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. None when 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_config sets it from app:showtray). Presence-based, matching AGENTMUX_DEV and AGENTMUX_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.