Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,301 changes: 2,183 additions & 118 deletions Cargo.lock

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,42 @@ InternalName = "ClaudeCodeUsageMonitor"
Comments = "https://codezeno.com.au"
LegalCopyright = "Copyright (C) 2026 Code Zeno Pty Ltd"

[[bin]]
name = "claude-code-usage-monitor"
path = "src/main.rs"

[[bin]]
name = "ollama-login-helper"
path = "src/ollama_login_helper.rs"
required-features = ["ollama-login-webview"]

[dependencies]
ureq = { version = "2", default-features = false, features = ["native-tls", "json", "proxy-from-env"] }
native-tls = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dirs = "6"

[features]
# Pulls in wry + tao for an embedded WebView2 login window. Off by default
# because the native deps add ~6 MB to the release binary. Enable with:
# cargo build --release --features ollama-login-webview
# Then right-click the tray icon → "Log in to Ollama…" to capture fresh
# cookies via CoreWebView2.CookieManager.GetCookiesAsync().
default = []
ollama-login-webview = ["dep:wry", "dep:tao"]

[dependencies.wry]
version = "0.55"
optional = true
# On Windows: uses WebView2 via Edge automatically (no extra feature needed).
# On Linux: requires `linux-body` (webkit2gtk v2_40).
# We only target Windows for the tray, so default features suffice.

[dependencies.tao]
version = "0.35"
optional = true

[dependencies.windows]
version = "0.58"
features = [
Expand Down
4 changes: 4 additions & 0 deletions src/localization/dutch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "Open Antigravity en meld je opnieuw aan. Ververs of herstart de app daarna.",
codex_window_title: "Codex-gebruiksmonitor",
antigravity_window_title: "Antigravity-gebruiksmonitor",
ollama_model: "Ollama",
ollama_window_title: "Ollama-gebruiksmonitor",
ollama_token_expired_title: "Ollama-authenticatiefout",
ollama_token_expired_body: "Gebruik Models -> 'Inloggen bij Ollama...' of zet OLLAMA_CLOUD_SESSION, en vernieuw daarna.",
second_suffix: "s",
};
4 changes: 4 additions & 0 deletions src/localization/english.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "Open Antigravity and sign in again. After that, refresh or restart this app.",
codex_window_title: "Codex Usage Monitor",
antigravity_window_title: "Antigravity Usage Monitor",
ollama_model: "Ollama",
ollama_window_title: "Ollama Usage Monitor",
ollama_token_expired_title: "Ollama Auth Error",
ollama_token_expired_body: "Use Models -> 'Log in to Ollama...' or set OLLAMA_CLOUD_SESSION, then refresh.",
second_suffix: "s",
};
4 changes: 4 additions & 0 deletions src/localization/french.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "Ouvrez Antigravity et reconnectez-vous. Ensuite, actualisez ou redemarrez cette application.",
codex_window_title: "Moniteur d'utilisation Codex",
antigravity_window_title: "Moniteur d'utilisation Antigravity",
ollama_model: "Ollama",
ollama_window_title: "Moniteur d'utilisation Ollama",
ollama_token_expired_title: "Erreur d'authentification Ollama",
ollama_token_expired_body: "Utilisez Modeles -> 'Se connecter a Ollama...' ou definissez OLLAMA_CLOUD_SESSION, puis actualisez.",
second_suffix: "s",
};
4 changes: 4 additions & 0 deletions src/localization/german.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "Offnen Sie Antigravity und melden Sie sich erneut an. Aktualisieren oder starten Sie diese App anschliessend neu.",
codex_window_title: "Codex-Nutzungsmonitor",
antigravity_window_title: "Antigravity-Nutzungsmonitor",
ollama_model: "Ollama",
ollama_window_title: "Ollama-Nutzungsmonitor",
ollama_token_expired_title: "Ollama-Authentifizierungsfehler",
ollama_token_expired_body: "Nutzen Sie Modelle -> 'Bei Ollama anmelden...' oder setzen Sie OLLAMA_CLOUD_SESSION und aktualisieren Sie.",
second_suffix: "s",
};
4 changes: 4 additions & 0 deletions src/localization/japanese.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "Antigravity を開いて再度サインインしてください。その後、このアプリを更新するか再起動してください。",
codex_window_title: "Codex 使用量モニター",
antigravity_window_title: "Antigravity 使用量モニター",
ollama_model: "Ollama",
ollama_window_title: "Ollama 使用量モニター",
ollama_token_expired_title: "Ollama 認証エラー",
ollama_token_expired_body: "Models -> 'Ollama にログイン...' を使うか OLLAMA_CLOUD_SESSION を設定してから更新してください。",
second_suffix: "秒",
};
4 changes: 4 additions & 0 deletions src/localization/korean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "Antigravity를 열고 다시 로그인하세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요.",
codex_window_title: "Codex 사용량 모니터",
antigravity_window_title: "Antigravity 사용량 모니터",
ollama_model: "Ollama",
ollama_window_title: "Ollama 사용량 모니터",
ollama_token_expired_title: "Ollama 인증 오류",
ollama_token_expired_body: "Models -> 'Ollama에 로그인...'을 사용하거나 OLLAMA_CLOUD_SESSION을 설정한 뒤 새로 고치세요.",
second_suffix: "초",
};
12 changes: 8 additions & 4 deletions src/localization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,14 @@ pub struct Strings {
pub codex_token_expired_title: &'static str,
pub codex_token_expired_body: &'static str,
pub antigravity_token_expired_title: &'static str,
pub antigravity_token_expired_body: &'static str,
pub codex_window_title: &'static str,
pub antigravity_window_title: &'static str,
}
pub antigravity_token_expired_body: &'static str,
pub codex_window_title: &'static str,
pub antigravity_window_title: &'static str,
pub ollama_model: &'static str,
pub ollama_window_title: &'static str,
pub ollama_token_expired_title: &'static str,
pub ollama_token_expired_body: &'static str,
}

