feat: macOS menu-bar tray (af-tray) with launchd auto-start#725
Open
AbirAbbas wants to merge 4 commits into
Open
feat: macOS menu-bar tray (af-tray) with launchd auto-start#725AbirAbbas wants to merge 4 commits into
AbirAbbas wants to merge 4 commits into
Conversation
Adds `af-tray`, a small separate binary that puts an AgentField icon in
the macOS menu bar. It polls the control plane's public /health endpoint
to show running/stopped status, opens the dashboard, and drives the
control-plane lifecycle (start/stop/restart/start-at-login) via launchd —
the tray is a controller of an OS service, not a supervisor.
Design/isolation:
- Separate binary so the systray/CGO dependency never enters the server
binary (verified: ./cmd/af has no systray/dbus deps). Containers/headless
hosts (Railway/ECS/EC2) never build, install, or run it.
- Platform-neutral logic (health, launchd plist generation, launchctl arg
construction, atomic writes) lives in shared.go with contract tests that
run on the Linux CI; the systray loop and launchctl exec calls are
darwin-tagged. Non-darwin builds compile a graceful no-op stub.
- launchd semantics encode the intended behaviour: server uses
KeepAlive={SuccessfulExit:false} so a graceful Stop sticks but a crash
restarts; tray uses KeepAlive={Crashed:true} so Quit / no-GUI exit never
crash-loops; server runs with --open=false under launchd.
- install is idempotent/convergent (bootstrap + kickstart -k) so a
`curl | bash` update force-restarts a stale tray/server onto the new
binary with nothing manual.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On macOS (production channel) the installer now fetches the separate `agentfield-tray-<arch>` binary and delegates .app-bundle + launchd setup to `af-tray install` (mirroring how the skill install is delegated to the binary). Best-effort throughout: a missing/failed tray never fails the overall install, and it is never fetched on Linux/headless/container hosts. Opt out with --no-tray or TRAY_MODE=none. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds goreleaser build targets for agentfield-tray-darwin-{amd64,arm64}
(CGO on, macOS only) and includes them in the release matrix on the
macos-14 runner so the Cocoa/CGO link happens on a real macOS host. The
binaries are named to match the existing agentfield-* asset convention,
so the flatten/checksum/upload steps pick them up automatically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
…helpers Splits the CLI dispatch out of main() into a testable run([]string) int and adds unit tests for it plus the non-darwin stubs and the shared helpers (serverBinaryPath fallbacks, writeFileAtomic error paths). This lifts the control-plane patch coverage on the af-tray files from 66% to ~85%, above the 80% patch-coverage gate. The darwin-only files are not measured on the Linux coverage run; the systray loop / launchctl calls remain covered only by the on-device build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
af-tray, a Docker-Desktop-style menu-bar companion for AgentField on macOS. It puts an AgentField icon in the menu bar that shows control-plane status, opens the dashboard, and manages the control plane's lifecycle — with hands-off install/update via the existingcurl | bashflow.This is a macOS-first slice by design. Windows/Linux trays are deliberately out of scope here (see below); nothing in this PR paints us into a mac-only corner.
What it does
/healthendpoint every 5s; icon switches between active/inactive.launchctl. Not a supervisor.install.shfetches the tray on macOS and delegates.app-bundle + launchd setup toaf-tray install, which is idempotent/convergent (bootstrap+kickstart -k) so acurl | bashupdate force-restarts a stale tray/server onto the new binary with nothing manual.Design decisions
af-tray; the control-plane binary has zero systray/dbus deps (verified viago list -deps ./cmd/af). Containers/headless hosts (Railway/ECS/EC2) never build, fetch, or run the tray — and ifaf-tray runis somehow invoked without a GUI, it exits 0 (no crash-loop).KeepAlive={SuccessfulExit:false}(a graceful Stop sticks; a crash restarts) +--open=falseunder launchd. Tray:KeepAlive={Crashed:true}(Quit / no-GUI exit never crash-loops)..appusesLSUIElement(menu-bar-only, no dock icon).shared.gowith contract-based tests that run on the Linux CI; only the systray loop +launchctlexec calls are darwin-tagged. Non-darwin builds compile a graceful no-op stub.Testing
cmd/af-tray/shared_test.go, 12 tests) — run on Linux CI under-tags sqlite_fts5. Contract-based (behavior, not implementation): health = HTTP 200 only; plist contracts (--open=false,SuccessfulExit=false,Crashed=true,LSUIElement,RunAtLoad);kickstart -konly when killing;AGENTFIELD_PORThandling; atomic write replace + no temp leftovers; path layout;serverBinaryPathpreference.go build ./...; CGO-offcompile-matrixfor linux/darwin/windowsagentfield-server(confirms the new go.mod dep doesn't break the required cross-compiles);golangci-lintclean;go vet ./...; server-binary systray isolation; and the darwin sources type-check clean against a faithful systray stub (a real macOS build is the final validation of the Cocoa/CGO link).Scope / follow-ups
.exe(or WSLg); Linux tray = StatusNotifier + systemd-user/XDG autostart. Both are incremental — the systray loop itself is largely shareable..icoassets are included in the tray assets dir for that upcoming work; they aren't embedded/wired yet.🤖 Generated with Claude Code