toki

Development

Requirements

Providers are optional — Toki shows what is installed and authenticated. Claude Code plus claude-swap covers multi-account Claude workflows; Codex, Pi, Sarvam Code, OpenCode, Copilot CLI, Gemini CLI, and Grok CLI are each picked up when present.

Building

swift build
swift run Toki
scripts/build-app.sh          # bundle to .build/Toki.app
scripts/install-app.sh        # bundle and install to ~/Applications

Before shipping a local change:

swift build
swift test
scripts/build-app.sh
plutil -p .build/Toki.app/Contents/Info.plist

Widgets

The TokiWidgets.appex extension is embedded under Contents/PlugIns by both build scripts. Toki and the extension are separate sandboxed processes, so they share a snapshot (widget-data.json) through one of two channels, chosen at package time by TokiWidgetDataMode in each Info.plist:

Because the public DMG is ad-hoc signed (not notarized), released builds ship in local mode. The one caveat that carries to another Mac is Gatekeeper: the appex inherits the same “unidentified developer” friction as the app, so the quarantine flag must be cleared from the whole bundle for the extension to load and register — xattr -dr com.apple.quarantine (note -r, recursive) does this; clearing only the top level leaves the appex quarantined and the widget absent from the gallery. Test on a second Mac or a fresh user account before relying on it, since WidgetKit registration for ad-hoc extensions is not something Apple formally supports.

When a widget looks wrong:

Releases and update channels

Every release starts from a tag push; .github/workflows/release.yml builds the DMG and publishes the GitHub release. The tag decides the channel:

Set appVersion in Sources/Toki/Config/Constants.swift to match the tag (without the v) before tagging — the packaging script, the in-app updater’s version check, and the DMG verification all read it.

To graduate a beta to production:

  1. Tag v2.5.0-beta.1 (with appVersion = "2.5.0-beta.1") and test on the Beta channel. Iterate with -beta.2, -beta.3, … as needed.
  2. When it’s ready, set appVersion = "2.5.0" and tag v2.5.0. That build ships to everyone: stable users see it as a normal update, and beta users are offered it too, because 2.5.0 outranks 2.5.0-beta.N — so testers land back on the production build without touching their channel setting.

Version ordering is semver-aware (2.4.3 < 2.5.0-beta.1 < 2.5.0-beta.2 < 2.5.0); the logic and its tests live in UpdateChecker.compareVersions and Tests/TokiTests/UpdateChannelTests.swift.

Concurrency checking

CI builds with stricter concurrency than a plain swift build, and the difference has broken this project’s CI more than once — a pure static helper on a @MainActor type needs nonisolated, which only the stricter mode catches. Reproduce it locally before pushing:

swift build --build-tests -Xswiftc -strict-concurrency=complete

Conventions

swift-format is not vendored here. Keep changes compiler-clean, locally scoped, and consistent with the surrounding SwiftUI/AppKit style.

Comments should explain what the code cannot: why a timeout is the value it is, why a coordinate space is not flipped, why an ordering is deliberate. Comments that restate the line below them are noise and get deleted.

Troubleshooting