pub fn resolve_language(language_override: Option<LanguageId>) -> LanguageId {
language_override.unwrap_or_else(detect_system_language)
Expand Down
6 changes: 5 additions & 1 deletion src/localization/portuguese_brazil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_title: "Erro de Autenticação do Antigravity",
antigravity_token_expired_body: "Abra o Antigravity e entre novamente. Depois disso, atualize ou reinicie este aplicativo.",
codex_window_title: "Monitor de uso do Codex",
antigravity_window_title: "Monitor de uso do Antigravity",
antigravity_window_title: "Monitor de uso de Antigravity",
ollama_model: "Ollama",
ollama_window_title: "Monitor de uso do Ollama",
ollama_token_expired_title: "Erro de Autenticacao do Ollama",
ollama_token_expired_body: "Use Models -> 'Entrar no Ollama...' ou defina OLLAMA_CLOUD_SESSION e atualize.",
};
4 changes: 4 additions & 0 deletions src/localization/russian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "Откройте Antigravity и войдите снова. После этого обновите или перезапустите приложение.",
codex_window_title: "Монитор использования Codex",
antigravity_window_title: "Монитор использования Antigravity",
ollama_model: "Ollama",
ollama_window_title: "Монитор использования Ollama",
ollama_token_expired_title: "Ошибка авторизации Ollama",
ollama_token_expired_body: "Используйте Models -> 'Войти в Ollama...' или задайте OLLAMA_CLOUD_SESSION, затем обновите.",
};
4 changes: 4 additions & 0 deletions src/localization/simplified_chinese.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "请打开 Antigravity 并重新登录。完成后,请刷新或重新启动此应用程序。",
codex_window_title: "Codex 使用量监控",
antigravity_window_title: "Antigravity 使用量监控",
ollama_model: "Ollama",
ollama_window_title: "Ollama 使用量监控",
ollama_token_expired_title: "Ollama 验证错误",
ollama_token_expired_body: "请使用 Models -> '登录 Ollama...' 或设置 OLLAMA_CLOUD_SESSION,然后刷新。",
second_suffix: "秒",
};
4 changes: 4 additions & 0 deletions src/localization/spanish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "Abre Antigravity e inicia sesion otra vez. Despues, actualiza o reinicia esta aplicacion.",
codex_window_title: "Monitor de uso de Codex",
antigravity_window_title: "Monitor de uso de Antigravity",
ollama_model: "Ollama",
ollama_window_title: "Monitor de uso de Ollama",
ollama_token_expired_title: "Error de autenticacion de Ollama",
ollama_token_expired_body: "Usa Models -> 'Iniciar sesion en Ollama...' o define OLLAMA_CLOUD_SESSION y luego actualiza.",
second_suffix: "s",
};
4 changes: 4 additions & 0 deletions src/localization/traditional_chinese.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ pub(super) const STRINGS: Strings = Strings {
antigravity_token_expired_body: "請開啟 Antigravity 並重新登入。完成後,請重新整理或重新啟動此應用程式。",
codex_window_title: "Codex 使用量監控",
antigravity_window_title: "Antigravity 使用量監控",
ollama_model: "Ollama",
ollama_window_title: "Ollama 使用量監控",
ollama_token_expired_title: "Ollama 驗證錯誤",
ollama_token_expired_body: "請使用 Models -> '登入 Ollama...' 或設定 OLLAMA_CLOUD_SESSION,然後重新整理。",
second_suffix: "秒",
};
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod diagnose;
mod localization;
mod models;
mod native_interop;
#[cfg(feature = "ollama-login-webview")]
mod ollama_login;
mod poller;
mod theme;
mod tray_icon;
Expand Down
4 changes: 4 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ pub struct AppUsageData {
pub claude_code: Option<UsageData>,
pub codex: Option<UsageData>,
pub antigravity: Option<UsageData>,
/// Ollama Cloud plan-tier usage. Polled from ollama.com/settings
/// using the user's browser-captured session cookie. `None` when the
/// cookie is missing or the poll fails (e.g. session expired).
pub ollama: Option<UsageData>,
}
85 changes: 85 additions & 0 deletions src/ollama_login.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Ollama Cloud login via embedded WebView2 (wry).
//!
//! # Why this exists
//!
//! ollama.com's auth is WorkOS-hosted. A raw `Cookie:` header from any
//! non-Chrome process fails the session-binding check → 303 to
//! `signin.ollama.com`. There is no public Ollama Cloud usage API. The
//! proven workaround on Windows is to log in inside a real browser context
//! and read the cookies that browser got from WorkOS.
//!
//! # How it runs
//!
//! The tray spawns a **separate helper process** (`ollama-login-helper.exe`)
//! that opens a WebView2 window pointing at `https://ollama.com/signin`.
//! The user completes WorkOS auth in that window. When the helper detects
//! the user has landed on `/settings` (or new cookies have been set), it
//! writes them to
//! `%LOCALAPPDATA%\ClaudeCodeUsageMonitor\ollama_session_cookie.txt` and
//! exits. The tray picks up the cookies on the next poll cycle.
//!
//! A separate process is required because tao's `EventLoop::run()` on
//! Windows MUST own the Win32 message pump. Running it inside the tray's
//! `DispatchMessageW` handler doesn't work — the event loop starves and
//! WebView2 never paints or navigates.
//!
//! # Feature gate
//!
//! Feature-gated behind `ollama-login-webview` so the default build doesn't
//! pull wry/tao. Enable with:
//! `cargo build --release --features ollama-login-webview`

