Syrtis
Windows port of TokenBar — the menu-bar/tray AI coding-agent token-usage monitor. Same Rust parsing core, a native WinUI 3 shell.
Syrtis is the name this port ships under; the macOS build is still called
TokenBar. Solution and namespace identifiers remain TokenBar.* — they are
internal, and the Velopack package id Nyanako.Syrtis is deliberately
independent of both so a rename cannot move an installed app.
Status:
v1.3.0is the latest release. See the release list for history,docs/release-velopack.mdfor the current packaging contract, anddocs/release.mdfor the earlier portable one.
Features
- Eight lenses in the flyout: Overview, Quota, Models, Monthly, Daily, Hourly, Stats, Agents — each hideable except Overview and Models. Ctrl+1..9 select a tab (Overview, then the client tabs in row order), Ctrl+[ / Ctrl+] step through them, and holding Ctrl shows the numbers.
- Quota lens — overview strip, heatmap, per-client window and history cards, and an Agent-limits view, backed by a usage-attribution subsystem that resolves which client consumed which window.
- Tray icon, seven display modes (today/total tokens, today/total cost, tokens-per-minute, quota-remaining, hidden), drawn directly into the icon with bars/ring/popsicle gauge styles.
- 3D contribution graph — a real-time D3D11-rendered activity grid with orbit/pan/zoom.
- In-app updates — a Sparkle-style update dialog backed by Velopack.
- Traditional Chinese UI, alongside English.
Install
Download the installer for your architecture from the releases page. Four channels, permanent and non-interchangeable once installed:
| Channel | .NET runtime |
|---|---|
win-x64 |
Bundled (Full) |
win-x64-lite |
Acquired at install time |
win-arm64 |
Bundled (Full) |
win-arm64-lite |
Acquired at install time |
Full is the safe default; Lite is smaller to download but only wins on a machine that already has
.NET 10 installed. See docs/lite-distribution.md for the full
channel/package-identity contract.
Package managers
Scoop, from this repository's own bucket:
scoop bucket add syrtis https://github.com/Nanako0129/Syrtis-Windows
scoop install syrtis
Scoop manages the app itself, so scoop update syrtis is how it updates, and the bucket is
refreshed after each release by scripts/update-scoop-manifest.sh.
If the in-app Check for updates button reports a failure, that is expected here and nothing is
broken: it talks to the installer's own update feed, which does not manage this copy.
Only the Full channel is published this way — Lite relies on the installer to fetch .NET 10, and a Scoop install never runs that installer. Your usage data lives outside the Scoop directory, so it survives updates and is left behind on uninstall.
winget is not published yet. Manifests are prepared in
packaging/winget/ and validate against the real tool; what remains is the
submission pull request to microsoft/winget-pkgs.
Supported providers
The app reads quota for seven agent CLIs, using credentials already written to disk by a login you have performed. That is usually the provider's own CLI — with one exception worth knowing before you go looking for a missing card:
| Provider | Mechanism |
|---|---|
| Claude | OAuth via the OS keychain / Claude Code login credentials |
| Codex | OAuth via ~/.codex/auth.json ($CODEX_HOME honored) |
| GitHub Copilot | OAuth via opencode's auth.json — signing into the GitHub Copilot CLI alone is not enough, and the card is omitted rather than shown as an error |
| OpenCode Go | The opencode-go API key in opencode's auth.json (read-only, never copied; sent only to opencode.ai). The quota appears on the OpenCode tab |
| Grok | OAuth via ~/.grok/auth.json |
| Grok Bot | The Grok Bot app's sign-in (%APPDATA%\Grok Bot\sand-secrets.json, Electron safeStorage via DPAPI). Syrtis checks that file each refresh to see whether Grok Bot is signed in, and unlocks and uses the sign-in only after you allow it, with Allow on the card or the "Use Grok Bot's sign-in" switch in Settings → Agent limits; Windows does not ask separately. Sent only to api2.cursor.sh. Turning the switch off takes effect from the next refresh; a refresh already under way may finish. When Grok Bot is signed out, the Cursor IDE sign-in (%APPDATA%\Cursor\User\globalStorage\state.vscdb, read-only) is used instead, if there is one, and sent only to cursor.com; while Grok Bot is signed in it is not used, switch off included. Syrtis keeps no copy of either, only a one-way fingerprint to tell accounts apart |
| Kiro | Bearer token from the Kiro IDE sign-in, expected at ~/.aws/sso/cache/kiro-auth-token.json (not yet observed on a Windows device; read-only, never refreshed or copied; sent only to codewhisperer.us-east-1.amazonaws.com). Kiro CLI's own store is not read on Windows |
| Antigravity | OAuth or local IDE credentials, whichever the installed client itself uses |
Claude Code usage in WSL
Claude Code running inside WSL writes transcripts to the Linux home, not to
%USERPROFILE%\.claude, so Syrtis may show 0 tokens today while quota still
updates. Point the scanner at the WSL directories with TOKSCALE_EXTRA_DIRS
(comma-separated client:path entries; this replaces any value you already have).
In Windows PowerShell, replace <distro> with your WSL distribution name
(see wsl --list --quiet) and <user> with your Linux username
(see wsl -d <distro> -e whoami). First check that the path is reachable:
Test-Path '\\wsl.localhost\<distro>\home\<user>\.claude\projects'
If it prints False, check the names. Some older Windows 10 / WSL setups expose
the share only as \\wsl$\<distro>\...; use that form in both commands. Then set
the variable:
[Environment]:https://github.com/nanako0129/syrtis-windows/blob/HEAD/:SetEnvironmentVariable('TOKSCALE_EXTRA_DIRS',
'claude:\\wsl.localhost\<distro>\home\<user>\.claude\projects,claude:\\wsl.localhost\<distro>\home\<user>\.claude\transcripts', 'User')
Fully quit Syrtis from the tray and relaunch it. To undo, run:
[Environment]:https://github.com/nanako0129/syrtis-windows/blob/HEAD/:SetEnvironmentVariable('TOKSCALE_EXTRA_DIRS', $null, 'User')
Then restart Syrtis.
Architecture
| Layer | Path | Notes |
|---|---|---|
| Rust core | crates/tb_core_ffi + vendor/tokscale-core |
Public shared engine pinned as a Git submodule; consumer provenance is recorded in vendor/ENGINE.md. C ABI, JSON envelope, built as cdylib for P/Invoke |
| C ABI contract | include/ctb.h |
16 entry points, {"ok":true,"data":…} / {"ok":false,"err":…} — except tb_probe, which keeps its Phase 0 {"ok":true,"messages":N} shape |
| Interop | src/TokenBar.Interop |
net10.0, platform-neutral — P/Invoke facade + envelope decode |
| Logic | src/TokenBar.Core |
net10.0, platform-neutral — C# port of the macOS TokenBarCore |
| Shell | src/TokenBar.App |
WinUI 3, unpackaged. Windows-only build (not in the slnx): dotnet build src/TokenBar.App/TokenBar.App.csproj -c Release -p:Platform=x64 -p:RuntimeIdentifier=win-x64 |
| Windows native packaging | src/Directory.Build.targets |
The sole Platform/RuntimeIdentifier → explicit Rust-target mapping. BuildTbNative produces the target-specific tb_core_ffi.dll; missing/conflicting tuples, wrong PE machines, and stale output/publish bytes fail fast. |
Windows native packaging is opt-in for TokenBar.App, TokenBar.Smoke, and
TokenBar.Core.Tests. The supported tuples are x64/win-x64 →
x86_64-pc-windows-msvc and ARM64/win-arm64 →
aarch64-pc-windows-msvc; Windows managed builds never select a default
target/release DLL.
Build
Prereqs: Rust 1.96.1, .NET SDK 10.0.301, PowerShell 7.0+; on Windows the MSVC toolchain.
The root global.json pins that SDK with rollForward: disable, so a machine carrying only a
later 10.0.x cannot run dotnet from the repository root at all. tools/sdkfree/
is a deliberate escape hatch for that.
On macOS and Linux the Windows native targets are inert, but the tests still load the release
dylib, which src/Directory.Build.targets copies only if it is already there — so build it first,
exactly as scripts/check.sh does:
cargo build --release --locked
cd tools/sdkfree && dotnet test ../../src/TokenBar.Core.Tests/TokenBar.Core.Tests.csproj
On Windows it is not sufficient on its own. TokenBar.Core.Tests opts into native packaging,
so the same project also needs -p:Platform=x64 and the BuildTbNative step from the Windows
commands below — without them ValidateTbNativeConfiguration rejects the default AnyCPU/empty-RID
tuple before any test runs. The escape hatch lifts the SDK pin; it does not remove the native
prerequisites.
Use it for tests only. Builds and packaging go through the root pin, which is what makes them reproducible.
git submodule update --init --recursive
# macOS (inner loop — no Windows needed)
scripts/check.sh
# Windows x64: restore the locked graph, build the explicit native source, then run the x64 gates
dotnet restore src/TokenBar.slnx --locked-mode
dotnet restore src/TokenBar.App/TokenBar.App.csproj --locked-mode
.\scripts\dev.ps1
dotnet msbuild src/TokenBar.Smoke/TokenBar.Smoke.csproj -t:BuildTbNative -p:Configuration=Release -p:Platform=x64 -p:RuntimeIdentifier=win-x64 -p:TbNativeCargoLocked=true -p:RestoreLockedMode=true
dotnet build src/TokenBar.Core.Tests/TokenBar.Core.Tests.csproj -c Release -p:Platform=x64 --no-restore
dotnet test src/TokenBar.Core.Tests/TokenBar.Core.Tests.csproj -c Release -p:Platform=x64 --no-build --no-restore
dotnet build src/TokenBar.App/TokenBar.App.csproj -c Release -p:Platform=x64 -p:RuntimeIdentifier=win-x64 --no-restore
dotnet build src/TokenBar.Smoke/TokenBar.Smoke.csproj -c Release -p:Platform=x64 --no-restore
dotnet run --project src/TokenBar.Smoke -c Release -p:Platform=x64 --no-build --no-restore
dotnet publish src/TokenBar.Smoke/TokenBar.Smoke.csproj -c Release -r win-x64 --self-contained -o out/smoke-win-x64 --no-restore
ARM64 is cross-build/package validation only on an x64 runner:
dotnet restore src/TokenBar.App/TokenBar.App.csproj --locked-mode
dotnet restore src/TokenBar.Smoke/TokenBar.Smoke.csproj --locked-mode
dotnet msbuild src/TokenBar.Smoke/TokenBar.Smoke.csproj -t:BuildTbNative -p:Configuration=Release -p:Platform=ARM64 -p:RuntimeIdentifier=win-arm64 -p:TbNativeCargoLocked=true -p:RestoreLockedMode=true
dotnet build src/TokenBar.App/TokenBar.App.csproj -c Release -p:Platform=ARM64 -p:RuntimeIdentifier=win-arm64 --no-restore
dotnet publish src/TokenBar.Smoke/TokenBar.Smoke.csproj -c Release -r win-arm64 --self-contained -o out/smoke-win-arm64 --no-restore
The unsigned portable App package command performs the locked restore, native build, publish, ZIP creation, and structure/version/PE/hash checks. Use a clean Git checkout and a new empty output root on a Windows host or CI runner:
.\scripts\build-app-artifact.ps1 -Rid win-x64 -OutputRoot "$env:RUNNER_TEMP\tokenbar-x64"
.\scripts\build-app-artifact.ps1 -Rid win-arm64 -OutputRoot "$env:RUNNER_TEMP\tokenbar-arm64"
The cross-language CrossCheck stays platform-neutral and has no Platform/RID:
TZ=Asia/Taipei dotnet run \
--project src/TokenBar.CrossCheck \
-c Release -- \
crosscheck/fixtures \
crosscheck/csharp-out
CI publishes only short-retention Smoke test-harness artifacts and sanitized
App evidence/checksums. Hosted CI never uploads App ZIP/EXE/DLL files. The
hosted runners perform structure, version, PE, and hash checks; they do not
claim an interactive WinUI startup gate. cargo fmt is not currently a
repository CI gate.
See docs/verification-history.md for the
historical gate records (which specific runs proved which claim, and when).
Branch naming and merge conventions are in CONTRIBUTING.md.
Known limitations
Windows does not yet have macOS parity on: Discord Rich Presence, per-client tray items, Simplified Chinese, menu-bar font colour, a flat-heatmap chart mode, agent brand icons, an Agent-limits sparkline/chart layout, a Stats attribution-breakdown card, and multi-account Claude.
Credits
Shared parsing engine from tokscale-core, originally derived from tokscale by junhoyeo. Original menu-bar concept by handlecusion's tokcat. The spinning cat traces back to RunCat by Takuto Nakamura, and the quota-pace cards reference CodexBar by Peter Steinberger.
License
MIT — see LICENSE.
The shipped binary also contains third-party code, chiefly the vendored
tokscale-core engine compiled into tb_core_ffi.dll. Their notices are in
THIRD-PARTY-NOTICES.md, which packaging copies into
the application payload so it ships beside Syrtis.App.exe in the portable ZIP,
the .nupkg, and the installer. That file is explicit about being incomplete,
about what it does not yet cover — most of the dependency graph by count — and
about the generator that would close it.