Skip to Content
文档托盘架构

Tray Architecture & Behavior Spec

Scope: this document is the source of truth for how the runex tray menu-bar icon behaves.

Status: ported from runexfab (2026-07-07). Updated by every change that touches the tray process model, callback wiring, or button semantics.


1. Process model

┌──────────────────────────────────────────────────────────────────┐ │ User's menu bar (RX icon) │ │ 4 items + separator │ └──────────────────────────────────────────────────────────────────┘ │ pystray NSStatusItem (Cocoa runloop) ┌──────────────────────────────────────────────────────────────────┐ │ Process: runex │ │ │ │ ┌────────────────────────┐ ┌──────────────────────────────┐ │ │ │ Main thread │ │ Background daemon threads │ │ │ │ (Cocoa runloop) │ │ - _worker_toggle_pause │ │ │ │ - pystray menu cb's │ │ - _watch_listener (health) │ │ │ │ - icon.update_menu() │ │ │ │ │ └────────┬───────────────┘ └──────────┬───────────────────┘ │ │ │ spawn via subprocess.Popen │ │ │ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Child process: runex ontology listen │ │ │ │ - filesystem watcher (FSEvents on macOS) │ │ │ │ - runex engine event loop │ │ │ └─────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘

2. Threading rules

OperationAllowed threadWhy
icon.update_menu()main thread onlypystray manipulates NSStatusItem, must be on Cocoa runloop
icon.icon = X (setter)any threadpystray’s setter internally posts via performSelectorOnMainThread
_notify() (osascript subprocess)any threadexternal subprocess, no UI thread requirement
state.kill_listen() / state.spawn_listen()any threadhold state.lock for the whole body
_worker_toggle_pause bodydaemon threadkeeps callback <10 ms
_watch_listener bodydaemon threadpolls listen_child.poll() every 5 s

3. Menu

4 items + 1 separator:

ItemAction
暂停 / 工作中 (toggle)Pause/resume background sync + listen host
开机自启 (toggle)Install/uninstall LaunchAgent plist
关于Display dialog with version
退出Quit tray

See src/runex/tray/app.py for full implementation.

4. Pause semantics

  • 暂停: SIGTERM the listen child + dispatch pause_connection for each active IngestConnection via in-process Python API
  • 工作中: dispatch resume_connection for each paused connection + re-spawn listen child
  • Manual runex node update from terminal still fires guards while paused
  • DB is never touched by pause/resume — data survives

5. Failure modes

FailureDetectionRecoveryData loss
Listener crashes_watch_listener polls every 5 sAuto-respawnnone
Tray killed externallymacOS orphan reaper on next launch_reap_orphaned_listen() for stale PIDsnone
Vault file editedwatcher detectsin-place mirror, node_id preservednone
Vault file deletedwatcher detectsnode marked mirror_state=orphannone
Last updated on