use crate::diagnose;

/// Spawn the standalone `ollama-login-helper.exe` as a separate process.
/// This is the ONLY way to make wry/tao work on Windows: the event loop
/// must own the Win32 message pump, which is impossible when running
/// inside the tray's `DispatchMessageW` handler.
///
/// The helper binary opens a WebView2 window, the user completes auth,
/// the helper captures cookies, writes them to the cookie file, and exits.
/// The tray picks up the cookies on the next poll cycle.
///
/// This function returns immediately (non-blocking). The tray stays
/// responsive while the helper runs.
pub fn run_login_blocking() {
diagnose::log("ollama-login-webview: spawning ollama-login-helper.exe");

let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.to_path_buf()));

let helper_exe = exe_dir
.as_ref()
.map(|d| d.join("ollama-login-helper.exe"))
.unwrap_or_else(|| std::path::PathBuf::from("ollama-login-helper.exe"));

diagnose::log(format!(
"ollama-login-webview: helper path = {}",
helper_exe.display()
));

if !helper_exe.exists() {
diagnose::log(format!(
"ollama-login-webview: helper exe NOT FOUND at {}",
helper_exe.display()
));
return;
}

match std::process::Command::new(&helper_exe)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(child) => {
diagnose::log(format!(
"ollama-login-webview: helper spawned PID={}",
child.id()
));
}
Err(e) => {
diagnose::log_error("ollama-login-webview: failed to spawn helper", e);
}
}
}
Loading