Skip to content

fix: update menubar content in real time while menu is open#426

Merged
jundot merged 2 commits intojundot:mainfrom
EmotionalAmo:fix/menubar-real-time-update
Mar 29, 2026
Merged

fix: update menubar content in real time while menu is open#426
jundot merged 2 commits intojundot:mainfrom
EmotionalAmo:fix/menubar-real-time-update

Conversation

@EmotionalAmo
Copy link
Copy Markdown
Contributor

@EmotionalAmo EmotionalAmo commented Mar 27, 2026

Fixes #425

Problem

After clicking "Start Server", the menu bar content (server status text, Admin Panel / Chat button enabled state) never updates while the menu is open. The user must close and reopen the menu to see the latest state. Even if the menu stays open, the content is completely frozen.

Root Causes

1. NSTimer registered under NSDefaultRunLoopMode only (app.py:136)

When the status-bar menu is open, macOS enters NSEventTrackingRunLoopMode. NSDefaultRunLoopMode is suspended during this time, so healthCheck_ never fires and the menu content freezes completely.

2. _build_menu() replaces the entire NSMenu object (app.py:490, 730)

Even if the timer fired, calling self.status_item.setMenu_(newMenu) while a menu is being displayed would close it — causing disruptive flicker and not the desired in-place update.

Fix

Two complementary changes in packaging/omlx_app/app.py:

1. NSRunLoopCommonModes

Register the health-check timer under NSRunLoopCommonModes instead of NSDefaultRunLoopMode so it fires in all run-loop modes, including during menu interaction.

# Before
NSRunLoop.currentRunLoop().addTimer_forMode_(self.health_timer, NSDefaultRunLoopMode)

# After
NSRunLoop.currentRunLoop().addTimer_forMode_(self.health_timer, NSRunLoopCommonModes)

2. In-place menu item updates via _refresh_menu_in_place()

Instead of replacing the NSMenu object, store references to the key NSMenuItem objects and mutate them directly:

  • Status header: setAttributedTitle_() with updated color/text
  • Server control buttons: setHidden_() to toggle Start / Stop / Force Restart visibility
  • Admin Panel / Chat: setEnabled_() based on running state

3. NSMenuDelegatemenuWillOpen_ / menuDidClose_

  • menuWillOpen_: always refreshes menu content right before it appears, so even without the timer the menu is never stale on open.
  • menuDidClose_: clears the _menu_is_open flag so healthCheck_ uses _build_menu() (full rebuild with stats) when the menu is closed.

Test Steps

  1. Launch the app — menu shows "● oMLX Server is stopped" + "Start Server"
  2. Open the menu and keep it open, then click "Start Server"
  3. ✅ Status text updates in place: "Stopped" → "Starting..." → "Running" — no need to close the menu
  4. ✅ "Admin Panel" and "Chat with oMLX" become enabled automatically once running
  5. Close and reopen the menu — state is correct immediately (menuWillOpen_)
  6. Menubar icon updates alongside status changes

Two root causes were fixed:

1. NSTimer was registered under NSDefaultRunLoopMode only, which is
   suspended while the status-bar menu is open (macOS enters
   NSEventTrackingRunLoopMode). Changed to NSRunLoopCommonModes so
   healthCheck_ fires even during menu interaction.

2. _build_menu() replaces the entire NSMenu object via setMenu_(), which
   would close a currently-visible menu. Instead, when the menu is open
   we now call _refresh_menu_in_place(), which mutates the existing
   NSMenuItem objects in place (status header attributed title, server
   control button visibility via setHidden_, Admin Panel / Chat enabled
   state).

Additional changes:
- Adopt NSMenuDelegate; menuWillOpen_ always refreshes items before the
  menu is shown, so reopening also reflects the latest state instantly.
- menuDidClose_ clears the _menu_is_open flag so _build_menu() is used
  for full rebuilds (stats submenu, etc.) when the menu is closed.
