Skip to content

a native macOS app for tokenmaxx, with a menu bar item - #31

Merged
DexterStorey merged 4 commits into
RubricLab:mainfrom
pedroapfilho:gui-app
Oct 1, 2026
Merged

DexterStorey merged 4 commits into
RubricLab:mainfrom
pedroapfilho:gui-app

Conversation

@pedroapfilho

@pedroapfilho pedroapfilho commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

This is a proposal, so tell me whether it's worth doing. It adds a native macOS app for tokenmaxx, built with GPUI (Zed's UI framework) and the GPUI Kit components. The app does everything the terminal dashboard does, and it can also sit in the menu bar.

It's a large diff (+22k lines), but about 19k of that is gui/Cargo.lock and the JSON test fixtures. The real changes are the daemon changes below and the Rust app in gui/.

What the app does

  • Accounts: accounts are listed per provider, fullest first, with usage bars, reset countdowns, plan, and health. The active account is marked.
    • Each row has a Use button. A … menu holds the rest: sign in again, spend a Codex reset credit, spill into extra usage at the threshold, and sign out.
    • "Add account" either signs in with a subscription in your terminal, or takes an API key.
  • Analytics: a throughput chart and a pricing breakdown by provider, cost type, and model, over 1h / 5h / 24h / 7d / 31d.
  • Settings:
    • Per provider: routing, auto-switch, switch threshold, cooldown, and which limits to show.
    • Routing for pi.
    • Appearance: auto, light, or dark. Auto follows macOS and changes live.
    • Show tokenmaxx in: menu bar and Dock, menu bar only, or Dock only. It changes live.
    • The terminal used for sign-in: Ghostty, iTerm, WezTerm, kitty, Alacritty, or Terminal.
    • A command-line tool status, covered below.
  • Menu bar item: an icon only. Its menu lists each provider's accounts with their 5h and 7d usage, and clicking one switches to it. It also shows tokens per hour and the last 24h value, plus Refresh, Open, Settings, and Quit.

How it fits with the CLI

The daemon stays the one source of truth. The app is a thin client of the same unix socket (manager.sock) and the same ~/.tokenmaxx state.

  • Standalone app. tokenmaxx.app contains a bun build --compile copy of the CLI, so it works without Bun or npm installed.
  • One version drives the daemon. If a tokenmaxx on the user's PATH is at least as new as the app's copy, the app starts the daemon and runs sign-in through that one. Otherwise it uses its own copy, and Settings offers an Update button, which runs bun add -g tokenmaxx@<version> or the npm equivalent. It also offers to put the bundled copy on the PATH. The app never replaces a daemon newer than the version it would start.
  • Sign-in stays in a terminal. Subscription sign-in needs a terminal (a TTY), so the app opens the user's terminal running tokenmaxx login <cli> and picks up the new account on its next poll.

Changes to the existing TypeScript

  • New daemon methods: routing/read, routing/set (codex, claude, pi), account/addApiKey, and version/latest. The routing logic moved out of cli.ts into src/routing.ts.
  • CLI and dashboard use them. The CLI and the TUI now make these changes through the daemon, and the TUI no longer returns a routing action to the CLI.
  • --compile support. The daemon now starts correctly from a compiled binary; Bun.main is under /$bunfs/ there. A new compiledBinary flag makes the update hints point to the app instead of bun add -g.
  • tokenmaxx --version. New; the app uses it to compare versions.
  • daemon start now replaces a running daemon of a different version, the same way the other commands already do.
  • Bug fix in healInstalledConfigs. It used to re-apply routing to every routed config using the starting daemon's own port. A daemon started with a different TOKENMAXX_HOME and TOKENMAXX_PROXY_PORT would then point the user's real codex and claude configs at itself. It now only rewrites configs already routed to its own port. A regression test covers this.

Trying it

bun run gui:dev     # compiles the CLI, then runs the app against it
bun run gui:bundle  # gui/target/tokenmaxx.app (ad-hoc signed)

This needs Rust stable and Xcode's command-line tools. gui/ isn't part of CI; the existing workflow still runs only on Ubuntu.

When testing, isolate the client configs as well: CODEX_HOME, CLAUDE_CONFIG_DIR, and PI_CODING_AGENT_DIR, not only TOKENMAXX_HOME. Otherwise a routing toggle changes your real configs.

Tested

  • bun run check: 93 tests pass, including the new daemon-method tests and the heal regression test.
  • cargo test: 12 tests pass, covering the formatting ported from tui/format.ts, the app parsing every TUI fixture, the menu command ids, and version ordering.
  • The dev app and the bundled .app against a test daemon:
    • An auto-switch toggle showed up in tokenmaxx status.
    • Light and dark switched live.
    • Menu bar only mode removed the Dock icon live.
    • The status item existed.

Not tested yet:

  • The menu bar menu's contents and switching from it.
  • Terminal sign-in, adding an API key, sign-out, reset credits, and the command-line tool install and update.
  • Rows with real accounts.
  • Dock only mode, and closing then reopening the window.

Open questions

  1. Do we want this at all? It's a second UI in Rust on top of a TypeScript daemon.
  2. Distribution. Anyone else can only get the app by building it from source. Shipping it properly needs a macOS CI job that builds, signs with a Developer ID, and notarizes a .dmg/.zip for GitHub Releases, plus a Homebrew cask. That needs a Rubric Labs Apple Developer account.
  3. gpui-kit pins a weekly GPUI snapshot (gpui-pre =0.3.6), so expect small API changes whenever we bump it.

@DexterStorey

Copy link
Copy Markdown
Contributor

Merging this — thanks Pedro, the app is a big step. main had moved a lot since you opened it (#25, #28, #29, #32, and Claude banked resets), so the branch got a merge plus a few changes on top:

  • Routing through the daemon covers every provider. routing/set accepts xai, routing/read reports grok's CLI and route, and account/addApiKey registers xAI keys. Before, a non-openai provider fell through to the Anthropic registration.
  • healInstalledConfigs keeps your fix (it doesn't touch a config another instance owns) on top of grok accounts #29's per-provider loop and settings shows true routing state: live refresh + Bun.TOML parse fallback #25's TOML fallback. It skips a provider only when its base URL is positively another port, so an unparseable config still heals as before.
  • The app now parses grok accounts and the renamed reset methods. Provider gained Xai, and ProviderFlags/RoutingTarget follow. Provider::ALL still lists only codex and Claude, so the UI is unchanged and grok simply isn't shown in the app yet. The reset calls moved to account/resetCredits and account/consumeReset, which serve both codex and Claude now, and there's a new ResetCode::Unavailable. Without the Xai variant, every dashboard read would have failed to parse once the daemon started reporting an xai provider state.
  • The GUI fixtures were regenerated from the current TypeScript scenarios, so parses_every_dashboard_scenario covers xai accounts and Claude reset counts.
  • A bad xAI key now reads as "rejected". xAI answers 400 "Incorrect API key provided", which used to surface as "xAI API returned HTTP 400".

Verified:

  • TypeScript: typecheck, biome and 150 tests on Bun 1.2.20 and 1.4.2.
  • Every new socket method against a sandboxed daemon.
  • The Settings routing toggle round-trips through the daemon in the TUI.
  • A deploy to a live install: configs byte-identical after the heal, and the dashboard renders.
  • The Rust app: builds on stable 1.98 (the Homebrew 1.85 is too old for tray-icon/muda, which need 1.90). All 12 cargo tests pass, and the model parses a live daemon's dashboard/analytics and routing/read payloads, xai provider state included.

@DexterStorey
DexterStorey merged commit 739e8bb into RubricLab:main Oct 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants