A native macOS menu-bar app (Swift) that
- keeps the Mac awake (
IOPMAssertion) while the Python polling bridgeopencode-talk-bridgeis running, and - starts/stops/monitors that bridge as a launchd user agent, showing
its live state (
starting/polling/working/opencode_down/error/stopped) in the menu-bar icon.
The app is a thin controller — credentials and the polling logic live in
the bridge itself. Status comes from the bridge's status.json (single
source of truth).
Agent-only app (LSUIElement), so no Dock icon, no in-app menu bar.
Install the bridge from PyPI (no git clone needed):
uv tool install opencode-talk-bridge
# or: pipx install opencode-talk-bridgeBoth place the console script at ~/.local/bin/opencode-talk-bridge, which
is the app's default binary path.
The release .dmg is ad-hoc signed, not notarized (the project has no
paid Apple Developer Program account). Gatekeeper blocks the first launch
of an unnotarized app downloaded from the internet. Either of the following
clears it once:
- Right-click
TalkBridgeMenubar.app→ Öffnen → confirm Öffnen in the dialog. (Easiest path.) - Or in Terminal:
xattr -dr com.apple.quarantine /Applications/TalkBridgeMenubar.app
The source is on GitHub and the build pipeline is in
.github/workflows/release.yml — no
hidden binaries.
On first launch, if the bridge binary can't be found, a setup window appears automatically:
- Confirm the bridge binary path (defaults to
~/.local/bin/opencode-talk-bridge) and the config directory (defaults to~/.config/opencode-talk-bridge, where.env,status.json, andbridge.sqlite3live). - The window shows live checks: an executable bridge binary (required),
the config dir (created on demand), and a
.envrecommendation (credentials live there; the app never touches them). - Optionally Konfig-Ordner anlegen and .env öffnen, then plist installieren to write the launchd agent, and Fertig.
The setup window is self-healing: it reappears on the next launch as long as the bridge binary can't be resolved, so a wrong or moved path never leaves the app silently broken. You can reopen it any time from the menu's Einrichtung… item, and the same checks appear in Einstellungen → Bridge.
- launchd label:
com.leiverkus.opencode-talk-bridge - The app generates the launchd plist itself (the PyPI wheel doesn't
ship one) from the configured binary path, config dir, and log paths,
then writes it to
~/Library/LaunchAgents/. The plist'sProgramArgumentsrun<binary> --env-file <configdir>/.envwithWorkingDirectory = <configdir>, so the bridge finds its.env,status.json, and DB there. - Start/stop go through
launchctl bootstrap gui/<uid> <plist>andlaunchctl bootout gui/<uid>/<label>. A second start on an already-loaded service kicks it withlaunchctl kickstart -k. - Status comes from
<configdir>/status.json. The reader usesDispatchSourceFS events for instant updates plus a 2 s timer fallback (atomic temp+rename writes invalidate a single FD watch).
The launchd-service variant was chosen over a child process so the bridge
survives an app restart and is independently observable via launchctl print gui/$(id -u)/com.leiverkus.opencode-talk-bridge.
Requires Swift 5.10+ and macOS 13+.
swift build
swift run TalkBridgeMenubarThe app appears as a status item in the menu bar.
swift testUI is excluded — the 41 unit tests cover status decoding (all six states),
plist generation, the launchd-target string, the sleep-assertion lifecycle
(against a protocol mock), the wake-coordinator state machine, bridge-setup
validation (executable-bit aware), the status reader (initial read, atomic
replace, and retarget to a new path), and the service-state poller
(dedupe plus the forced re-publish that re-enables the menu after a failed
action).
E2EIntegrationTests drives the real BridgeService/SleepAssertion
against the uv/pipx-installed bridge and the live launchd domain. It is
skipped unless RUN_E2E=1, so CI and normal runs never touch launchd:
uv tool install opencode-talk-bridge
RUN_E2E=1 swift test --filter E2EIntegrationTestsIt installs the generated plist, bootstraps the service, confirms it is
loaded, reads status.json, bootouts, and verifies the IOPM assertion
shows up in pmset -g assertions and is released — cleaning up after
itself.
Scripts/build-app.sh # → dist/TalkBridgeMenubar.app
Scripts/sign.sh # codesign; ad-hoc by default
Scripts/make-dmg.sh # → dist/TalkBridgeMenubar.dmg
Scripts/notarize.sh # no-op unless APPLE_NOTARY_* are set.github/workflows/release.yml runs the same chain on a v* tag push and
uploads the DMG as a release asset.
The pipeline is notarization-ready without restructuring: sign.sh honours
SIGN_IDENTITY (default - = ad-hoc) and notarize.sh is a no-op until the
notary credentials exist. To ship a notarized build, add a Developer ID to
the runner keychain and set these repo secrets, then re-tag:
SIGN_IDENTITY "Developer ID Application: … (TEAMID)"
APPLE_NOTARY_APPLE_ID Apple ID e-mail
APPLE_NOTARY_TEAM_ID 10-char team id
APPLE_NOTARY_PASSWORD app-specific password
Locally you can do the same: SIGN_IDENTITY="…" Scripts/sign.sh && Scripts/make-dmg.sh && APPLE_NOTARY_APPLE_ID=… APPLE_NOTARY_TEAM_ID=… APPLE_NOTARY_PASSWORD=… Scripts/notarize.sh.
MIT. © 2026 Patrick Leiverkus.