A small macOS menu bar app that runs opencode serve as a managed child process and surfaces the HTTP API to the rest of your devices.
OpenCode's CLI exposes an HTTP API through opencode serve. Running that in a terminal works, but the server dies with the terminal, port collisions silently fail, your Mac happily sleeps mid-request, and when you bring up Tailscale after the fact the URL you copied is wrong. OpenCode Tray treats opencode serve as a first-class menu-bar service: it starts at login, keeps the Mac awake only while the server is running, falls forward to the next free port on collisions, and updates the displayed URL whenever a network interface comes or goes. A QR code in the menu makes phone-onboarding one scan.
Build the app bundle and open it:
sh scripts/build-app.sh
open dist/OpenCodeTray.appClick the tray icon → Settings… → confirm the defaults → Start Server. To reach it from your phone, tap Show Server QR in the menu and scan.
swift run OpenCodeTray- Keep Hostname set to
0.0.0.0. - Put the phone on the same network — Tailscale, WireGuard/VPN, or the same Wi-Fi LAN.
- Tray → Show Server QR.
The QR uses the first reachable address it finds, preferring Tailscale → WireGuard → LAN. Bringing up a VPN after starting the server is fine — opencode serve binds to 0.0.0.0, so the kernel accepts connections on new interfaces as they appear, and the tray's URL display refreshes live via NWPathMonitor.
- Settings → OpenCode Basic Auth → tick on.
- Set Username and Password.
- Use Copy Server URL / Show Server QR as normal; enter the credentials when the browser prompts.
Credentials are never embedded in shared URLs.
Settings → Keep Awake → tick Keep Awake while server is running.
The app holds an IOKit PreventUserIdleSystemSleep assertion only while opencode is up. The moment the server stops — manually, on crash, on quit — the assertion is released.
Two optional sub-toggles:
- Stay awake even with lid closed — adds a
PreventSystemSleepassertion. macOS only honors this on AC power without further help (see next section). - Keep Awake on battery — keep assertions active when running on battery too.
macOS enforces lid-close-on-battery at the system level; the standard PreventSystemSleep assertion is ignored on battery. The workaround is pmset -b disablesleep 1, which needs root. OpenCode Tray ships a tiny privileged helper to do this on demand:
- Settings → Keep Awake → Install… under "Helper".
- macOS prompts for Touch ID / admin password (once).
- The installer copies an ad-hoc-signed helper to
/usr/local/libexec/opencode-tray-helper, drops a LaunchDaemon at/Library/LaunchDaemons/ai.opencode.tray.helper.plist, and bootstraps it. - Tick Keep Awake on battery.
From then on, when opencode is running on battery the helper sets pmset -b disablesleep 1; on server stop / app quit it sets it back to 0. To remove: Settings → Uninstall….
If the configured port is already taken when you click Start Server, the tray scans forward up to 50 ports, picks the first free one, and uses that. The actual port is reflected everywhere — tray status, Server URL, Copy URL, QR, Open Docs — until the server stops. A log line records the substitution (Tray → Copy Recent Logs).
Tray menu items: Open Server Docs, Copy Server URL, Copy Local URL, Copy Recent Logs.
Status block (auto-refreshes on menu open and on network changes):
- OpenCode: Stopped / Starting / Running (pid) / Failed
- Server URL: the address QR/Copy uses
- Local URL: local-only URL (hidden if equal to Server URL)
- Keep Awake: Off / On / On (Lid OK) / On (Lid OK, Battery Forced) / Paused (on battery)
Actions:
- Start Server / Stop Server
- Open Server Docs, Copy Server URL, Copy Local URL, Show Server QR, Copy Recent Logs
- Keep Awake (quick toggle)
- Start at Login (quick toggle)
- Settings…
- Quit
- Executable:
opencode(PATH + common install locations) - Hostname:
0.0.0.0 - Port:
4096(auto-shifts to next free) - Working directory: home directory
- mDNS: off, domain
opencode.local - CORS origins: none
- OpenCode Basic Auth: off
- Start server when tray opens: on
- Start at Login: off
- Keep Awake: off
- Helper: not installed
For QR and Copy Server URL when Hostname is 0.0.0.0:
- Tailscale IP (
100.64.0.0/10or interface name containing "tailscale") - WireGuard/VPN IP (private IPv4 on
utun*/wg*) - LAN private IPv4 (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16) - Fallback to
localhost
opencode is resolved in order against: the Settings value, PATH, ~/.opencode/bin, ~/.bun/bin, /opt/homebrew/bin, /usr/local/bin. Settings has Detect and Browse… to pin a specific binary. Node/Bun shim wrappers are unwrapped to the native binary where possible.
- Binary:
/usr/local/libexec/opencode-tray-helper - Plist:
/Library/LaunchDaemons/ai.opencode.tray.helper.plist - Mach service:
ai.opencode.tray.helper
Install/uninstall is driven from Settings. Under the hood the Swift app writes a one-shot install script to $TMPDIR and runs it through osascript ... with administrator privileges.
- App settings:
~/Library/Preferences/ai.opencode.tray.plist - Launch-at-login:
~/Library/LaunchAgents/ai.opencode.tray.plist
The tray runs opencode serve as a child Process, captures stdout + stderr into a 12 KB rolling buffer, and emits state changes via a single callback. Restart is stop + start; saving Settings triggers a restart automatically if the server is running.
URL resolution is recomputed on every menu open and on every NWPathMonitor update, so the displayed URL tracks the current network without needing a server restart. Because opencode serve binds to 0.0.0.0, the kernel accepts traffic on all current and future IPv4 interfaces — a Tailscale or VPN tunnel coming up later requires no re-bind.
Keep Awake is a thin wrapper around IOPMAssertionCreateWithName. The setting is gated on the server's running state, so assertions are released the moment opencode exits.
The battery-override path uses a privileged helper rather than asking for sudo on every toggle. The helper is ad-hoc signed (no paid Apple Developer cert needed), installed once via osascript ... with administrator privileges, and runs as a LaunchDaemon. Communication is NSXPCConnection on the ai.opencode.tray.helper mach service. The helper's only privileged operation is /usr/bin/pmset -b disablesleep <0|1>.

