diff --git a/README.md b/README.md index 8a62fce5..3ff59113 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ In the default theme, left-click a provider tray icon to show or hide the widget | Cursor | Sign in to Cursor, then enable it in **Providers**. The local session is detected automatically. | | Grok Build | Run `grok login` in the Grok Build CLI, then enable Grok in **Providers**. The signed-in session is detected automatically. | | GitHub Copilot | Sign in with the Copilot CLI (`copilot`, then `/login`) or the GitHub CLI (`gh auth login`), then enable Copilot in **Providers**. The stored login is detected automatically. | +| Mistral (Vibe + Studio) | Set `MISTRAL_API_KEY` or store a Mistral API key in Windows Credential Manager, then enable Mistral in **Providers**. Create the key in Studio under **API keys** at console.mistral.ai. | For OpenCode Go, set `OPENCODE_GO_WORKSPACE_ID` and `OPENCODE_GO_AUTH_COOKIE`, or create `%APPDATA%\opencode-go\config.json`: @@ -90,6 +91,8 @@ Grok Build usage comes from the session the CLI stores in `%USERPROFILE%\.grok\a GitHub Copilot usage comes from `api.github.com/copilot_internal/user`, the endpoint Copilot's own editors and CLI read. The monitor uses the first GitHub login that endpoint accepts, in this order: the `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN` environment variable, the Copilot CLI login, then the GitHub CLI login, both read from Windows Credential Manager. Copilot spends one premium-request pool per calendar month, so the monitor shows it on the long-window row and leaves the short-window row empty. Metered chat and completion allowances, as on Copilot Free, are available to themes through `copilot.limits.chat` and `copilot.limits.completions`. +Mistral usage (Vibe CLI and Studio/La Plateforme) comes from `api.mistral.ai` with a Bearer API key, read from `MISTRAL_API_KEY` or from the `mistral:vibe-api-key` entry in Windows Credential Manager — the same key the Vibe CLI uses. The short-window row reads the `x-ratelimit-*` headers returned by `GET /v1/models`; the long-window row shows the calendar-month spend from `GET /v1/billing/usage` (in EUR) against the `monthly_budget` from `GET /v1/billing/subscription`, resetting at midnight UTC on the 1st, and is labelled `30d`. Billing routes that the key cannot read (403/404) are skipped without failing the poll. + ## Data and privacy The monitor reads local sign-in credentials for enabled providers and sends usage requests directly to their official services. It has no backend service, collects no telemetry, and does not upload credentials or project files. diff --git a/src/app_settings.rs b/src/app_settings.rs index 472e5d20..18aaef00 100644 --- a/src/app_settings.rs +++ b/src/app_settings.rs @@ -66,6 +66,8 @@ pub struct SettingsFile { show_grok: bool, #[serde(default)] show_copilot: bool, + #[serde(default)] + show_mistral: bool, #[serde(default = "default_true")] pub custom_theme_enabled: bool, /// Show what is left of each allowance instead of what has been spent, so @@ -140,6 +142,7 @@ impl Default for SettingsFile { show_cursor: false, show_grok: false, show_copilot: false, + show_mistral: false, custom_theme_enabled: true, usage_countdown: false, use_windows_default_font: true, @@ -239,6 +242,7 @@ impl SettingsFile { ProviderId::Cursor => self.show_cursor, ProviderId::Grok => self.show_grok, ProviderId::Copilot => self.show_copilot, + ProviderId::Mistral => self.show_mistral, } } @@ -287,6 +291,7 @@ impl SettingsFile { ProviderId::Cursor => self.show_cursor = enabled, ProviderId::Grok => self.show_grok = enabled, ProviderId::Copilot => self.show_copilot = enabled, + ProviderId::Mistral => self.show_mistral = enabled, } } @@ -654,6 +659,7 @@ mod tests { ProviderId::Cursor, ProviderId::Grok, ProviderId::Copilot, + ProviderId::Mistral, ])); let json = settings_json(&settings); @@ -664,6 +670,7 @@ mod tests { assert_eq!(json["show_cursor"], true); assert_eq!(json["show_grok"], true); assert_eq!(json["show_copilot"], true); + assert_eq!(json["show_mistral"], true); let decoded = decode_settings(&json.to_string()).unwrap(); assert_eq!(decoded.enabled_providers(), settings.enabled_providers()); diff --git a/src/context_menu.rs b/src/context_menu.rs index bf87f113..05a14ad4 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -424,6 +424,13 @@ pub fn classic_context_menu() -> ContextMenuDocument { provider: Provider::Copilot, }, ), + ContextMenuItem::action( + "provider-mistral", + "Mistral", + Action::ToggleProvider { + provider: Provider::Mistral, + }, + ), ], ); let languages = std::iter::once(ContextMenuItem::action( @@ -730,6 +737,9 @@ mod tests { assert!(serde_json::to_string(&menu) .unwrap() .contains("provider-copilot")); + assert!(serde_json::to_string(&menu) + .unwrap() + .contains("provider-mistral")); assert!(menu.items.iter().any(|item| { item.id == "toggle-widget" && matches!( diff --git a/src/icons/brand-marks.ttf b/src/icons/brand-marks.ttf index f05fc762..36f8867f 100644 Binary files a/src/icons/brand-marks.ttf and b/src/icons/brand-marks.ttf differ diff --git a/src/localization/locales/de.toml b/src/localization/locales/de.toml index 4550efd7..87809488 100644 --- a/src/localization/locales/de.toml +++ b/src/localization/locales/de.toml @@ -20,6 +20,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "Einstellungen" start_with_windows = "Mit Windows starten" language = "Sprache" @@ -58,13 +59,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Grok-Authentifizierungsfehler" grok_token_expired_body = "Führen Sie 'grok login' in einem Terminal aus und folgen Sie den Anmeldeanweisungen. Aktualisieren oder starten Sie diese App anschließend neu." copilot_token_expired_title = "Copilot-Authentifizierungsfehler" +mistral_token_expired_title = "Mistral-Authentifizierungsfehler" copilot_token_expired_body = "Melden Sie sich mit der Copilot-CLI ('copilot', dann /login) oder der GitHub-CLI ('gh auth login') an oder setzen Sie GH_TOKEN. Aktualisieren oder starten Sie diese App anschließend neu." +mistral_token_expired_body = "Legen Sie MISTRAL_API_KEY fest oder speichern Sie einen Mistral-API-Schlüssel im Windows-Anmeldeinformations-Manager (erstellen Sie einen in Studio unter API-Schlüssel auf console.mistral.ai). Aktualisieren oder starten Sie diese App anschließend neu." codex_window_title = "Codex-Nutzungsmonitor" antigravity_window_title = "Antigravity-Nutzungsmonitor" opencode_window_title = "OpenCode-Nutzungsmonitor" cursor_window_title = "Cursor-Nutzungsmonitor" grok_window_title = "Grok-Nutzungsmonitor" copilot_window_title = "Copilot-Nutzungsmonitor" +mistral_window_title = "Mistral-Nutzungsmonitor" [translations] "Use Windows default font" = "Windows-Standardschrift verwenden" @@ -131,6 +135,7 @@ copilot_window_title = "Copilot-Nutzungsmonitor" "Codex" = "Codex" "Collect usage from Anthropic" = "Nutzungsdaten von Anthropic abrufen" "Collect usage from GitHub" = "Nutzungsdaten von GitHub abrufen" +"Collect usage from Mistral" = "Nutzungsdaten von Mistral abrufen" "Collect usage from Google" = "Nutzungsdaten von Google abrufen" "Collect usage from OpenAI" = "Nutzungsdaten von OpenAI abrufen" "Collect usage from xAI" = "Nutzungsdaten von xAI abrufen" @@ -258,6 +263,7 @@ copilot_window_title = "Copilot-Nutzungsmonitor" "Live preview" = "Live-Vorschau" "Medium" = "Medium" "Menu action" = "Menüaktion" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "Mausaktion" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/en.toml b/src/localization/locales/en.toml index 55d05362..b5149dbb 100644 --- a/src/localization/locales/en.toml +++ b/src/localization/locales/en.toml @@ -20,6 +20,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "Settings" start_with_windows = "Start with Windows" language = "Language" @@ -58,13 +59,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Grok Auth Error" grok_token_expired_body = "Run 'grok login' in a terminal and follow the sign-in prompts. After that, refresh or restart this app." copilot_token_expired_title = "Copilot Auth Error" +mistral_token_expired_title = "Mistral Auth Error" copilot_token_expired_body = "Sign in with the Copilot CLI ('copilot', then /login) or the GitHub CLI ('gh auth login'), or set GH_TOKEN. After that, refresh or restart this app." +mistral_token_expired_body = "Set MISTRAL_API_KEY or store a Mistral API key in Windows Credential Manager (create one in Studio under API keys at console.mistral.ai). After that, refresh or restart this app." codex_window_title = "Codex Usage Monitor" antigravity_window_title = "Antigravity Usage Monitor" opencode_window_title = "OpenCode Usage Monitor" cursor_window_title = "Cursor Usage Monitor" grok_window_title = "Grok Usage Monitor" copilot_window_title = "Copilot Usage Monitor" +mistral_window_title = "Mistral Usage Monitor" [translations] "Use Windows default font" = "Use Windows default font" @@ -131,6 +135,7 @@ copilot_window_title = "Copilot Usage Monitor" "Codex" = "Codex" "Collect usage from Anthropic" = "Collect usage from Anthropic" "Collect usage from GitHub" = "Collect usage from GitHub" +"Collect usage from Mistral" = "Collect usage from Mistral" "Collect usage from Google" = "Collect usage from Google" "Collect usage from OpenAI" = "Collect usage from OpenAI" "Collect usage from xAI" = "Collect usage from xAI" @@ -258,6 +263,7 @@ copilot_window_title = "Copilot Usage Monitor" "Live preview" = "Live preview" "Medium" = "Medium" "Menu action" = "Menu action" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "Mouse action" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/es.toml b/src/localization/locales/es.toml index d19c1eb9..eef1e318 100644 --- a/src/localization/locales/es.toml +++ b/src/localization/locales/es.toml @@ -20,6 +20,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "Configuración" start_with_windows = "Iniciar con Windows" language = "Idioma" @@ -58,13 +59,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Error de autenticación de Grok" grok_token_expired_body = "Ejecuta 'grok login' en una terminal y sigue las indicaciones de inicio de sesión. Después, actualiza o reinicia esta aplicación." copilot_token_expired_title = "Error de autenticación de Copilot" +mistral_token_expired_title = "Error de autenticación de Mistral" copilot_token_expired_body = "Inicia sesión con la CLI de Copilot ('copilot' y luego /login) o con la CLI de GitHub ('gh auth login'), o define GH_TOKEN. Después, actualiza o reinicia esta aplicación." +mistral_token_expired_body = "Define MISTRAL_API_KEY o guarda una clave de API de Mistral en el Administrador de credenciales de Windows (créala en Studio, en Claves de API, en console.mistral.ai). Después, actualiza o reinicia esta aplicación." codex_window_title = "Monitor de uso de Codex" antigravity_window_title = "Monitor de uso de Antigravity" opencode_window_title = "Monitor de uso de OpenCode" cursor_window_title = "Monitor de uso de Cursor" grok_window_title = "Monitor de uso de Grok" copilot_window_title = "Monitor de uso de Copilot" +mistral_window_title = "Monitor de uso de Mistral" [translations] "Use Windows default font" = "Usar la fuente predeterminada de Windows" @@ -131,6 +135,7 @@ copilot_window_title = "Monitor de uso de Copilot" "Codex" = "Codex" "Collect usage from Anthropic" = "Recopilar uso de Anthropic" "Collect usage from GitHub" = "Recopilar uso de GitHub" +"Collect usage from Mistral" = "Recopilar uso de Mistral" "Collect usage from Google" = "Recopilar uso de Google" "Collect usage from OpenAI" = "Recopilar uso de OpenAI" "Collect usage from xAI" = "Recopilar uso de xAI" @@ -258,6 +263,7 @@ copilot_window_title = "Monitor de uso de Copilot" "Live preview" = "Vista previa en vivo" "Medium" = "Medium" "Menu action" = "Acción de menú" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "Acción del ratón" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/fr.toml b/src/localization/locales/fr.toml index 828af6f6..8b4a9bb5 100644 --- a/src/localization/locales/fr.toml +++ b/src/localization/locales/fr.toml @@ -20,6 +20,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "Paramètres" start_with_windows = "Démarrer avec Windows" language = "Langue" @@ -58,13 +59,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Erreur d'authentification Grok" grok_token_expired_body = "Exécutez 'grok login' dans un terminal et suivez les instructions de connexion. Ensuite, actualisez ou redémarrez cette application." copilot_token_expired_title = "Erreur d'authentification Copilot" +mistral_token_expired_title = "Erreur d'authentification Mistral" copilot_token_expired_body = "Connectez-vous avec la CLI Copilot ('copilot', puis /login) ou la CLI GitHub ('gh auth login'), ou définissez GH_TOKEN. Ensuite, actualisez ou redémarrez cette application." +mistral_token_expired_body = "Définissez MISTRAL_API_KEY ou enregistrez une clé API Mistral dans le Gestionnaire d'informations d'identification Windows (créez-en une dans Studio › Clés API sur console.mistral.ai). Ensuite, actualisez ou redémarrez cette application." codex_window_title = "Moniteur d'utilisation Codex" antigravity_window_title = "Moniteur d'utilisation Antigravity" opencode_window_title = "Moniteur d'utilisation OpenCode" cursor_window_title = "Moniteur d'utilisation Cursor" grok_window_title = "Moniteur d'utilisation Grok" copilot_window_title = "Moniteur d'utilisation Copilot" +mistral_window_title = "Moniteur d'utilisation Mistral" [translations] "Use Windows default font" = "Utiliser la police par défaut de Windows" @@ -131,6 +135,7 @@ copilot_window_title = "Moniteur d'utilisation Copilot" "Codex" = "Codex" "Collect usage from Anthropic" = "Collecter l'utilisation depuis Anthropic" "Collect usage from GitHub" = "Collecter l'utilisation depuis GitHub" +"Collect usage from Mistral" = "Collecter l'utilisation depuis Mistral" "Collect usage from Google" = "Collecter l'utilisation depuis Google" "Collect usage from OpenAI" = "Collecter l'utilisation depuis OpenAI" "Collect usage from xAI" = "Collecter l'utilisation depuis xAI" @@ -258,6 +263,7 @@ copilot_window_title = "Moniteur d'utilisation Copilot" "Live preview" = "Aperçu en direct" "Medium" = "Medium" "Menu action" = "Action de menu" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "Action de la souris" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/ja.toml b/src/localization/locales/ja.toml index 86bb6dd0..7d5ac7e3 100644 --- a/src/localization/locales/ja.toml +++ b/src/localization/locales/ja.toml @@ -22,6 +22,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "設定" start_with_windows = "Windows と同時に開始" language = "言語" @@ -60,13 +61,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Grok 認証エラー" grok_token_expired_body = "ターミナルで 'grok login' を実行し、サインインの案内に従ってください。その後、このアプリを更新または再起動してください。" copilot_token_expired_title = "Copilot 認証エラー" +mistral_token_expired_title = "Mistral 認証エラー" copilot_token_expired_body = "Copilot CLI('copilot' を実行して /login)または GitHub CLI('gh auth login')でサインインするか、GH_TOKEN を設定してください。その後、このアプリを更新または再起動してください。" +mistral_token_expired_body = "MISTRAL_API_KEY を設定するか、Mistral API キーを Windows の資格情報マネージャーに保存してください(console.mistral.ai の Studio › API キーで作成できます)。その後、このアプリを更新または再起動してください。" codex_window_title = "Codex 使用量モニター" antigravity_window_title = "Antigravity 使用量モニター" opencode_window_title = "OpenCode 使用量モニター" cursor_window_title = "Cursor 使用量モニター" grok_window_title = "Grok 使用量モニター" copilot_window_title = "Copilot 使用量モニター" +mistral_window_title = "Mistral 使用量モニター" [translations] "Use Windows default font" = "Windows の既定のフォントを使用" @@ -133,6 +137,7 @@ copilot_window_title = "Copilot 使用量モニター" "Codex" = "Codex" "Collect usage from Anthropic" = "Anthropic から使用量を取得" "Collect usage from GitHub" = "GitHub から使用量を取得" +"Collect usage from Mistral" = "Mistral から使用量を取得" "Collect usage from Google" = "Google から使用量を取得" "Collect usage from OpenAI" = "OpenAI から使用量を取得" "Collect usage from xAI" = "xAI から使用量を取得" @@ -260,6 +265,7 @@ copilot_window_title = "Copilot 使用量モニター" "Live preview" = "ライブプレビュー" "Medium" = "Medium" "Menu action" = "メニュー操作" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "マウス操作" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/ko.toml b/src/localization/locales/ko.toml index 03f641dc..7900cd32 100644 --- a/src/localization/locales/ko.toml +++ b/src/localization/locales/ko.toml @@ -22,6 +22,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "설정" start_with_windows = "Windows 시작 시 자동 실행" language = "언어" @@ -60,13 +61,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Grok 인증 오류" grok_token_expired_body = "터미널에서 'grok login'을 실행하고 로그인 안내를 따르세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요." copilot_token_expired_title = "Copilot 인증 오류" +mistral_token_expired_title = "Mistral 인증 오류" copilot_token_expired_body = "Copilot CLI('copilot' 실행 후 /login) 또는 GitHub CLI('gh auth login')로 로그인하거나 GH_TOKEN을 설정하세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요." +mistral_token_expired_body = "MISTRAL_API_KEY를 설정하거나 Mistral API 키를 Windows 자격 증명 관리자에 저장하세요(console.mistral.ai의 Studio › API 키에서 만들 수 있음). 그런 다음 이 앱을 새로 고치거나 다시 시작하세요." codex_window_title = "Codex 사용량 모니터" antigravity_window_title = "Antigravity 사용량 모니터" opencode_window_title = "OpenCode 사용량 모니터" cursor_window_title = "Cursor 사용량 모니터" grok_window_title = "Grok 사용량 모니터" copilot_window_title = "Copilot 사용량 모니터" +mistral_window_title = "Mistral 사용량 모니터" [translations] "Use Windows default font" = "Windows 기본 글꼴 사용" @@ -133,6 +137,7 @@ copilot_window_title = "Copilot 사용량 모니터" "Codex" = "Codex" "Collect usage from Anthropic" = "Anthropic에서 사용량 수집" "Collect usage from GitHub" = "GitHub에서 사용량 수집" +"Collect usage from Mistral" = "Mistral에서 사용량 수집" "Collect usage from Google" = "Google에서 사용량 수집" "Collect usage from OpenAI" = "OpenAI에서 사용량 수집" "Collect usage from xAI" = "xAI에서 사용량 수집" @@ -260,6 +265,7 @@ copilot_window_title = "Copilot 사용량 모니터" "Live preview" = "실시간 미리보기" "Medium" = "Medium" "Menu action" = "메뉴 동작" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "마우스 동작" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/nl.toml b/src/localization/locales/nl.toml index 0b049a1f..4e9e3d03 100644 --- a/src/localization/locales/nl.toml +++ b/src/localization/locales/nl.toml @@ -20,6 +20,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "Instellingen" start_with_windows = "Opstarten met Windows" language = "Taal" @@ -58,13 +59,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Grok-authenticatiefout" grok_token_expired_body = "Voer 'grok login' uit in een terminal en volg de aanmeldstappen. Ververs of herstart de app daarna." copilot_token_expired_title = "Copilot-authenticatiefout" +mistral_token_expired_title = "Mistral-authenticatiefout" copilot_token_expired_body = "Meld je aan met de Copilot-CLI ('copilot', daarna /login) of de GitHub-CLI ('gh auth login'), of stel GH_TOKEN in. Ververs of herstart de app daarna." +mistral_token_expired_body = "Stel MISTRAL_API_KEY in of bewaar een Mistral-API-sleutel in Windows Credential Manager (maak er een aan in Studio › API-sleutels op console.mistral.ai). Ververs of herstart de app daarna." codex_window_title = "Codex-gebruiksmonitor" antigravity_window_title = "Antigravity-gebruiksmonitor" opencode_window_title = "OpenCode-gebruiksmonitor" cursor_window_title = "Cursor-gebruiksmonitor" grok_window_title = "Grok-gebruiksmonitor" copilot_window_title = "Copilot-gebruiksmonitor" +mistral_window_title = "Mistral-gebruiksmonitor" [translations] "Use Windows default font" = "Standaardlettertype van Windows gebruiken" @@ -131,6 +135,7 @@ copilot_window_title = "Copilot-gebruiksmonitor" "Codex" = "Codex" "Collect usage from Anthropic" = "Gebruiksgegevens ophalen bij Anthropic" "Collect usage from GitHub" = "Gebruiksgegevens ophalen bij GitHub" +"Collect usage from Mistral" = "Gebruiksgegevens ophalen bij Mistral" "Collect usage from Google" = "Gebruiksgegevens ophalen bij Google" "Collect usage from OpenAI" = "Gebruiksgegevens ophalen bij OpenAI" "Collect usage from xAI" = "Gebruiksgegevens ophalen bij xAI" @@ -258,6 +263,7 @@ copilot_window_title = "Copilot-gebruiksmonitor" "Live preview" = "Livevoorbeeld" "Medium" = "Medium" "Menu action" = "Menuactie" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "Muisactie" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/pl.toml b/src/localization/locales/pl.toml index 9195b267..92fbf912 100644 --- a/src/localization/locales/pl.toml +++ b/src/localization/locales/pl.toml @@ -20,6 +20,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "Ustawienia" start_with_windows = "Uruchamiaj z systemem Windows" language = "Język" @@ -58,13 +59,16 @@ cursor_token_expired_body = "Zaloguj się do Cursora (lub ustaw zmienną CURSOR_ grok_token_expired_title = "Błąd uwierzytelniania Grok" grok_token_expired_body = "Uruchom polecenie „grok login” w terminalu i postępuj zgodnie z instrukcjami logowania. Potem odśwież lub uruchom tę aplikację ponownie." copilot_token_expired_title = "Błąd uwierzytelniania Copilot" +mistral_token_expired_title = "Błąd uwierzytelniania Mistral" copilot_token_expired_body = "Zaloguj się przez Copilot CLI („copilot”, a potem /login) lub GitHub CLI („gh auth login”) albo ustaw zmienną GH_TOKEN. Potem odśwież lub uruchom tę aplikację ponownie." +mistral_token_expired_body = "Ustaw zmienną MISTRAL_API_KEY albo zapisz klucz API Mistral w Menedżerze poświadczeń systemu Windows (utwórz go w Studio › Klucze API na console.mistral.ai). Potem odśwież lub uruchom tę aplikację ponownie." codex_window_title = "Monitor zużycia Codex" antigravity_window_title = "Monitor zużycia Antigravity" opencode_window_title = "Monitor zużycia OpenCode" cursor_window_title = "Monitor zużycia Cursor" grok_window_title = "Monitor zużycia Grok" copilot_window_title = "Monitor zużycia Copilot" +mistral_window_title = "Monitor zużycia Mistral" [translations] "Use Windows default font" = "Używaj domyślnej czcionki systemu Windows" @@ -131,6 +135,7 @@ copilot_window_title = "Monitor zużycia Copilot" "Codex" = "Codex" "Collect usage from Anthropic" = "Zbieraj dane o zużyciu z Anthropic" "Collect usage from GitHub" = "Zbieraj dane o zużyciu z GitHuba" +"Collect usage from Mistral" = "Zbieraj dane o zużyciu z Mistrala" "Collect usage from Google" = "Zbieraj dane o zużyciu z Google" "Collect usage from OpenAI" = "Zbieraj dane o zużyciu z OpenAI" "Collect usage from xAI" = "Zbieraj dane o zużyciu z xAI" @@ -258,6 +263,7 @@ copilot_window_title = "Monitor zużycia Copilot" "Live preview" = "Podgląd na żywo" "Medium" = "Średnia" "Menu action" = "Akcja menu" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "Akcja myszy" "Mouse enter" = "Najechanie myszą" diff --git a/src/localization/locales/pt-BR.toml b/src/localization/locales/pt-BR.toml index f372f908..a16186cd 100644 --- a/src/localization/locales/pt-BR.toml +++ b/src/localization/locales/pt-BR.toml @@ -20,6 +20,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "Configurações" start_with_windows = "Iniciar com o Windows" language = "Idioma" @@ -58,13 +59,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Erro de Autenticação do Grok" grok_token_expired_body = "Execute 'grok login' em um terminal e siga as instruções de login. Depois disso, atualize ou reinicie este aplicativo." copilot_token_expired_title = "Erro de Autenticação do Copilot" +mistral_token_expired_title = "Erro de Autenticação do Mistral" copilot_token_expired_body = "Faça login com a CLI do Copilot ('copilot' e depois /login) ou com a CLI do GitHub ('gh auth login'), ou defina GH_TOKEN. Depois disso, atualize ou reinicie este aplicativo." +mistral_token_expired_body = "Defina MISTRAL_API_KEY ou salve uma chave de API do Mistral no Gerenciador de Credenciais do Windows (crie uma no Studio › Chaves de API em console.mistral.ai). Depois disso, atualize ou reinicie este aplicativo." codex_window_title = "Monitor de uso do Codex" antigravity_window_title = "Monitor de uso do Antigravity" opencode_window_title = "Monitor de uso do OpenCode" cursor_window_title = "Monitor de uso do Cursor" grok_window_title = "Monitor de uso do Grok" copilot_window_title = "Monitor de uso do Copilot" +mistral_window_title = "Monitor de uso do Mistral" [translations] "Use Windows default font" = "Usar a fonte padrão do Windows" @@ -131,6 +135,7 @@ copilot_window_title = "Monitor de uso do Copilot" "Codex" = "Codex" "Collect usage from Anthropic" = "Coletar uso da Anthropic" "Collect usage from GitHub" = "Coletar uso do GitHub" +"Collect usage from Mistral" = "Coletar uso do Mistral" "Collect usage from Google" = "Coletar uso do Google" "Collect usage from OpenAI" = "Coletar uso da OpenAI" "Collect usage from xAI" = "Coletar uso da xAI" @@ -258,6 +263,7 @@ copilot_window_title = "Monitor de uso do Copilot" "Live preview" = "Prévia ao vivo" "Medium" = "Medium" "Menu action" = "Ação de menu" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "Ação do mouse" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/ru.toml b/src/localization/locales/ru.toml index 977e961a..ce5597d9 100644 --- a/src/localization/locales/ru.toml +++ b/src/localization/locales/ru.toml @@ -20,6 +20,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "Настройки" start_with_windows = "Запускать вместе с Windows" language = "Язык" @@ -58,13 +59,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Ошибка авторизации Grok" grok_token_expired_body = "Запустите 'grok login' в терминале и следуйте инструкциям для входа. После этого обновите или перезапустите приложение." copilot_token_expired_title = "Ошибка авторизации Copilot" +mistral_token_expired_title = "Ошибка авторизации Mistral" copilot_token_expired_body = "Войдите через Copilot CLI ('copilot', затем /login) или GitHub CLI ('gh auth login') либо задайте GH_TOKEN. После этого обновите или перезапустите приложение." +mistral_token_expired_body = "Задайте MISTRAL_API_KEY или сохраните ключ API Mistral в диспетчере учетных данных Windows (создайте его в Studio › API-ключи на console.mistral.ai). После этого обновите или перезапустите приложение." codex_window_title = "Монитор использования Codex" antigravity_window_title = "Монитор использования Antigravity" opencode_window_title = "Монитор использования OpenCode" cursor_window_title = "Монитор использования Cursor" grok_window_title = "Монитор использования Grok" copilot_window_title = "Монитор использования Copilot" +mistral_window_title = "Монитор использования Mistral" [translations] "Use Windows default font" = "Использовать шрифт Windows по умолчанию" @@ -131,6 +135,7 @@ copilot_window_title = "Монитор использования Copilot" "Codex" = "Codex" "Collect usage from Anthropic" = "Получать данные об использовании Anthropic" "Collect usage from GitHub" = "Получать данные об использовании GitHub" +"Collect usage from Mistral" = "Получать данные об использовании Mistral" "Collect usage from Google" = "Получать данные об использовании Google" "Collect usage from OpenAI" = "Получать данные об использовании OpenAI" "Collect usage from xAI" = "Получать данные об использовании xAI" @@ -258,6 +263,7 @@ copilot_window_title = "Монитор использования Copilot" "Live preview" = "Предпросмотр" "Medium" = "Medium" "Menu action" = "Действие меню" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "Действие мыши" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/th.toml b/src/localization/locales/th.toml index 1de09c42..b57279e1 100644 --- a/src/localization/locales/th.toml +++ b/src/localization/locales/th.toml @@ -22,6 +22,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "การตั้งค่า" start_with_windows = "เริ่มพร้อม Windows" language = "ภาษา" @@ -60,13 +61,16 @@ cursor_token_expired_body = "ลงชื่อเข้าใช้ Cursor (ห grok_token_expired_title = "Grok ยืนยันตัวตนไม่สำเร็จ" grok_token_expired_body = "เปิดเทอร์มินัลแล้วรันคำสั่ง 'grok login' จากนั้นทำตามขั้นตอนการลงชื่อเข้าใช้ เสร็จแล้วให้รีเฟรชหรือเปิดแอปนี้ใหม่" copilot_token_expired_title = "Copilot ยืนยันตัวตนไม่สำเร็จ" +mistral_token_expired_title = "Mistral ยืนยันตัวตนไม่สำเร็จ" copilot_token_expired_body = "ลงชื่อเข้าใช้ด้วย Copilot CLI (รัน 'copilot' แล้วใช้ /login) หรือ GitHub CLI ('gh auth login') หรือกำหนดค่า GH_TOKEN เสร็จแล้วให้รีเฟรชหรือเปิดแอปนี้ใหม่" +mistral_token_expired_body = "ตั้งค่า MISTRAL_API_KEY หรือบันทึกคีย์ API ของ Mistral ในตัวจัดการข้อมูลประจำตัวของ Windows (สร้างได้ที่ Studio › คีย์ API บน console.mistral.ai) จากนั้นรีเฟรชหรือเปิดแอปนี้ใหม่" codex_window_title = "ตัวติดตามการใช้งาน Codex" antigravity_window_title = "ตัวติดตามการใช้งาน Antigravity" opencode_window_title = "ตัวติดตามการใช้งาน OpenCode" cursor_window_title = "ตัวติดตามการใช้งาน Cursor" grok_window_title = "ตัวติดตามการใช้งาน Grok" copilot_window_title = "ตัวติดตามการใช้งาน Copilot" +mistral_window_title = "ตัวติดตามการใช้งาน Mistral" [translations] "Use Windows default font" = "ใช้แบบอักษรเริ่มต้นของ Windows" @@ -133,6 +137,7 @@ copilot_window_title = "ตัวติดตามการใช้งาน C "Codex" = "Codex" "Collect usage from Anthropic" = "ดึงข้อมูลการใช้งานจาก Anthropic" "Collect usage from GitHub" = "ดึงข้อมูลการใช้งานจาก GitHub" +"Collect usage from Mistral" = "ดึงข้อมูลการใช้งานจาก Mistral" "Collect usage from Google" = "ดึงข้อมูลการใช้งานจาก Google" "Collect usage from OpenAI" = "ดึงข้อมูลการใช้งานจาก OpenAI" "Collect usage from xAI" = "ดึงข้อมูลการใช้งานจาก xAI" @@ -260,6 +265,7 @@ copilot_window_title = "ตัวติดตามการใช้งาน C "Live preview" = "ดูตัวอย่างแบบสด" "Medium" = "ปานกลาง" "Menu action" = "การทำงานของเมนู" +"Mistral" = "Mistral" "Monitor" = "จอภาพ" "Mouse action" = "การทำงานของเมาส์" "Mouse enter" = "เมาส์เข้า" diff --git a/src/localization/locales/tr.toml b/src/localization/locales/tr.toml index 6dc7044f..f9f4288d 100644 --- a/src/localization/locales/tr.toml +++ b/src/localization/locales/tr.toml @@ -20,6 +20,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "Ayarlar" start_with_windows = "Windows ile Başlat" language = "Dil" @@ -58,13 +59,16 @@ cursor_token_expired_body = "Cursor'da oturum açın (veya CURSOR_SESSION_TOKEN grok_token_expired_title = "Grok Kimlik Doğrulama Hatası" grok_token_expired_body = "Bir terminalde 'grok login' komutunu çalıştırın ve oturum açma yönergelerini izleyin. Ardından bu uygulamayı yenileyin veya yeniden başlatın." copilot_token_expired_title = "Copilot Kimlik Doğrulama Hatası" +mistral_token_expired_title = "Mistral Kimlik Doğrulama Hatası" copilot_token_expired_body = "Copilot CLI ('copilot', ardından /login) veya GitHub CLI ('gh auth login') ile oturum açın ya da GH_TOKEN değerini ayarlayın. Ardından bu uygulamayı yenileyin veya yeniden başlatın." +mistral_token_expired_body = "MISTRAL_API_KEY değerini ayarlayın veya bir Mistral API anahtarını Windows Kimlik Bilgileri Yöneticisi'ne kaydedin (console.mistral.ai adresindeki Studio › API Anahtarları bölümünden oluşturabilirsiniz). Ardından bu uygulamayı yenileyin veya yeniden başlatın." codex_window_title = "Codex Kullanım İzleyicisi" antigravity_window_title = "Antigravity Kullanım İzleyicisi" opencode_window_title = "OpenCode Kullanım İzleyicisi" cursor_window_title = "Cursor Kullanım İzleyicisi" grok_window_title = "Grok Kullanım İzleyicisi" copilot_window_title = "Copilot Kullanım İzleyicisi" +mistral_window_title = "Mistral Kullanım İzleyicisi" [translations] "Use Windows default font" = "Windows varsayılan yazı tipini kullan" @@ -131,6 +135,7 @@ copilot_window_title = "Copilot Kullanım İzleyicisi" "Codex" = "Codex" "Collect usage from Anthropic" = "Anthropic kullanımını topla" "Collect usage from GitHub" = "GitHub kullanımını topla" +"Collect usage from Mistral" = "Mistral kullanımını topla" "Collect usage from Google" = "Google kullanımını topla" "Collect usage from OpenAI" = "OpenAI kullanımını topla" "Collect usage from xAI" = "xAI kullanımını topla" @@ -258,6 +263,7 @@ copilot_window_title = "Copilot Kullanım İzleyicisi" "Live preview" = "Canlı önizleme" "Medium" = "Medium" "Menu action" = "Menü eylemi" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "Fare eylemi" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/zh-CN.toml b/src/localization/locales/zh-CN.toml index d7fc3421..8b73e637 100644 --- a/src/localization/locales/zh-CN.toml +++ b/src/localization/locales/zh-CN.toml @@ -22,6 +22,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "设置" start_with_windows = "开机时启动" language = "语言" @@ -60,13 +61,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Grok 验证错误" grok_token_expired_body = "请在终端中运行 'grok login',并按照登录提示操作。完成后,请刷新或重新启动此应用程序。" copilot_token_expired_title = "Copilot 验证错误" +mistral_token_expired_title = "Mistral 验证错误" copilot_token_expired_body = "请使用 Copilot CLI(运行 'copilot' 后输入 /login)或 GitHub CLI('gh auth login')登录,或设置 GH_TOKEN。完成后,请刷新或重新启动此应用程序。" +mistral_token_expired_body = "请设置 MISTRAL_API_KEY,或在 Windows 凭据管理器中保存 Mistral API 密钥(可在 console.mistral.ai 的 Studio › API 密钥中创建)。完成后,请刷新或重新启动此应用程序。" codex_window_title = "Codex 使用量监控" antigravity_window_title = "Antigravity 使用量监控" opencode_window_title = "OpenCode Usage Monitor" cursor_window_title = "Cursor 使用量监控" grok_window_title = "Grok 使用量监控" copilot_window_title = "Copilot 使用量监控" +mistral_window_title = "Mistral 使用量监控" [translations] "Use Windows default font" = "使用 Windows 默认字体" @@ -133,6 +137,7 @@ copilot_window_title = "Copilot 使用量监控" "Codex" = "Codex" "Collect usage from Anthropic" = "从 Anthropic 获取使用量" "Collect usage from GitHub" = "从 GitHub 获取使用量" +"Collect usage from Mistral" = "从 Mistral 获取使用量" "Collect usage from Google" = "从 Google 获取使用量" "Collect usage from OpenAI" = "从 OpenAI 获取使用量" "Collect usage from xAI" = "从 xAI 获取使用量" @@ -260,6 +265,7 @@ copilot_window_title = "Copilot 使用量监控" "Live preview" = "实时预览" "Medium" = "Medium" "Menu action" = "菜单操作" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "鼠标操作" "Mouse enter" = "Mouse enter" diff --git a/src/localization/locales/zh-TW.toml b/src/localization/locales/zh-TW.toml index 0c6eb84a..64080a9b 100644 --- a/src/localization/locales/zh-TW.toml +++ b/src/localization/locales/zh-TW.toml @@ -22,6 +22,7 @@ opencode_model = "OpenCode" cursor_model = "Cursor" grok_model = "Grok" copilot_model = "Copilot" +mistral_model = "Mistral" settings = "設定" start_with_windows = "開機時啟動" language = "語言" @@ -60,13 +61,16 @@ cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), th grok_token_expired_title = "Grok 驗證錯誤" grok_token_expired_body = "請在終端機中執行 'grok login',並依照登入提示操作。完成後,請重新整理或重新啟動此應用程式。" copilot_token_expired_title = "Copilot 驗證錯誤" +mistral_token_expired_title = "Mistral 驗證錯誤" copilot_token_expired_body = "請使用 Copilot CLI(執行 'copilot' 後輸入 /login)或 GitHub CLI('gh auth login')登入,或設定 GH_TOKEN。完成後,請重新整理或重新啟動此應用程式。" +mistral_token_expired_body = "請設定 MISTRAL_API_KEY,或在 Windows 認證管理員中儲存 Mistral API 金鑰(可於 console.mistral.ai 的 Studio › API 金鑰建立)。完成後,請重新整理或重新啟動此應用程式。" codex_window_title = "Codex 使用量監控" antigravity_window_title = "Antigravity 使用量監控" opencode_window_title = "OpenCode 使用量監控" cursor_window_title = "Cursor 使用量監控" grok_window_title = "Grok 使用量監控" copilot_window_title = "Copilot 使用量監控" +mistral_window_title = "Mistral 使用量監控" [translations] "Use Windows default font" = "使用 Windows 預設字型" @@ -133,6 +137,7 @@ copilot_window_title = "Copilot 使用量監控" "Codex" = "Codex" "Collect usage from Anthropic" = "從 Anthropic 取得使用量" "Collect usage from GitHub" = "從 GitHub 取得使用量" +"Collect usage from Mistral" = "從 Mistral 取得使用量" "Collect usage from Google" = "從 Google 取得使用量" "Collect usage from OpenAI" = "從 OpenAI 取得使用量" "Collect usage from xAI" = "從 xAI 取得使用量" @@ -260,6 +265,7 @@ copilot_window_title = "Copilot 使用量監控" "Live preview" = "即時預覽" "Medium" = "Medium" "Menu action" = "選單動作" +"Mistral" = "Mistral" "Monitor" = "Monitor" "Mouse action" = "滑鼠動作" "Mouse enter" = "Mouse enter" diff --git a/src/localization/mod.rs b/src/localization/mod.rs index bd23eab4..ad492c7a 100644 --- a/src/localization/mod.rs +++ b/src/localization/mod.rs @@ -100,6 +100,10 @@ impl LanguageId { strings.copilot_token_expired_title, strings.copilot_token_expired_body, ), + ProviderId::Mistral => ( + strings.mistral_token_expired_title, + strings.mistral_token_expired_body, + ), } } @@ -149,6 +153,7 @@ pub struct Strings { pub cursor_model: &'static str, pub grok_model: &'static str, pub copilot_model: &'static str, + pub mistral_model: &'static str, pub settings: &'static str, pub start_with_windows: &'static str, pub language: &'static str, @@ -188,12 +193,15 @@ pub struct Strings { pub grok_token_expired_body: &'static str, pub copilot_token_expired_title: &'static str, pub copilot_token_expired_body: &'static str, + pub mistral_token_expired_title: &'static str, + pub mistral_token_expired_body: &'static str, pub codex_window_title: &'static str, pub antigravity_window_title: &'static str, pub opencode_window_title: &'static str, pub cursor_window_title: &'static str, pub grok_window_title: &'static str, pub copilot_window_title: &'static str, + pub mistral_window_title: &'static str, } pub fn resolve_language(language_override: Option) -> LanguageId { diff --git a/src/poller.rs b/src/poller.rs index 996b94e2..8b5c9059 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -286,6 +286,7 @@ mod codex; mod copilot; mod cursor; mod grok; +mod mistral; mod opencode; mod retry_after; mod windows_credentials; @@ -298,7 +299,7 @@ struct ProviderPoller { credential_watch: fn(bool) -> CredentialWatchSnapshot, } -const PROVIDER_POLLERS: [ProviderPoller; 7] = [ +const PROVIDER_POLLERS: [ProviderPoller; 8] = [ ProviderPoller { id: ProviderId::Claude, poll: claude::poll_claude_code, @@ -334,6 +335,11 @@ const PROVIDER_POLLERS: [ProviderPoller; 7] = [ poll: copilot::poll_copilot, credential_watch: copilot::credential_watch_snapshot, }, + ProviderPoller { + id: ProviderId::Mistral, + poll: mistral::poll_mistral, + credential_watch: mistral::credential_watch_snapshot, + }, ]; fn provider_poller(provider: ProviderId) -> Option<&'static ProviderPoller> { diff --git a/src/poller/mistral.rs b/src/poller/mistral.rs new file mode 100644 index 00000000..5d8eefd8 --- /dev/null +++ b/src/poller/mistral.rs @@ -0,0 +1,528 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use serde::Deserialize; + +use super::windows_credentials; +use super::{build_agent, get_header_i64, PollError}; +use crate::diagnose; +use crate::models::{UsageData, UsageSection}; + +const MISTRAL_MODELS_URL: &str = "https://api.mistral.ai/v1/models"; +const MISTRAL_BILLING_USAGE_URL: &str = "https://api.mistral.ai/v1/billing/usage"; +const MISTRAL_BILLING_SUBSCRIPTION_URL: &str = "https://api.mistral.ai/v1/billing/subscription"; +/// The precedence the Vibe CLI itself gives its key variable. +const API_KEY_ENVIRONMENT: &str = "MISTRAL_API_KEY"; +/// Where the monitor keeps a key the user entered for it; the Vibe CLI keeps +/// its own copy in the OS keyring, which on Windows is Credential Manager. +const CREDENTIAL_TARGET: &str = "mistral:vibe-api-key"; + +const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + +#[derive(Debug, Deserialize)] +struct BillingUsageResponse { + #[serde(default)] + data: Vec, +} + +#[derive(Debug, Deserialize)] +struct BillingUsageEntry { + #[serde(default)] + total_cost: Option, + #[serde(default)] + cost: Option, + #[serde(default)] + start_date: Option, + #[serde(default)] + end_date: Option, +} + +#[derive(Debug, Deserialize)] +struct BillingSubscriptionResponse { + #[serde(default)] + monthly_budget: Option, +} + +struct ApiKeyCandidate { + source: String, + key: String, +} + +pub(super) fn poll_mistral() -> Result { + let candidates = api_key_candidates(); + if candidates.is_empty() { + diagnose::log( + "Mistral usage poll failed: no API key found (set MISTRAL_API_KEY or store a key in Windows Credential Manager)", + ); + return Err(PollError::NoCredentials); + } + + let mut last_error = PollError::AuthRequired; + for candidate in &candidates { + match fetch_mistral_usage(&candidate.key) { + Ok(usage) => return Ok(usage), + Err(error @ PollError::AuthRequired) => { + diagnose::log(format!( + "Mistral rejected the key from {}: {error:?}", + candidate.source + )); + last_error = error; + } + Err(error) => return Err(error), + } + } + Err(last_error) +} + +/// Fingerprints only: secrets are hashed, never logged. +pub(super) fn credential_watch_snapshot(_all_sources: bool) -> Vec { + let environment = match non_empty_environment(API_KEY_ENVIRONMENT) { + Some(value) => secret_signature("environment", &value), + None => format!("{API_KEY_ENVIRONMENT}|missing"), + }; + let stored = match windows_credentials::read_generic(CREDENTIAL_TARGET) { + Some(secret) => secret_signature(CREDENTIAL_TARGET, &secret), + None => format!("{CREDENTIAL_TARGET}|missing"), + }; + vec![environment, stored] +} + +fn api_key_candidates() -> Vec { + let mut candidates = Vec::new(); + if let Some(key) = non_empty_environment(API_KEY_ENVIRONMENT) { + candidates.push(ApiKeyCandidate { + source: format!("environment variable {API_KEY_ENVIRONMENT}"), + key, + }); + } + if let Some(secret) = windows_credentials::read_generic(CREDENTIAL_TARGET) { + candidates.push(ApiKeyCandidate { + source: format!("Windows credential {CREDENTIAL_TARGET}"), + key: secret.trim().to_string(), + }); + } + candidates + .into_iter() + .filter(|candidate| is_header_safe_key(&candidate.key)) + .collect() +} + +fn fetch_mistral_usage(api_key: &str) -> Result { + let agent = build_agent()?; + + let mut models_response = match agent + .get(MISTRAL_MODELS_URL) + .header("Authorization", format!("Bearer {api_key}")) + .header("Accept", "application/json") + .header("User-Agent", USER_AGENT) + .call() + .and_then(super::check_http_status) + { + Ok(response) => response, + Err(ureq::Error::StatusCode(401 | 403)) => return Err(PollError::AuthRequired), + Err(ureq::Error::StatusCode(code)) => { + diagnose::log(format!("Mistral models endpoint returned status {code}")); + return Err(PollError::HttpStatus(code)); + } + Err(error) => { + diagnose::log_error("Mistral models request failed", error); + return Err(PollError::NetworkError); + } + }; + // The model list itself is only a carrier for the rate-limit headers; the + // body is not needed, but draining it keeps the connection reusable. + let _ = models_response.body_mut().read_json::(); + + let session = rate_limit_section(&models_response); + + // Billing routes need a key with billing scope; without it the account + // still gets its rate-limit window instead of a hard failure. + let (spend, budget) = match fetch_billing(&agent, api_key) { + Ok(billing) => billing, + Err(PollError::AuthRequired) => return Err(PollError::AuthRequired), + Err(error) => { + diagnose::log(format!("Mistral billing endpoints unavailable: {error:?}")); + (None, None) + } + }; + + let monthly = spend.map(|spend| monthly_section(spend, budget, SystemTime::now())); + if session.is_none() && monthly.is_none() { + diagnose::log("Mistral response carried neither rate-limit headers nor spend data"); + return Err(PollError::UnexpectedResponse); + } + Ok(UsageData { + limits: Vec::new(), + session: session.unwrap_or_default(), + weekly: monthly.clone().unwrap_or_default(), + weekly_label: monthly.as_ref().map(|_| "30d".into()), + monthly, + credits: None, + stale: false, + ..Default::default() + }) +} + +/// Monthly spend and budget are optional; a 403 on billing routes simply means +/// the key cannot read them. +fn fetch_billing( + agent: &ureq::Agent, + api_key: &str, +) -> Result<(Option, Option), PollError> { + let mut usage_response = match agent + .get(MISTRAL_BILLING_USAGE_URL) + .header("Authorization", format!("Bearer {api_key}")) + .header("Accept", "application/json") + .header("User-Agent", USER_AGENT) + .call() + .and_then(super::check_http_status) + { + Ok(response) => response, + Err(ureq::Error::StatusCode(401)) => return Err(PollError::AuthRequired), + Err(ureq::Error::StatusCode(403 | 404)) => return Ok((None, None)), + Err(ureq::Error::StatusCode(code)) => return Err(PollError::HttpStatus(code)), + Err(error) => { + diagnose::log_error("Mistral billing usage request failed", error); + return Err(PollError::NetworkError); + } + }; + let usage: BillingUsageResponse = usage_response.body_mut().read_json().map_err(|error| { + diagnose::log_error("unable to parse Mistral billing usage response", error); + PollError::UnexpectedResponse + })?; + let spend = monthly_spend(&usage.data, SystemTime::now()); + + let mut subscription_response = match agent + .get(MISTRAL_BILLING_SUBSCRIPTION_URL) + .header("Authorization", format!("Bearer {api_key}")) + .header("Accept", "application/json") + .header("User-Agent", USER_AGENT) + .call() + .and_then(super::check_http_status) + { + Ok(response) => response, + Err(ureq::Error::StatusCode(401)) => return Err(PollError::AuthRequired), + Err(ureq::Error::StatusCode(403 | 404)) => return Ok((Some(spend), None)), + Err(ureq::Error::StatusCode(code)) => return Err(PollError::HttpStatus(code)), + Err(error) => { + diagnose::log_error("Mistral billing subscription request failed", error); + return Err(PollError::NetworkError); + } + }; + let subscription: BillingSubscriptionResponse = subscription_response + .body_mut() + .read_json() + .map_err(|error| { + diagnose::log_error( + "unable to parse Mistral billing subscription response", + error, + ); + PollError::UnexpectedResponse + })?; + let budget = subscription + .monthly_budget + .filter(|value| value.is_finite() && *value > 0.0); + Ok((Some(spend), budget)) +} + +/// Mistral rate limits are reported per minute as response headers on every +/// endpoint; `/v1/models` is the cheapest one to ask. +fn rate_limit_section(response: &super::HttpResponse) -> Option { + let requests = window_from_headers( + get_header_i64(response, "x-ratelimit-limit-requests"), + get_header_i64(response, "x-ratelimit-remaining-requests"), + response + .headers() + .get("x-ratelimit-reset-requests") + .and_then(|value| value.to_str().ok()), + ); + if requests.is_some() { + return requests; + } + window_from_headers( + get_header_i64(response, "x-ratelimit-limit-tokens"), + get_header_i64(response, "x-ratelimit-remaining-tokens"), + response + .headers() + .get("x-ratelimit-reset-tokens") + .and_then(|value| value.to_str().ok()), + ) +} + +fn window_from_headers( + limit: Option, + remaining: Option, + reset: Option<&str>, +) -> Option { + let limit = limit?; + let remaining = remaining?; + if limit <= 0 { + return None; + } + let percentage = ((limit - remaining) as f64 / limit as f64 * 100.0).clamp(0.0, 100.0); + let resets_at = + parse_reset_seconds(reset).map(|seconds| SystemTime::now() + Duration::from_secs(seconds)); + Some(UsageSection { + available: true, + percentage, + resets_at, + }) +} + +/// `x-ratelimit-reset-*` is a duration until the window refills, either a bare +/// number of seconds or a suffixed string such as `30s` or `1m`. +fn parse_reset_seconds(value: Option<&str>) -> Option { + let value = value?.trim(); + if value.is_empty() { + return None; + } + if let Ok(seconds) = value.parse::() { + return Some(seconds); + } + let (digits, unit) = value.split_at(value.len().saturating_sub(1)); + let multiplier = match unit { + "s" => 1, + "m" => 60, + "h" => 3_600, + _ => return None, + }; + digits.parse::().ok().map(|value| value * multiplier) +} + +/// Sum the cost entries that overlap the current calendar month. Entries +/// without dates belong to the month the endpoint already scoped to. +fn monthly_spend(entries: &[BillingUsageEntry], now: SystemTime) -> f64 { + let (month_start, month_end) = current_month_bounds(now); + entries + .iter() + .filter(|entry| entry_overlaps_month(entry, month_start, month_end)) + .filter_map(|entry| { + entry + .total_cost + .or(entry.cost) + .filter(|value| value.is_finite() && *value >= 0.0) + }) + .sum() +} + +fn entry_overlaps_month(entry: &BillingUsageEntry, month_start: u64, month_end: u64) -> bool { + let start = entry + .start_date + .as_deref() + .and_then(|value| super::parse_iso8601(Some(value))) + .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_secs()); + let end = entry + .end_date + .as_deref() + .and_then(|value| super::parse_iso8601(Some(value))) + .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_secs()); + match (start, end) { + (None, None) => true, + (start, end) => { + let start = start.unwrap_or(0); + let end = end.unwrap_or(u64::MAX); + start < month_end && end >= month_start + } + } +} + +/// Spend against the monthly budget, on the long-window row like Copilot's +/// calendar-month pool; the budgetless reading still reports the reset time. +fn monthly_section(spend: f64, budget: Option, now: SystemTime) -> UsageSection { + let percentage = match budget { + Some(budget) => (spend / budget * 100.0).clamp(0.0, 100.0), + None => 0.0, + }; + UsageSection { + available: true, + percentage, + resets_at: Some(UNIX_EPOCH + Duration::from_secs(next_month_start(now))), + } +} + +fn current_month_bounds(now: SystemTime) -> (u64, u64) { + let now_secs = now + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let (year, month, _) = civil_from_days(now_secs / 86_400); + let start = days_from_civil(year, month, 1) * 86_400; + let (next_year, next_month) = if month == 12 { + (year + 1, 1) + } else { + (year, month + 1) + }; + let end = days_from_civil(next_year, next_month, 1) * 86_400; + (start, end) +} + +fn next_month_start(now: SystemTime) -> u64 { + current_month_bounds(now).1 +} + +/// Days since 1970-01-01 to civil date, after Howard Hinnant's algorithm. +fn civil_from_days(days: u64) -> (u64, u64, u64) { + let z = days as i64 + 719_468; + let era = if z >= 0 { z } else { z - 146_096 } / 146_097; + let doe = (z - era * 146_097) as u64; + let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; + let y = yoe + era as u64 * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + (if m <= 2 { y + 1 } else { y }, m, d) +} + +fn days_from_civil(year: u64, month: u64, day: u64) -> u64 { + let y = if month <= 2 { year - 1 } else { year } as i64; + let era = if y >= 0 { y } else { y - 399 } / 400; + let yoe = (y - era * 400) as u64; + let mp = if month > 2 { month - 3 } else { month + 9 }; + let doy = (153 * mp + 2) / 5 + day - 1; + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + (era * 146_097 + doe as i64 - 719_468) as u64 +} + +/// Keys go into a header, so anything that could split it is unusable. +fn is_header_safe_key(key: &str) -> bool { + !key.is_empty() && key.bytes().all(|byte| byte.is_ascii_graphic()) +} + +fn non_empty_environment(name: &str) -> Option { + std::env::var(name) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} + +fn secret_signature(source: &str, value: &str) -> String { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + format!("{source}|present|{}|{:x}", value.len(), hasher.finish()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn entry(cost: Option, start: Option<&str>, end: Option<&str>) -> BillingUsageEntry { + BillingUsageEntry { + total_cost: cost, + cost: None, + start_date: start.map(str::to_string), + end_date: end.map(str::to_string), + } + } + + #[test] + fn billing_entries_sum_within_the_current_month() { + // 2026-11-03T00:00:00Z is inside November 2026. + let now = UNIX_EPOCH + Duration::from_secs(1_793_664_000); + let entries = vec![ + entry( + Some(10.5), + Some("2026-11-01T00:00:00Z"), + Some("2026-11-03T00:00:00Z"), + ), + entry( + Some(2.0), + Some("2026-10-31T00:00:00Z"), + Some("2026-11-01T00:00:00Z"), + ), + entry( + Some(99.0), + Some("2026-10-01T00:00:00Z"), + Some("2026-10-31T00:00:00Z"), + ), + entry(Some(1.25), None, None), + entry(Some(-5.0), None, None), + entry(None, None, None), + ]; + assert_eq!(monthly_spend(&entries, now), 13.75); + } + + #[test] + fn monthly_section_measures_spend_against_the_budget() { + let now = UNIX_EPOCH + Duration::from_secs(1_793_664_000); + let section = monthly_section(25.0, Some(100.0), now); + assert!(section.available); + assert_eq!(section.percentage, 25.0); + // Resets at midnight UTC on 2026-12-01. + assert_eq!( + section.resets_at, + Some(UNIX_EPOCH + Duration::from_secs(1_796_083_200)) + ); + + let uncapped = monthly_section(25.0, None, now); + assert_eq!(uncapped.percentage, 0.0); + assert!(uncapped.resets_at.is_some()); + + let over = monthly_section(250.0, Some(100.0), now); + assert_eq!(over.percentage, 100.0); + } + + #[test] + fn month_bounds_wrap_the_year() { + // 2026-11-30T00:00:00Z: November runs 2026-11-01 .. 2026-12-01. + let november = UNIX_EPOCH + Duration::from_secs(1_796_083_200 - 86_400); + assert_eq!( + current_month_bounds(november), + (1_793_491_200, 1_796_083_200) + ); + // 2026-12-31T00:00:00Z: December wraps into January 2027. + let december = UNIX_EPOCH + Duration::from_secs(1_798_761_600 - 86_400); + assert_eq!( + current_month_bounds(december), + (1_796_083_200, 1_798_761_600) + ); + } + + #[test] + fn rate_limit_windows_prefer_requests_and_clamp_to_the_limit() { + let window = window_from_headers(Some(100), Some(40), Some("30")).unwrap(); + assert!(window.available); + assert_eq!(window.percentage, 60.0); + assert!(window.resets_at.is_some()); + + let saturated = window_from_headers(Some(100), Some(-5), None).unwrap(); + assert_eq!(saturated.percentage, 100.0); + assert!(saturated.resets_at.is_none()); + + assert!(window_from_headers(Some(0), Some(0), None).is_none()); + assert!(window_from_headers(None, Some(5), None).is_none()); + assert!(window_from_headers(Some(100), None, None).is_none()); + } + + #[test] + fn reset_headers_accept_seconds_and_duration_strings() { + assert_eq!(parse_reset_seconds(Some("45")), Some(45)); + assert_eq!(parse_reset_seconds(Some("30s")), Some(30)); + assert_eq!(parse_reset_seconds(Some("2m")), Some(120)); + assert_eq!(parse_reset_seconds(Some("1h")), Some(3_600)); + assert_eq!(parse_reset_seconds(Some("soon")), None); + assert_eq!(parse_reset_seconds(Some("")), None); + assert_eq!(parse_reset_seconds(None), None); + } + + #[test] + fn keys_that_could_split_a_header_are_refused() { + assert!(is_header_safe_key("abc123XYZ")); + assert!(!is_header_safe_key("")); + assert!(!is_header_safe_key("abc\r\nInjected: yes")); + assert!(!is_header_safe_key("two words")); + } + + #[test] + fn civil_date_round_trips_through_the_days_algorithms() { + for days in [0_u64, 1, 365, 19_000, 21_000, 56_000] { + let (y, m, d) = civil_from_days(days); + assert_eq!(days_from_civil(y, m, d), days); + } + assert_eq!(civil_from_days(0), (1970, 1, 1)); + assert_eq!(civil_from_days(21_018), (2027, 7, 19)); + } +} diff --git a/src/poller/tests.rs b/src/poller/tests.rs index a9665996..0f1fc631 100644 --- a/src/poller/tests.rs +++ b/src/poller/tests.rs @@ -139,6 +139,7 @@ fn claude_failure_does_not_block_codex_when_both_are_enabled() { ProviderId::Cursor => unreachable!("Cursor is disabled"), ProviderId::Grok => unreachable!("Grok is disabled"), ProviderId::Copilot => unreachable!("Copilot is disabled"), + ProviderId::Mistral => unreachable!("Mistral is disabled"), }, ) .expect("codex data should keep the poll successful"); @@ -162,6 +163,7 @@ fn codex_failure_does_not_block_claude_when_both_are_enabled() { ProviderId::Cursor => unreachable!("Cursor is disabled"), ProviderId::Grok => unreachable!("Grok is disabled"), ProviderId::Copilot => unreachable!("Copilot is disabled"), + ProviderId::Mistral => unreachable!("Mistral is disabled"), }, ) .expect("claude data should keep the poll successful"); @@ -185,6 +187,7 @@ fn returns_first_error_when_no_enabled_provider_succeeds() { ProviderId::Cursor => Err(PollError::NoCredentials), ProviderId::Grok => Err(PollError::NoCredentials), ProviderId::Copilot => Err(PollError::NoCredentials), + ProviderId::Mistral => Err(PollError::NoCredentials), }, ) .expect_err("all-provider failure should return an error"); @@ -305,6 +308,7 @@ fn antigravity_failure_does_not_block_codex_when_both_are_enabled() { ProviderId::Cursor => unreachable!("Cursor is disabled"), ProviderId::Grok => unreachable!("Grok is disabled"), ProviderId::Copilot => unreachable!("Copilot is disabled"), + ProviderId::Mistral => unreachable!("Mistral is disabled"), }, ) .expect("codex data should keep the poll successful"); @@ -328,6 +332,7 @@ fn opencode_failure_does_not_block_codex_when_both_are_enabled() { ProviderId::Cursor => unreachable!("Cursor is disabled"), ProviderId::Grok => unreachable!("Grok is disabled"), ProviderId::Copilot => unreachable!("Copilot is disabled"), + ProviderId::Mistral => unreachable!("Mistral is disabled"), }, ) .expect("Codex data should keep the poll successful"); diff --git a/src/providers.rs b/src/providers.rs index aa685b6b..a8da3c34 100644 --- a/src/providers.rs +++ b/src/providers.rs @@ -16,6 +16,7 @@ pub enum ProviderId { Cursor = 4, Grok = 5, Copilot = 6, + Mistral = 7, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -34,7 +35,7 @@ pub struct ProviderDescriptor { pub default_enabled: bool, } -pub const PROVIDER_DESCRIPTORS: [ProviderDescriptor; 7] = [ +pub const PROVIDER_DESCRIPTORS: [ProviderDescriptor; 8] = [ ProviderDescriptor { id: ProviderId::Claude, key: "claude", @@ -98,10 +99,19 @@ pub const PROVIDER_DESCRIPTORS: [ProviderDescriptor; 7] = [ native_menu_command_id: 66, default_enabled: false, }, + ProviderDescriptor { + id: ProviderId::Mistral, + key: "mistral", + cache_key: "mistral", + display_name: "Mistral", + settings_description: "Collect usage from Mistral", + native_menu_command_id: 69, + default_enabled: false, + }, ]; impl ProviderId { - pub const ALL: [Self; 7] = [ + pub const ALL: [Self; 8] = [ Self::Claude, Self::Codex, Self::Antigravity, @@ -109,6 +119,7 @@ impl ProviderId { Self::Cursor, Self::Grok, Self::Copilot, + Self::Mistral, ]; pub const fn descriptor(self) -> &'static ProviderDescriptor { diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index 73e7c38d..0d8fe2df 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -1086,13 +1086,13 @@ fn starter_theme_round_trips_and_validates() { .collect::>(); // Classic contains separate light and dark progress layers so the // 1.4.9 palette follows the taskbar mode without runtime recolouring: - // five providers over two windows in two modes, plus Grok and Copilot, - // which fill only the long-window row, plus a credit overlay on that row - // for the three providers that report credits, plus the two eight-segment - // Fable layers. + // five providers over two windows in two modes, plus Grok, Copilot and + // Mistral, which fill only the long-window row, plus a credit overlay on + // that row for the three providers that report credits, plus the two + // eight-segment Fable layers. assert_eq!( segments, - [vec![10; 5 * 2 * 2 + 2 * 2 + 3 * 2], vec![8; 2]].concat() + [vec![10; 5 * 2 * 2 + 3 * 2 + 3 * 2], vec![8; 2]].concat() ); assert!(theme.surfaces[0] .children @@ -2131,7 +2131,7 @@ fn starter_adapts_width_segments_and_collapsed_provider_rows() { ), ( ThemeRuntime::from_providers(ProviderSet::from_enabled(ProviderId::ALL)), - 725, + 815, 2, ), ] { @@ -2207,7 +2207,7 @@ fn starter_has_a_taskbar_widget_and_provider_tray_icons() { theme.surfaces[0].placement.reference.region, ReferenceRegion::SystemTray ); - assert_eq!(theme.surfaces.len(), 8); + assert_eq!(theme.surfaces.len(), 9); assert!(theme.surfaces[1..] .iter() .all(|surface| surface.placement.nest == SurfaceNest::TrayIcon)); @@ -2327,7 +2327,7 @@ fn classic_vertical_stacks_enabled_providers_and_preserves_tray_roots() { ThemeRuntime::from_providers(providers).with_host_dimensions(host_width, 1440); let width = (host_width - 2).min(62); let provider_height = |provider| match provider { - ProviderId::Grok | ProviderId::Copilot => 42, + ProviderId::Grok | ProviderId::Copilot | ProviderId::Mistral => 42, _ => 64, }; let height = providers.iter().map(provider_height).sum(); diff --git a/src/theme_engine/theme_expression.rs b/src/theme_engine/theme_expression.rs index 21fb22f9..ad0e5a2e 100644 --- a/src/theme_engine/theme_expression.rs +++ b/src/theme_engine/theme_expression.rs @@ -523,6 +523,7 @@ pub(super) fn format_usage_line(base: &str, context: &DataContext) -> Option= 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "colour", + "colour": { + "color": "#9D174DFF" + } + }, + "corner_radius": "3" + }, + { + "id": "mistral-tray-low-fill", + "name": "Mistral normal black fill", + "render": "mistral.headline.percentage < 90", + "parent": "mistral-tray-icon", + "x": "3", + "y": "3", + "width": "58", + "height": "58", + "background": { + "type": "colour", + "colour": { + "color": "#E05E00FF" + } + }, + "corner_radius": "1" + }, + { + "id": "mistral-tray-high-fill", + "name": "Mistral high-usage white fill", + "render": "mistral.headline.percentage >= 90", + "parent": "mistral-tray-icon", + "x": "3", + "y": "3", + "width": "58", + "height": "58", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF" + } + }, + "corner_radius": "1" + }, + { + "id": "mistral-tray-placeholder", + "name": "Mistral loading placeholder", + "render": "data.poll_ok == 0 || mistral.available == 0", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "Mi", + "font_family": "Arial Bold", + "font_size": "34", + "weight": "bold", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-low-one-digit", + "name": "Mistral normal one-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display < 9.5 && mistral.headline.percentage < 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "50", + "weight": "bold", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-low-two-digits", + "name": "Mistral normal two-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 9.5 && mistral.headline.display < 99.5 && mistral.headline.percentage < 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "42", + "weight": "bold", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-low-three-digits", + "name": "Mistral normal three-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 99.5 && mistral.headline.percentage < 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "30", + "weight": "bold", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-high-one-digit", + "name": "Mistral high-usage one-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display < 9.5 && mistral.headline.percentage >= 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "50", + "weight": "bold", + "align": "center", + "color": { + "color": "#9D174DFF" + } + } + }, + { + "id": "mistral-tray-high-two-digits", + "name": "Mistral high-usage two-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 9.5 && mistral.headline.display < 99.5 && mistral.headline.percentage >= 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "42", + "weight": "bold", + "align": "center", + "color": { + "color": "#9D174DFF" + } + } + }, + { + "id": "mistral-tray-high-three-digits", + "name": "Mistral high-usage three-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 99.5 && mistral.headline.percentage >= 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "30", + "weight": "bold", + "align": "center", + "color": { + "color": "#9D174DFF" + } + } + } + ] + }, { "id": "opencode-tray-icon", "name": "OpenCode usage", diff --git a/src/themes/classic-vertical.json b/src/themes/classic-vertical.json index 97af21ee..14368390 100644 --- a/src/themes/classic-vertical.json +++ b/src/themes/classic-vertical.json @@ -20,7 +20,7 @@ "offset_y": -52 }, "width": "min(62, max(1, host.width - 2))", - "height": "max(1, providers.count * 64 - (providers.grok.enabled + providers.copilot.enabled) * 22)", + "height": "max(1, providers.count * 64 - (providers.grok.enabled + providers.copilot.enabled + providers.mistral.enabled) * 22)", "mouse_events": { "right_click": "show_context_menu(\"classic-v1\")" }, @@ -1112,6 +1112,14 @@ "width": "canvas.width", "height": "42" }, + { + "id": "mistral-provider", + "name": "Mistral", + "parent": "provider-column", + "render": "providers.mistral.enabled", + "width": "canvas.width", + "height": "42" + }, { "id": "copilot-title", "name": "copilot-title", @@ -1135,6 +1143,29 @@ } } }, + { + "id": "mistral-title", + "name": "mistral-title", + "parent": "mistral-provider", + "render": "system.dark", + "x": "2", + "y": "2", + "width": "max(1, canvas.width - 4)", + "height": "14", + "content": { + "type": "text", + "template": "Mistral", + "font_family": "Segoe UI", + "font_size": "min(10, (canvas.width - 4) / 5.5)", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "center", + "color": { + "color": "#FF7000" + } + } + }, { "id": "copilot-title-light", "name": "copilot-title-light", @@ -1158,6 +1189,29 @@ } } }, + { + "id": "mistral-title-light", + "name": "mistral-title-light", + "parent": "mistral-provider", + "render": "1 - system.dark", + "x": "2", + "y": "2", + "width": "max(1, canvas.width - 4)", + "height": "14", + "content": { + "type": "text", + "template": "Mistral", + "font_family": "Segoe UI", + "font_size": "min(10, (canvas.width - 4) / 5.5)", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "center", + "color": { + "color": "#9A3412" + } + } + }, { "id": "copilot-weekly-value-dark", "name": "copilot-weekly-value-dark", @@ -1181,6 +1235,29 @@ } } }, + { + "id": "mistral-weekly-value-dark", + "name": "mistral-weekly-value-dark", + "parent": "mistral-provider", + "render": "system.dark", + "x": "2", + "y": "18", + "width": "max(1, canvas.width - 4)", + "height": "14", + "content": { + "type": "text", + "template": "{mistral.weekly.label} {mistral.weekly.display:usage_badge}", + "font_family": "Segoe UI", + "font_size": "min(10, (canvas.width - 4) / 5.5)", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "center", + "color": { + "color": "#F5F5F5" + } + } + }, { "id": "copilot-weekly-value-light", "name": "copilot-weekly-value-light", @@ -1204,6 +1281,29 @@ } } }, + { + "id": "mistral-weekly-value-light", + "name": "mistral-weekly-value-light", + "parent": "mistral-provider", + "render": "1 - system.dark", + "x": "2", + "y": "18", + "width": "max(1, canvas.width - 4)", + "height": "14", + "content": { + "type": "text", + "template": "{mistral.weekly.label} {mistral.weekly.display:usage_badge}", + "font_family": "Segoe UI", + "font_size": "min(10, (canvas.width - 4) / 5.5)", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "center", + "color": { + "color": "#202020" + } + } + }, { "id": "copilot-weekly-bar", "name": "copilot weekly usage", @@ -1225,6 +1325,28 @@ }, "corner_radius": "1" } + }, + { + "id": "mistral-weekly-bar", + "name": "mistral weekly usage", + "parent": "mistral-provider", + "render": "mistral.weekly.available", + "x": "4", + "y": "33", + "width": "max(1, canvas.width - 8)", + "height": "3", + "content": { + "type": "progress", + "value": "mistral.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#DB2777" + }, + "track": { + "color": "#80808040" + }, + "corner_radius": "1" + } } ] }, @@ -2555,6 +2677,212 @@ } ] }, + { + "id": "mistral-tray-icon", + "name": "Mistral usage", + "render": "providers.mistral.enabled", + "mouse_events": { + "click": "toggle(\"main\", render)", + "double_click": "toggle(\"main\", render)", + "right_click": "show_context_menu(\"classic-v1\")" + }, + "placement": { + "nest": "tray_icon" + }, + "width": "64", + "height": "64", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF" + } + }, + "corner_radius": "3", + "children": [ + { + "id": "mistral-tray-high-outline", + "name": "Mistral high-usage black outline", + "render": "mistral.headline.percentage >= 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "colour", + "colour": { + "color": "#9D174DFF" + } + }, + "corner_radius": "3" + }, + { + "id": "mistral-tray-low-fill", + "name": "Mistral normal black fill", + "render": "mistral.headline.percentage < 90", + "parent": "mistral-tray-icon", + "x": "3", + "y": "3", + "width": "58", + "height": "58", + "background": { + "type": "colour", + "colour": { + "color": "#E05E00FF" + } + }, + "corner_radius": "1" + }, + { + "id": "mistral-tray-high-fill", + "name": "Mistral high-usage white fill", + "render": "mistral.headline.percentage >= 90", + "parent": "mistral-tray-icon", + "x": "3", + "y": "3", + "width": "58", + "height": "58", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF" + } + }, + "corner_radius": "1" + }, + { + "id": "mistral-tray-placeholder", + "name": "Mistral loading placeholder", + "render": "data.poll_ok == 0 || mistral.available == 0", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "Mi", + "font_family": "Arial Bold", + "font_size": "34", + "weight": "bold", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-low-one-digit", + "name": "Mistral normal one-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display < 9.5 && mistral.headline.percentage < 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "50", + "weight": "bold", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-low-two-digits", + "name": "Mistral normal two-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 9.5 && mistral.headline.display < 99.5 && mistral.headline.percentage < 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "42", + "weight": "bold", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-low-three-digits", + "name": "Mistral normal three-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 99.5 && mistral.headline.percentage < 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "30", + "weight": "bold", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-high-one-digit", + "name": "Mistral high-usage one-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display < 9.5 && mistral.headline.percentage >= 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "50", + "weight": "bold", + "align": "center", + "color": { + "color": "#9D174DFF" + } + } + }, + { + "id": "mistral-tray-high-two-digits", + "name": "Mistral high-usage two-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 9.5 && mistral.headline.display < 99.5 && mistral.headline.percentage >= 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "42", + "weight": "bold", + "align": "center", + "color": { + "color": "#9D174DFF" + } + } + }, + { + "id": "mistral-tray-high-three-digits", + "name": "Mistral high-usage three-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 99.5 && mistral.headline.percentage >= 90", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "30", + "weight": "bold", + "align": "center", + "color": { + "color": "#9D174DFF" + } + } + } + ] + }, { "id": "opencode-tray-icon", "name": "OpenCode usage", diff --git a/src/themes/compact-fluent-quad.json b/src/themes/compact-fluent-quad.json index 0a5a9e2e..882b94a5 100644 --- a/src/themes/compact-fluent-quad.json +++ b/src/themes/compact-fluent-quad.json @@ -2784,6 +2784,14 @@ "width": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62", "height": "46" }, + { + "id": "mistral-provider", + "name": "Mistral provider", + "render": "providers.mistral.enabled", + "parent": "provider-row", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62", + "height": "46" + }, { "id": "copilot-pool-segments-dark", "name": "dark Copilot pool segments", @@ -2819,6 +2827,41 @@ "segment_gap": "1" } }, + { + "id": "mistral-pool-segments-dark", + "name": "dark Mistral pool segments", + "render": "system.dark", + "visibility": "100", + "parent": "mistral-provider", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "9", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "mistral.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#FF8A00FF", + "opacity": "1" + }, + "track": { + "color": "#444444FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, { "id": "copilot-pool-value-dark", "name": "dark Copilot pool value", @@ -2852,6 +2895,39 @@ } } }, + { + "id": "mistral-pool-value-dark", + "name": "dark Mistral pool value", + "render": "system.dark", + "visibility": "100", + "parent": "mistral-provider", + "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "25", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{mistral.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#FFC49AFF", + "opacity": "1" + } + } + }, { "id": "copilot-pool-segments-light", "name": "light Copilot pool segments", @@ -2887,6 +2963,41 @@ "segment_gap": "1" } }, + { + "id": "mistral-pool-segments-light", + "name": "light Mistral pool segments", + "render": "1 - system.dark", + "visibility": "100", + "parent": "mistral-provider", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "9", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "mistral.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#E05E00FF", + "opacity": "1" + }, + "track": { + "color": "#AAAAAAFF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, { "id": "copilot-pool-value-light", "name": "light Copilot pool value", @@ -2919,6 +3030,39 @@ "opacity": "1" } } + }, + { + "id": "mistral-pool-value-light", + "name": "light Mistral pool value", + "render": "1 - system.dark", + "visibility": "100", + "parent": "mistral-provider", + "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "25", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{mistral.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#C24E00FF", + "opacity": "1" + } + } } ] }, @@ -4828,6 +4972,336 @@ } ] }, + { + "id": "mistral-tray-icon", + "name": "Mistral usage", + "render": "0", + "visibility": "100", + "placement": { + "reference": { + "region": "system_tray", + "display": 0 + }, + "nest": "tray_icon", + "horizontal": "right", + "vertical": "bottom", + "surface_horizontal": "right", + "surface_vertical": "bottom", + "offset_x": 0, + "offset_y": 0 + }, + "width": "64", + "height": "64", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "1" + } + }, + "border": null, + "mouse_events": { + "click": "toggle(\"main\", render)", + "double_click": "toggle(\"main\", render)", + "right_click": "show_context_menu(\"classic-v1\")" + }, + "corner_radius": "3", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + }, + "children": [ + { + "id": "mistral-tray-high-outline", + "name": "Mistral high-usage black outline", + "render": "mistral.headline.percentage >= 90", + "visibility": "100", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "colour", + "colour": { + "color": "#9D174DFF", + "opacity": "1" + } + }, + "border": null, + "corner_radius": "3", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "mistral-tray-low-fill", + "name": "Mistral normal black fill", + "render": "mistral.headline.percentage < 90", + "visibility": "100", + "parent": "mistral-tray-icon", + "x": "3", + "y": "3", + "width": "58", + "height": "58", + "background": { + "type": "colour", + "colour": { + "color": "#E05E00FF", + "opacity": "1" + } + }, + "border": null, + "corner_radius": "1", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "mistral-tray-high-fill", + "name": "Mistral high-usage white fill", + "render": "mistral.headline.percentage >= 90", + "visibility": "100", + "parent": "mistral-tray-icon", + "x": "3", + "y": "3", + "width": "58", + "height": "58", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "1" + } + }, + "border": null, + "corner_radius": "1", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "mistral-tray-placeholder", + "name": "Mistral loading placeholder", + "render": "data.poll_ok == 0 || mistral.available == 0", + "visibility": "100", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "Mi", + "font_family": "Arial Bold", + "font_size": "34", + "weight": "bold", + "rendering": "antialiased", + "contrast": "1.4", + "align": "center", + "color": { + "color": "#FFFFFFFF", + "opacity": "1" + } + } + }, + { + "id": "mistral-tray-low-one-digit", + "name": "Mistral normal one-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display < 9.5 && mistral.headline.percentage < 90", + "visibility": "100", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "50", + "weight": "bold", + "rendering": "antialiased", + "contrast": "1.4", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-low-two-digits", + "name": "Mistral normal two-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 9.5 && mistral.headline.display < 99.5 && mistral.headline.percentage < 90", + "visibility": "100", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "42", + "weight": "bold", + "rendering": "antialiased", + "contrast": "1.4", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-low-three-digits", + "name": "Mistral normal three-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 99.5 && mistral.headline.percentage < 90", + "visibility": "100", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "30", + "weight": "bold", + "rendering": "antialiased", + "contrast": "1.4", + "align": "center", + "color": { + "color": "#FFFFFFFF" + } + } + }, + { + "id": "mistral-tray-high-one-digit", + "name": "Mistral high-usage one-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display < 9.5 && mistral.headline.percentage >= 90", + "visibility": "100", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "50", + "weight": "bold", + "rendering": "antialiased", + "contrast": "1.4", + "align": "center", + "color": { + "color": "#9D174DFF" + } + } + }, + { + "id": "mistral-tray-high-two-digits", + "name": "Mistral high-usage two-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 9.5 && mistral.headline.display < 99.5 && mistral.headline.percentage >= 90", + "visibility": "100", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "42", + "weight": "bold", + "rendering": "antialiased", + "contrast": "1.4", + "align": "center", + "color": { + "color": "#9D174DFF" + } + } + }, + { + "id": "mistral-tray-high-three-digits", + "name": "Mistral high-usage three-digit percentage", + "render": "data.poll_ok && mistral.available && mistral.headline.display >= 99.5 && mistral.headline.percentage >= 90", + "visibility": "100", + "parent": "mistral-tray-icon", + "width": "64", + "height": "64", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{mistral.headline.display:0}", + "font_family": "Arial Bold", + "font_size": "30", + "weight": "bold", + "rendering": "antialiased", + "contrast": "1.4", + "align": "center", + "color": { + "color": "#9D174DFF" + } + } + } + ] + }, { "id": "opencode-tray-icon", "name": "OpenCode usage", diff --git a/src/ui/theme.rs b/src/ui/theme.rs index f9f7a449..c1f8e3a4 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -39,6 +39,7 @@ const UI_FALLBACK_FONT_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/ /// | U+F005 | Cursor | Lobe Icons (MIT) | /// | U+F006 | Grok | Lobe Icons (MIT) | /// | U+F007 | Copilot | Lobe Icons (MIT) | +/// | U+F008 | Mistral | Simple Icons (CC0-1.0) | /// /// Those licences cover the path data. The logos remain the trademarks of their /// owners and identify the provider a setting belongs to. @@ -62,6 +63,7 @@ pub(crate) fn provider_mark_glyph(provider: ProviderId) -> char { ProviderId::Cursor => '\u{f005}', ProviderId::Grok => '\u{f006}', ProviderId::Copilot => '\u{f007}', + ProviderId::Mistral => '\u{f008}', } } diff --git a/tests/windows-vm/cases/vertical-taskbar.ps1 b/tests/windows-vm/cases/vertical-taskbar.ps1 index 480706ed..c9e73258 100644 --- a/tests/windows-vm/cases/vertical-taskbar.ps1 +++ b/tests/windows-vm/cases/vertical-taskbar.ps1 @@ -93,7 +93,7 @@ function Select-VerticalTheme([string]$Path, [switch]$AllProviders, [switch]$Loc language='en'; poll_interval_ms=900000; custom_theme_enabled=$true; active_theme_path=$Path lock_taskbar=[bool]$LockTaskbar show_claude_code=$true; show_codex=[bool]$AllProviders; show_antigravity=[bool]$AllProviders - show_opencode=[bool]$AllProviders; show_cursor=[bool]$AllProviders; show_grok=[bool]$AllProviders; show_copilot=[bool]$AllProviders + show_opencode=[bool]$AllProviders; show_cursor=[bool]$AllProviders; show_grok=[bool]$AllProviders; show_copilot=[bool]$AllProviders; show_mistral=[bool]$AllProviders last_update_check_unix=[DateTimeOffset]::UtcNow.ToUnixTimeSeconds() } } @@ -229,7 +229,7 @@ try { $fixture.id = 'classic-vertical-sample' $fixture.name = 'Classic Vertical sample usage fixture' foreach ($object in $fixture.surfaces[0].children) { - if ($object.id -match '^(claude|codex|antigravity|opencode|cursor|grok|copilot)-(session|weekly)-') { + if ($object.id -match '^(claude|codex|antigravity|opencode|cursor|grok|copilot|mistral)-(session|weekly)-') { $provider = $Matches[1]; $window = $Matches[2] if ($object.content.type -eq 'progress') { $object.render = '1' @@ -237,7 +237,7 @@ try { } elseif ($object.content.type -eq 'text') { $badge = '{' + $provider + '.' + $window + '.display:usage_badge}' $object.content.template = $object.content.template.Replace($badge, $(if ($window -eq 'session') { '42%' } else { '69%' })) - $object.content.template = $object.content.template.Replace(('{' + $provider + '.weekly.label}'), $(if ($provider -in @('grok','copilot')) { '30d' } else { '7d' })) + $object.content.template = $object.content.template.Replace(('{' + $provider + '.weekly.label}'), $(if ($provider -in @('grok','copilot','mistral')) { '30d' } else { '7d' })) } } }