- Server control section now always adds all three items (Stop / Force
  Restart / Start) and uses setHidden_ to show the relevant one, enabling
  in-place toggling without menu replacement.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Copy link
Copy Markdown
Owner

@jundot jundot 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 PR! The approach is solid. Found a few issues though.

_fetch_stats() freezes the menu while open

NSRunLoopCommonModes makes healthCheck_ fire during menu tracking on the main thread. _fetch_stats() does sync HTTP with 2s timeout, up to 3 calls worst case. This blocks the main thread for 6-8s every 5s while the menu is open.

Fix: skip _fetch_stats() when self._menu_is_open is True.

STOPPING state: all buttons hidden

ServerStatus has 6 values including STOPPING, but none of the three visibility conditions cover it. No control button shows up during STOPPING. Add ServerStatus.STOPPING to the Stop button's visibility tuple.

Button order reversed for UNRESPONSIVE

Original shows Force Restart first (the more important action), then Stop. The PR flips this. Was this intentional?

Minor: icon template state

_build_menu() calls setTemplate_(True) to gray out Admin/Chat icons when stopped. _refresh_menu_in_place() only toggles setEnabled_() without updating template state, so icon color can get out of sync.

Four issues raised in jundot#426:

1. Skip _fetch_stats() when menu is open to avoid blocking the main
   thread. _fetch_stats() makes up to 3 synchronous HTTP requests with
   2s timeouts each, which could stall the UI for ~6s during menu
   event tracking. Stats are fetched on the next healthCheck_ cycle
   after the menu closes.

2. Add ServerStatus.STOPPING to the Stop Server button visibility
   condition in both _build_menu() and _refresh_menu_in_place(). The
   button was hidden during the STOPPING transition, leaving no control
   visible to the user.

3. Restore original button order: Force Restart appears before Stop
   Server (Force Restart is the primary action when UNRESPONSIVE).
   The previous commit had them in the wrong order.

4. Sync icon template state in _refresh_menu_in_place() by calling
   setTemplate_(True) on the Admin Panel and Chat icons after updating
   their enabled state, keeping icon rendering consistent with
   _build_menu().

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@EmotionalAmo
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! All four issues have been addressed in the follow-up commit (339922b):

  1. _fetch_stats() blocks main thread — Skipped when _menu_is_open is True. The stats fetch only runs (and _last_stats_fetch only advances) when the menu is closed, so it will catch up on the next timer cycle after the menu is dismissed.

  2. STOPPING state hides all buttons — Added ServerStatus.STOPPING to the Stop Server visibility condition in both _build_menu() and _refresh_menu_in_place().

  3. Button order reversed for UNRESPONSIVE — Not intentional. Restored the original order: Force Restart first, Stop Server second.

  4. Icon template state out of sync — Added icon.setTemplate_(True) after each setEnabled_() call in _refresh_menu_in_place() to keep icon rendering consistent with _build_menu().

@jundot jundot merged commit 61e6848 into jundot:main Mar 29, 2026
@EmotionalAmo EmotionalAmo deleted the fix/menubar-real-time-update branch March 29, 2026 10:15
jundot pushed a commit that referenced this pull request Mar 29, 2026
* fix: update menubar content in real time while menu is open

Two root causes were fixed:

1. NSTimer was registered under NSDefaultRunLoopMode only, which is
   suspended while the status-bar menu is open (macOS enters
   NSEventTrackingRunLoopMode). Changed to NSRunLoopCommonModes so
   healthCheck_ fires even during menu interaction.

2. _build_menu() replaces the entire NSMenu object via setMenu_(), which
   would close a currently-visible menu. Instead, when the menu is open
   we now call _refresh_menu_in_place(), which mutates the existing
   NSMenuItem objects in place (status header attributed title, server
   control button visibility via setHidden_, Admin Panel / Chat enabled
   state).

Additional changes:
- Adopt NSMenuDelegate; menuWillOpen_ always refreshes items before the
  menu is shown, so reopening also reflects the latest state instantly.
