Skip to content

Move macOS menu bar into a gxserver-owned agent (server control + status) #38

Description

@gsxdsm

Summary

Move the macOS menu bar status item out of the main Ghostex.app and into a gxserver-owned, standalone LSUIElement agent (GxserverBar.app) that controls the daemon (Start / Stop / Restart, Open Logs) and shows the session-status indicator (attention / working / available), driven by gxserver's own presentation state over 127.0.0.1:58744. The old SessionStatusIndicatorController is removed from Ghostex.app; floating / in-window indicators are preserved.

A full implementation plan lives in the branch gxserver-menubar at docs/plans/2026-06-12-001-feat-gxserver-menubar-agent-plan.md.

Why

The daemon is the long-lived process that survives the app being closed ("Closing or quitting the macOS app must not stop gxserver", AppDelegate.swift:769), yet the only menu bar presence today lives in the window app and is a passive indicator with no server control. A control surface for the server should live with the server. Hard constraint: gxserver is a pure Node daemon and cannot draw an NSStatusItem, so it owns/launches a thin native agent rather than rendering the menu bar itself.

Implementation units (planned)

  • U1 — gxserver derives {attention, working, available} counts from presentation state; serves /api/ui/statusIndicators + broadcasts on /api/events.
  • U2 — New GxserverBar.app (LSUIElement): status item, control menu, HTTP/WS client, badge rendering moved from the old controller.
  • U3 — gxserver bootstraps the agent on startup (gated on resolving the staged bundle); agent persists and Start/Restart shell out to the bundled gxserver launcher.
  • U4 — Build + stage the agent in build-ghostex-host.sh (not the headless server package), macOS-only.
  • U5 — Cross-process focus via the existing ghostex:// scheme.
  • U6 — Remove SessionStatusIndicatorController from the main app, preserving floating indicators.

Open design questions (surfaced by multi-persona plan review)

These are forks an implementer can't safely invent and should be resolved before/during the named units:

  1. Reboot persistenceSMAppService.loginItem can't register an arbitrary gxserver-staged standalone .app (needs Contents/Library/LoginItems/ inside a parent app, and is user-disableable / lost on Sparkle update). LaunchAgent plist vs. daemon-driven bootstrap only vs. relocate the bundle? (Determines whether persistence extends to cold reboot.)
  2. NSStatusItem click vs. menu — a status item can't natively have both a bare click action and an attached menu. Proposed: attach the menu, make "Open Ghostex" the first item, session-focus as menu items (badge has no native sub-region routing).
  3. Dev/prod flavor isolation — single hardcoded bundle id com.madda.ghostex.bar + fixed port 58744 + fixed token path break ghostex-dev isolation. Flavor-aware id/home/port, or accept single-instance-on-prod?
  4. size / hideMenuBarIndicators preference source — the count aggregate carries no size/hide preference (today they come from sidebar settings). Mirror into gxserver, or agent reads a shared settings file?
  5. ghostex:// focus-session route — doesn't exist yet (handler does only terminal/open/edit); current click is an in-process HostEvent unavailable cross-process. Define the URL contract, or defer per-session focus to follow-up (app-focus only in v1)?
  6. Floating-indicator protocol splitSetSessionStatusIndicators bundles floating + menu-bar fields, and AppDelegate.swift:2161 is a shared click handler; the message must be split so removal doesn't break floating indicators.
  7. Security hardening — token + metadata file mode 0600; resolve the launcher from a fixed bundle-relative path (not daemon-written metadata) and validate before exec (command-injection surface); new endpoint asserts loopback binding; validate inbound ghostex:// (scheme is system-wide); login-item cleanup on uninstall.

Scope notes

  • Standalone/headless macOS gxserver (Homebrew/tarball) has no .app — bootstrap must no-op when the agent bundle is absent.
  • Remote / connection-profile gxserver instances are out of scope; the agent controls only the local daemon.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions