{"id":50960035,"url":"https://github.com/rbbydotdev/opencode-tray","last_synced_at":"2026-06-18T12:30:47.584Z","repository":{"id":357129564,"uuid":"1234047411","full_name":"rbbydotdev/opencode-tray","owner":"rbbydotdev","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-11T12:04:09.000Z","size":30,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-11T14:09:30.514Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rbbydotdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-09T17:22:12.000Z","updated_at":"2026-05-11T12:04:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rbbydotdev/opencode-tray","commit_stats":null,"previous_names":["rbbydotdev/opencode-tray"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/rbbydotdev/opencode-tray","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rbbydotdev%2Fopencode-tray","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rbbydotdev%2Fopencode-tray/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rbbydotdev%2Fopencode-tray/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rbbydotdev%2Fopencode-tray/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rbbydotdev","download_url":"https://codeload.github.com/rbbydotdev/opencode-tray/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rbbydotdev%2Fopencode-tray/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34491225,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-18T12:30:46.908Z","updated_at":"2026-06-18T12:30:47.577Z","avatar_url":"https://github.com/rbbydotdev.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# OpenCode Tray\n\nA 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.\n\n\u003cimg width=\"330\" alt=\"OpenCode Tray menu\" src=\"docs/tray-menu.png\" /\u003e\n\nOpenCode'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.\n\n## Tutorial\n\nBuild the app bundle and open it:\n\n```bash\nsh scripts/build-app.sh\nopen dist/OpenCodeTray.app\n```\n\nClick the tray icon → **Settings…** → confirm the defaults → **Start Server**. To reach it from your phone, tap **Show Server QR** in the menu and scan.\n\n## How-To\n\n### Run from source\n\n```bash\nswift run OpenCodeTray\n```\n\n### Connect from a phone\n\n1. Keep Hostname set to `0.0.0.0`.\n2. Put the phone on the same network — Tailscale, WireGuard/VPN, or the same Wi-Fi LAN.\n3. Tray → **Show Server QR**.\n\nThe 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`.\n\n### Use password auth\n\n1. Settings → **OpenCode Basic Auth** → tick on.\n2. Set Username and Password.\n3. Use Copy Server URL / Show Server QR as normal; enter the credentials when the browser prompts.\n\nCredentials are never embedded in shared URLs.\n\n### Stop the Mac sleeping while the server runs\n\nSettings → **Keep Awake** → tick **Keep Awake while server is running**.\n\nThe 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.\n\nTwo optional sub-toggles:\n- **Stay awake even with lid closed** — adds a `PreventSystemSleep` assertion. macOS only honors this on AC power without further help (see next section).\n- **Keep Awake on battery** — keep assertions active when running on battery too.\n\n### Override lid-close-on-battery (full caffeinate)\n\nmacOS 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:\n\n1. Settings → **Keep Awake** → **Install…** under \"Helper\".\n2. macOS prompts for Touch ID / admin password (once).\n3. 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.\n4. Tick **Keep Awake on battery**.\n\nFrom 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…**.\n\n### Survive port collisions\n\nIf 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**).\n\n### Open docs / copy URL / copy logs\n\nTray menu items: **Open Server Docs**, **Copy Server URL**, **Copy Local URL**, **Copy Recent Logs**.\n\n## Reference\n\n### Tray menu\n\nStatus block (auto-refreshes on menu open and on network changes):\n- **OpenCode:** Stopped / Starting / Running (pid) / Failed\n- **Server URL:** the address QR/Copy uses\n- **Local URL:** local-only URL (hidden if equal to Server URL)\n- **Keep Awake:** Off / On / On (Lid OK) / On (Lid OK, Battery Forced) / Paused (on battery)\n\nActions:\n- **Start Server / Stop Server**\n- **Open Server Docs**, **Copy Server URL**, **Copy Local URL**, **Show Server QR**, **Copy Recent Logs**\n- **Keep Awake** (quick toggle)\n- **Start at Login** (quick toggle)\n- **Settings…**\n- **Quit**\n\n### Defaults\n\n- Executable: `opencode` (PATH + common install locations)\n- Hostname: `0.0.0.0`\n- Port: `4096` (auto-shifts to next free)\n- Working directory: home directory\n- mDNS: off, domain `opencode.local`\n- CORS origins: none\n- OpenCode Basic Auth: off\n- Start server when tray opens: on\n- Start at Login: off\n- Keep Awake: off\n- Helper: not installed\n\n### URL preference order\n\nFor QR and Copy Server URL when Hostname is `0.0.0.0`:\n\n1. Tailscale IP (`100.64.0.0/10` or interface name containing \"tailscale\")\n2. WireGuard/VPN IP (private IPv4 on `utun*` / `wg*`)\n3. LAN private IPv4 (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`)\n4. Fallback to `localhost`\n\n### Executable resolution\n\n`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.\n\n### Helper paths\n\n- Binary: `/usr/local/libexec/opencode-tray-helper`\n- Plist: `/Library/LaunchDaemons/ai.opencode.tray.helper.plist`\n- Mach service: `ai.opencode.tray.helper`\n\nInstall/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`.\n\n### Persistence\n\n- App settings: `~/Library/Preferences/ai.opencode.tray.plist`\n- Launch-at-login: `~/Library/LaunchAgents/ai.opencode.tray.plist`\n\n## Explanation\n\nThe 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.\n\nURL 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.\n\nKeep 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.\n\nThe 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 \u003c0|1\u003e`.\n\n---\n\n\u003cimg width=\"560\" alt=\"OpenCode Tray settings\" src=\"docs/settings.png\" /\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frbbydotdev%2Fopencode-tray","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frbbydotdev%2Fopencode-tray","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frbbydotdev%2Fopencode-tray/lists"}