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:
- Reboot persistence —
SMAppService.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.)
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).
- 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?
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?
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)?
- Floating-indicator protocol split —
SetSessionStatusIndicators 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.
- 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
Summary
Move the macOS menu bar status item out of the main
Ghostex.appand 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 over127.0.0.1:58744. The oldSessionStatusIndicatorControlleris removed fromGhostex.app; floating / in-window indicators are preserved.A full implementation plan lives in the branch
gxserver-menubaratdocs/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 anNSStatusItem, so it owns/launches a thin native agent rather than rendering the menu bar itself.Implementation units (planned)
{attention, working, available}counts from presentation state; serves/api/ui/statusIndicators+ broadcasts on/api/events.GxserverBar.app(LSUIElement): status item, control menu, HTTP/WS client, badge rendering moved from the old controller.gxserverlauncher.build-ghostex-host.sh(not the headless server package), macOS-only.ghostex://scheme.SessionStatusIndicatorControllerfrom 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:
SMAppService.loginItemcan't register an arbitrary gxserver-staged standalone.app(needsContents/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.)NSStatusItemclick 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).com.madda.ghostex.bar+ fixed port58744+ fixed token path breakghostex-devisolation. Flavor-aware id/home/port, or accept single-instance-on-prod?size/hideMenuBarIndicatorspreference 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?ghostex://focus-session route — doesn't exist yet (handler does onlyterminal/open/edit); current click is an in-processHostEventunavailable cross-process. Define the URL contract, or defer per-session focus to follow-up (app-focus only in v1)?SetSessionStatusIndicatorsbundles floating + menu-bar fields, andAppDelegate.swift:2161is a shared click handler; the message must be split so removal doesn't break floating indicators.ghostex://(scheme is system-wide); login-item cleanup on uninstall.Scope notes
.app— bootstrap must no-op when the agent bundle is absent.🤖 Generated with Claude Code