- menuDidClose_ clears the _menu_is_open flag so _build_menu() is used
  for full rebuilds (stats submenu, etc.) when the menu is closed.
- Server control section now always adds all three items (Stop / Force
  Restart / Start) and uses setHidden_ to show the relevant one, enabling
  in-place toggling without menu replacement.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: address review feedback on menubar real-time update PR

Four issues raised in #426:

1. Skip _fetch_stats() when menu is open to avoid blocking the main
   thread. _fetch_stats() makes up to 3 synchronous HTTP requests with
   2s timeouts each, which could stall the UI for ~6s during menu
   event tracking. Stats are fetched on the next healthCheck_ cycle
   after the menu closes.

2. Add ServerStatus.STOPPING to the Stop Server button visibility
   condition in both _build_menu() and _refresh_menu_in_place(). The
   button was hidden during the STOPPING transition, leaving no control
   visible to the user.

3. Restore original button order: Force Restart appears before Stop
   Server (Force Restart is the primary action when UNRESPONSIVE).
   The previous commit had them in the wrong order.

4. Sync icon template state in _refresh_menu_in_place() by calling
   setTemplate_(True) on the Admin Panel and Chat icons after updating
   their enabled state, keeping icon rendering consistent with
   _build_menu().

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

---------

Co-authored-by: EmotionalAmo <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
@jundot
Copy link
Copy Markdown
Owner

jundot commented Mar 29, 2026

@EmotionalAmo v0.3.0rc1 is out with your menubar fix included. https://github.com/jundot/omlx/releases/tag/v0.3.0rc1 — if you get a chance, please give it a test and let me know if anything looks off. thanks!

AlexTzk pushed a commit to AlexTzk/omlx that referenced this pull request Mar 29, 2026
* fix: update menubar content in real time while menu is open

Two root causes were fixed:

1. NSTimer was registered under NSDefaultRunLoopMode only, which is
   suspended while the status-bar menu is open (macOS enters
   NSEventTrackingRunLoopMode). Changed to NSRunLoopCommonModes so
   healthCheck_ fires even during menu interaction.

2. _build_menu() replaces the entire NSMenu object via setMenu_(), which
   would close a currently-visible menu. Instead, when the menu is open
   we now call _refresh_menu_in_place(), which mutates the existing
   NSMenuItem objects in place (status header attributed title, server
   control button visibility via setHidden_, Admin Panel / Chat enabled
   state).

Additional changes:
- Adopt NSMenuDelegate; menuWillOpen_ always refreshes items before the
  menu is shown, so reopening also reflects the latest state instantly.
- menuDidClose_ clears the _menu_is_open flag so _build_menu() is used
  for full rebuilds (stats submenu, etc.) when the menu is closed.
- Server control section now always adds all three items (Stop / Force
  Restart / Start) and uses setHidden_ to show the relevant one, enabling
  in-place toggling without menu replacement.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: address review feedback on menubar real-time update PR

Four issues raised in jundot#426:

1. Skip _fetch_stats() when menu is open to avoid blocking the main
   thread. _fetch_stats() makes up to 3 synchronous HTTP requests with
   2s timeouts each, which could stall the UI for ~6s during menu
   event tracking. Stats are fetched on the next healthCheck_ cycle
   after the menu closes.

2. Add ServerStatus.STOPPING to the Stop Server button visibility
   condition in both _build_menu() and _refresh_menu_in_place(). The
   button was hidden during the STOPPING transition, leaving no control
   visible to the user.

3. Restore original button order: Force Restart appears before Stop
   Server (Force Restart is the primary action when UNRESPONSIVE).
   The previous commit had them in the wrong order.

4. Sync icon template state in _refresh_menu_in_place() by calling
   setTemplate_(True) on the Admin Panel and Chat icons after updating
   their enabled state, keeping icon rendering consistent with
   _build_menu().

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

---------

Co-authored-by: EmotionalAmo <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

菜单栏内容在服务器状态变化时不实时更新

2 participants