Skip to content

docs: plan — move macOS menu bar into a gxserver-owned agent#39

Draft
gsxdsm wants to merge 4 commits into
maddada:mainfrom
gsxdsm:gxserver-menubar
Draft

docs: plan — move macOS menu bar into a gxserver-owned agent#39
gsxdsm wants to merge 4 commits into
maddada:mainfrom
gsxdsm:gxserver-menubar

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jun 13, 2026

Copy link
Copy Markdown

What

Implementation plan only (no code) for moving the macOS menu bar status item out of Ghostex.app and into a standalone, gxserver-owned GxserverBar.app (LSUIElement) that controls the daemon (Start / Stop / Restart, Open Logs) and shows the session-status indicator, 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.

Adds: docs/plans/2026-06-12-001-feat-gxserver-menubar-agent-plan.md

Tracking issue: #38

Why

gxserver is the long-lived process that survives the app closing, yet the only menu bar presence today lives in the window app and has no server control. A control surface for the server should live with the server. gxserver (a Node daemon) can't draw an NSStatusItem, so it owns/launches a thin native agent instead.

Planned units

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

Open design questions (from multi-persona plan review)

Forks to settle before implementation — full detail in the plan's Open Questions section:

  1. Reboot persistence (SMAppService can't register a standalone staged .app) — LaunchAgent vs. daemon-driven bootstrap vs. relocate bundle.
  2. NSStatusItem click-vs-menu model (can't have both).
  3. Dev/prod flavor isolation (single bundle id + fixed port/token break ghostex-dev).
  4. size / hideMenuBarIndicators preference source for the agent.
  5. ghostex:// focus-session route (doesn't exist yet) — define or defer per-session focus.
  6. Floating-indicator protocol split so removal doesn't break floating indicators.
  7. Security hardening (0600 token/metadata, fixed launcher path validated before exec, loopback binding, ghostex:// input validation, login-item cleanup).

Notes

  • Plan/docs only — draft PR for review of approach before any code lands.
  • Standalone/headless macOS gxserver (Homebrew/tarball) has no .app; bootstrap no-ops when the bundle is absent.
  • Remote / connection-profile gxserver instances are out of scope; the agent controls only the local daemon.

🤖 Generated with Claude Code

gsxdsm and others added 2 commits June 13, 2026 00:08
Implementation plan for relocating the menu bar status item out of
Ghostex.app into a standalone gxserver-owned GxserverBar.app that
controls the daemon (start/stop/restart/logs) and shows session-status
counts. Includes multi-persona review findings and open design questions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Persistence: daemon-driven bootstrap, drop SMAppService/login item
- Interaction: menu-first ("Open Ghostex" first item), no badge click
- Restart: wait-for-stopped between stop and start (stop is async)
- Launcher path: fixed bundle-relative + validated, not runtime metadata
- Rendering: preserve available-suppression (available only when all-idle)
- Preference: agent needs hideMenuBar only; menu-bar size is hard-coded

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gsxdsm

gsxdsm commented Jun 13, 2026

Copy link
Copy Markdown
Author

Thanks — all six are real contradictions; fixed in the latest commit (plan only, still draft).

  1. Reboot persistence — resolved to daemon-driven, no login item. Dropped SMAppService (it can't register a gxserver-staged standalone .app anyway). The agent survives daemon stop/restart and returns the next time gxserver starts; no cold-reboot persistence, matching today's "no menu bar until the app/daemon runs." (KTD4, R6, U3)
  2. Badge rendering — preserved the available-suppression rule (available shown only when attention+working are zero, :124). U1 keeps raw counts; the agent applies suppression at render time; U2 test now expects two badges for {1,2,3}. (R3, U1/U2)
  3. Click-vs-menu — rewrote R7/U5 menu-first: attach the NSMenu, first item "Open Ghostex", no badge/sub-region click (:550). (KTD7)
  4. Restart wait — Restart now does Stop → poll until unreachable → Start, reusing the wait-for-stopped pattern (GxserverClient.swift:464), since /api/control/stop returns before shutdown (server.ts:902). (KTD3, R-J)
  5. Path ownership — launcher path is now fixed bundle-relative inside Ghostex.app, validated before exec; removed the runtime-metadata launcher-path recording (protocol/index.ts:233 doesn't carry it anyway). (KTD3, R-D, R-I)
  6. Size preference — corrected: menu-bar indicator is hard-coded small (:159), size is floating-only, so the agent only needs hideMenuBarSessionStatusIndicators. (R-F, U1)

One decision left open for you: dev/prod flavor isolation (R-H) — should ghostex-dev and the installed app each get their own flavor-aware agent (bundle id/home/port), or is single-instance-on-prod acceptable? It needs settling before U2 fixes the bundle id.

gsxdsm and others added 2 commits June 13, 2026 15:39
- Summary/Scope: floating indicators are preserved-with-care (share a
  protocol message per R-A), not "out of scope / if any"
- Reorder KTDs into sequence (KTD5/6/7)
- HLD: open -g runs each start (idempotent), not "first run" only

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Agent uses one fixed bundle id and the standard local endpoint/token;
ghostex-dev does not get a separate menu bar agent. Closes the last
open plan decision (R-H).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gsxdsm

gsxdsm commented Jun 13, 2026

Copy link
Copy Markdown
Author

Resolved the last open decision: dev/prod flavor → single-instance-on-prod. The agent ships one fixed bundle id (com.madda.ghostex.bar) targeting the standard local endpoint/token; ghostex-dev does not get its own menu bar agent (port 58744 is fixed anyway, so dev/prod daemons can't run simultaneously). Updated R-H and Resolved Decisions accordingly.

With that settled, no plan-level decisions remain open — only implementation-time security details (token/metadata 0600, loopback-binding assertion). Ready to move to implementation when you are.

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.

1 participant