Skip to content

feat: macOS menu bar app for remote CLI management#1

Open
gldc wants to merge 10 commits intobuckle42:masterfrom
gldc:feature/menubar-app
Open

feat: macOS menu bar app for remote CLI management#1
gldc wants to merge 10 commits intobuckle42:masterfrom
gldc:feature/menubar-app

Conversation

@gldc
Copy link

@gldc gldc commented Feb 19, 2026

Summary

  • Adds a macOS menu bar app (scripts/menubar.py) for one-click control of remote CLI services
  • Color-coded status icon (green/gray/red) with 5-second health polling
  • Shows Tailscale IP and MagicDNS hostname in the menu
  • Start/stop services, view logs, toggle auto-start on login
  • Open Voice UI / Terminal links use MagicDNS with IP fallback
  • Fixes PATH in start-remote-cli.sh so voice-wrapper launches correctly from launchd/menubar

Test plan

  • Install rumps (pip3 install rumps) and launch with nohup python3 scripts/menubar.py &>/dev/null &
  • Verify menu shows Tailscale IP and MagicDNS name
  • Start/stop services from the menu and confirm health status updates
  • Open Voice UI / Terminal and confirm they use MagicDNS URLs
  • Toggle auto-start on login on/off without the app crashing
  • Confirm voice-wrapper starts correctly when launched from menubar (PATH fix)

🤖 Generated with Claude Code

Gian Luca D'Intino-Conte and others added 10 commits February 19, 2026 00:02
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Also remove unused signal import from menubar.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Tailscale MagicDNS hostname to the menu bar app so users can see
their .ts.net domain alongside the IP. Also fix start-remote-cli.sh
to prepend Homebrew Python paths so voice-wrapper launches correctly
when started from launchd or the menubar app.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Calling launchctl load immediately starts a second instance of the
menubar app, and launchctl unload kills the running one. Just
write/remove the plist file instead — it takes effect on next login.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Falls back to Tailscale IP if MagicDNS is not available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Owner

@buckle42 buckle42 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @gldc — thanks for putting this together! The menu bar app is a great idea and fills a real usability gap. The UX thinking (three-state icon, quit dialog, MagicDNS fallback) is solid.

I found a few issues that need to be addressed before I can merge. Splitting them into critical (must fix) and important (should fix).


Critical

1. TCC violation in auto-start plist (menubar.py:228-237)

The generated plist points to os.path.abspath(__file__), which resolves to ~/Documents/.... macOS launchd agents can't access ~/Documents/ without Full Disk Access — the existing CLAUDE.md calls this out explicitly. The "Auto-start on Login" feature will silently fail after reboot.

The existing remote-cli.plist handles this by requiring scripts be copied to ~/.local/bin/remote-cli/. The menubar plist needs a similar approach — either copy the script to a safe location, or detect and warn when running from a TCC-protected path.

2. XML injection in plist template (menubar.py:26-42)

The plist is built with .format() string interpolation. If the repo path contains &, <, or >, the plist becomes malformed XML. Recommend using plistlib from the standard library instead, which handles escaping automatically.

3. Zombie processes on repeated start/stop (menubar.py:196-200)

start-remote-cli.sh runs a blocking watchdog loop that never returns. Each "Start Services" click spawns a Popen child that's never tracked or cleaned up. Repeated start/stop cycles will accumulate orphaned processes. Either track the Popen handle and terminate on stop, or use start_new_session=True to fully detach.


Important

4. Expensive polling (menubar.py:118-158)

Two tailscale subprocess calls every 5 seconds is heavy for a menu bar app. Tailscale IP/DNS rarely changes mid-session. Consider polling Tailscale info every 60 seconds — the PID-based health checks are fine at 5s since they're just file reads and signal checks.

5. Race condition in toggle (menubar.py:187-192)

Toggle logic compares the button title string to decide start vs stop, but the health check timer updates that title asynchronously. Could cause double-starts. A boolean flag for intent (separate from display state) would be more reliable.

6. No guard against double-start (menubar.py:196-200)

Nothing prevents calling _start_services() when already running. The start script does pkill -f "ttyd" which kills the running instance — brief outage that wasn't the user's intent.

7. Dead config code (menubar.py:106-108)

auto_start_services is never set to True anywhere. The "Auto-start on Login" toggle controls the launchd plist (different concept). This config path is unused and confusing — either wire it to a menu item or remove it.

8. Incomplete launchctl handling (menubar.py:241-243)

Uninstall deletes the plist but doesn't unload from launchd — the agent stays loaded until logout. Install doesn't call launchctl load either. The toggle doesn't take effect immediately.


Minor (can address later)

  • Missing rumps import guard — users get a raw ModuleNotFoundError
  • import sys inside a method instead of at the top of the file
  • Doesn't use shutil.which("tailscale") with fallback like voice-wrapper.py does
  • Hardcoded /opt/homebrew/opt/python@3.11/ in the PATH fix is specific to your machine — other users may have different Python versions
  • No user feedback when log files don't exist yet

Happy to discuss any of these if you have questions. The core concept is great — just needs these fixes to be solid for other users cloning the repo. Thanks again for the contribution!

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