From a49c0d83053e54d0277b687973e448e5e8605a25 Mon Sep 17 00:00:00 2001 From: fix2it Date: Mon, 19 Oct 2026 02:07:49 +0300 Subject: [PATCH 01/17] feat(compact): pace tick on usage gauges with a menu toggle Draw a 1 px tick on each Claude, Codex and Antigravity 5-hour and weekly gauge in the compact theme at the share of the window that has elapsed (remaining mode: 100 - elapsed). The engine exposes {window}.expected / expected_display / expected_available. Settings menu gets "Show pace" (on by default, display.show_pace binding). Co-Authored-By: Claude Opus 5.5 --- src/app_settings.rs | 24 + src/context_menu.rs | 16 + src/localization/locales/de.toml | 1 + src/localization/locales/en.toml | 1 + src/localization/locales/es.toml | 1 + src/localization/locales/fr.toml | 1 + src/localization/locales/ja.toml | 1 + src/localization/locales/ko.toml | 1 + src/localization/locales/nl.toml | 1 + src/localization/locales/pl.toml | 1 + src/localization/locales/pt-BR.toml | 1 + src/localization/locales/ru.toml | 1 + src/localization/locales/th.toml | 1 + src/localization/locales/tr.toml | 1 + src/localization/locales/zh-CN.toml | 1 + src/localization/locales/zh-TW.toml | 1 + src/studio_app/studio_context_menu_editor.rs | 2 + src/studio_app/studio_core.rs | 1 + src/studio_app/studio_helper.rs | 8 + src/studio_app/studio_native_menu.rs | 1 + src/studio_app/tests.rs | 1 + src/theme_engine.rs | 42 ++ src/theme_engine/tests.rs | 213 +++++++ src/themes/compact-fluent-quad.json | 624 +++++++++++++++++++ src/window.rs | 6 + src/window/message_loop.rs | 11 + src/window/placement_regression_tests.rs | 1 + src/window/window_context_menu.rs | 2 + 28 files changed, 966 insertions(+) diff --git a/src/app_settings.rs b/src/app_settings.rs index 472e5d20..fd18521c 100644 --- a/src/app_settings.rs +++ b/src/app_settings.rs @@ -75,6 +75,8 @@ pub struct SettingsFile { /// Preserve theme fonts and Windows substitution unless explicitly disabled. #[serde(default = "default_true")] pub use_windows_default_font: bool, + #[serde(default = "default_true")] + pub show_pace: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub active_theme_path: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -143,6 +145,7 @@ impl Default for SettingsFile { custom_theme_enabled: true, usage_countdown: false, use_windows_default_font: true, + show_pace: true, active_theme_path: None, dashboard_width: None, dashboard_height: None, @@ -690,6 +693,27 @@ mod tests { assert_eq!(settings_json(&counting_down)["usage_countdown"], true); } + #[test] + fn show_pace_defaults_to_on_and_round_trips() { + let settings = SettingsFile::default(); + assert!(settings.show_pace); + assert_eq!(settings_json(&settings)["show_pace"], true); + + let legacy = decode_settings(r#"{"poll_interval_ms":900000}"#).unwrap(); + assert!(legacy.show_pace); + + for show_pace in [false, true] { + let settings = SettingsFile { + show_pace, + ..Default::default() + }; + let json = settings_json(&settings); + assert_eq!(json["show_pace"], show_pace); + let loaded = decode_settings(&json.to_string()).unwrap(); + assert_eq!(loaded.show_pace, show_pace); + } + } + #[test] fn settings_always_use_the_theme_widget() { let mut settings = SettingsFile { diff --git a/src/context_menu.rs b/src/context_menu.rs index bf87f113..ee2e65a8 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -68,6 +68,7 @@ pub enum ContextMenuAction { ToggleAntigravityExternal, ToggleStartup, ToggleTaskbarLock, + ToggleShowPace, ToggleWidget, /// Accepted only so menus saved by older versions can be loaded and /// cleaned up. New menus cannot create or execute this legacy action. @@ -325,6 +326,7 @@ pub fn rendered_label( "Check for updates" => language.text("Check for updates"), "Lock in taskbar" => language.text("Lock in taskbar"), "Show widget" => language.text("Show widget"), + "Show pace" => language.text("Show pace"), _ => label, }; crate::theme_engine::format_template(translated, context) @@ -457,6 +459,7 @@ pub fn classic_context_menu() -> ContextMenuDocument { Action::ToggleStartup, ), ContextMenuItem::action("lock-taskbar", "Lock in taskbar", Action::ToggleTaskbarLock), + ContextMenuItem::action("show-pace", "Show pace", Action::ToggleShowPace), ContextMenuItem::submenu("language", "Language", languages), ContextMenuItem::separator("settings-separator"), ContextMenuItem::action( @@ -718,6 +721,19 @@ mod tests { assert!(menu.validate().is_empty()); assert!(menu.items.iter().any(|item| item.id == "refresh")); assert!(menu.items.iter().any(|item| item.id == "update-frequency")); + let settings = menu.items.iter().find(|item| item.id == "settings").unwrap(); + let ContextMenuItemKind::Submenu { items } = &settings.kind else { + panic!("Settings must be a submenu"); + }; + assert!(items.iter().any(|item| { + item.label == "Show pace" + && matches!( + &item.kind, + ContextMenuItemKind::Action { + action: ContextMenuAction::ToggleShowPace + } + ) + })); assert!(serde_json::to_string(&menu) .unwrap() .contains("provider-opencode")); diff --git a/src/localization/locales/de.toml b/src/localization/locales/de.toml index 4550efd7..971127fe 100644 --- a/src/localization/locales/de.toml +++ b/src/localization/locales/de.toml @@ -362,6 +362,7 @@ copilot_window_title = "Copilot-Nutzungsmonitor" "Short duration" = "Kurze Dauer" "Show context menu" = "Kontextmenü anzeigen" "Show dashboard" = "Dashboard anzeigen" +"Show pace" = "Show pace" "Show widget" = "Widget anzeigen" "Size" = "Größe" "Smaller value" = "Kleinerer Wert" diff --git a/src/localization/locales/en.toml b/src/localization/locales/en.toml index 55d05362..7dedde76 100644 --- a/src/localization/locales/en.toml +++ b/src/localization/locales/en.toml @@ -362,6 +362,7 @@ copilot_window_title = "Copilot Usage Monitor" "Short duration" = "Short duration" "Show context menu" = "Show context menu" "Show dashboard" = "Show dashboard" +"Show pace" = "Show pace" "Show widget" = "Show widget" "Size" = "Size" "Smaller value" = "Smaller value" diff --git a/src/localization/locales/es.toml b/src/localization/locales/es.toml index d19c1eb9..c6ce7f76 100644 --- a/src/localization/locales/es.toml +++ b/src/localization/locales/es.toml @@ -362,6 +362,7 @@ copilot_window_title = "Monitor de uso de Copilot" "Short duration" = "Duración corta" "Show context menu" = "Mostrar menú contextual" "Show dashboard" = "Mostrar panel" +"Show pace" = "Show pace" "Show widget" = "Mostrar widget" "Size" = "Tamaño" "Smaller value" = "Valor menor" diff --git a/src/localization/locales/fr.toml b/src/localization/locales/fr.toml index 828af6f6..7fe9771a 100644 --- a/src/localization/locales/fr.toml +++ b/src/localization/locales/fr.toml @@ -362,6 +362,7 @@ copilot_window_title = "Moniteur d'utilisation Copilot" "Short duration" = "Durée courte" "Show context menu" = "Afficher le menu contextuel" "Show dashboard" = "Afficher le tableau de bord" +"Show pace" = "Show pace" "Show widget" = "Afficher le widget" "Size" = "Taille" "Smaller value" = "Valeur la plus petite" diff --git a/src/localization/locales/ja.toml b/src/localization/locales/ja.toml index 86bb6dd0..3fb8ddbe 100644 --- a/src/localization/locales/ja.toml +++ b/src/localization/locales/ja.toml @@ -364,6 +364,7 @@ copilot_window_title = "Copilot 使用量モニター" "Short duration" = "短い期間" "Show context menu" = "コンテキストメニューを表示" "Show dashboard" = "ダッシュボードを表示" +"Show pace" = "Show pace" "Show widget" = "ウィジェットを表示" "Size" = "サイズ" "Smaller value" = "小さい値" diff --git a/src/localization/locales/ko.toml b/src/localization/locales/ko.toml index 03f641dc..5ef89822 100644 --- a/src/localization/locales/ko.toml +++ b/src/localization/locales/ko.toml @@ -364,6 +364,7 @@ copilot_window_title = "Copilot 사용량 모니터" "Short duration" = "간단한 기간" "Show context menu" = "컨텍스트 메뉴 표시" "Show dashboard" = "대시보드 표시" +"Show pace" = "Show pace" "Show widget" = "위젯 표시" "Size" = "크기" "Smaller value" = "더 작은 값" diff --git a/src/localization/locales/nl.toml b/src/localization/locales/nl.toml index 0b049a1f..57a05f38 100644 --- a/src/localization/locales/nl.toml +++ b/src/localization/locales/nl.toml @@ -362,6 +362,7 @@ copilot_window_title = "Copilot-gebruiksmonitor" "Short duration" = "Korte duur" "Show context menu" = "Contextmenu tonen" "Show dashboard" = "Dashboard tonen" +"Show pace" = "Show pace" "Show widget" = "Widget tonen" "Size" = "Grootte" "Smaller value" = "Kleinste waarde" diff --git a/src/localization/locales/pl.toml b/src/localization/locales/pl.toml index 9195b267..92e37d09 100644 --- a/src/localization/locales/pl.toml +++ b/src/localization/locales/pl.toml @@ -362,6 +362,7 @@ copilot_window_title = "Monitor zużycia Copilot" "Short duration" = "Skrócony czas trwania" "Show context menu" = "Pokaż menu kontekstowe" "Show dashboard" = "Pokaż panel" +"Show pace" = "Show pace" "Show widget" = "Pokaż widżet" "Size" = "Rozmiar" "Smaller value" = "Mniejsza z wartości" diff --git a/src/localization/locales/pt-BR.toml b/src/localization/locales/pt-BR.toml index f372f908..4fb1e0a6 100644 --- a/src/localization/locales/pt-BR.toml +++ b/src/localization/locales/pt-BR.toml @@ -362,6 +362,7 @@ copilot_window_title = "Monitor de uso do Copilot" "Short duration" = "Duração curta" "Show context menu" = "Mostrar menu de contexto" "Show dashboard" = "Mostrar painel" +"Show pace" = "Show pace" "Show widget" = "Mostrar widget" "Size" = "Tamanho" "Smaller value" = "Menor valor" diff --git a/src/localization/locales/ru.toml b/src/localization/locales/ru.toml index 977e961a..ae313796 100644 --- a/src/localization/locales/ru.toml +++ b/src/localization/locales/ru.toml @@ -362,6 +362,7 @@ copilot_window_title = "Монитор использования Copilot" "Short duration" = "Краткая длительность" "Show context menu" = "Показать контекстное меню" "Show dashboard" = "Показать панель" +"Show pace" = "Показывать темп" "Show widget" = "Показать виджет" "Size" = "Размер" "Smaller value" = "Меньшее значение" diff --git a/src/localization/locales/th.toml b/src/localization/locales/th.toml index 1de09c42..5bf9465e 100644 --- a/src/localization/locales/th.toml +++ b/src/localization/locales/th.toml @@ -364,6 +364,7 @@ copilot_window_title = "ตัวติดตามการใช้งาน C "Short duration" = "ระยะเวลาแบบสั้น" "Show context menu" = "แสดงเมนูคลิกขวา" "Show dashboard" = "แสดงแดชบอร์ด" +"Show pace" = "Show pace" "Show widget" = "แสดงวิดเจ็ต" "Size" = "ขนาด" "Smaller value" = "ค่าที่น้อยกว่า" diff --git a/src/localization/locales/tr.toml b/src/localization/locales/tr.toml index 6dc7044f..1a82060a 100644 --- a/src/localization/locales/tr.toml +++ b/src/localization/locales/tr.toml @@ -362,6 +362,7 @@ copilot_window_title = "Copilot Kullanım İzleyicisi" "Short duration" = "Kısa süre" "Show context menu" = "Bağlam menüsünü göster" "Show dashboard" = "Panoyu göster" +"Show pace" = "Show pace" "Show widget" = "Pencere öğesini göster" "Size" = "Boyut" "Smaller value" = "Küçük değer" diff --git a/src/localization/locales/zh-CN.toml b/src/localization/locales/zh-CN.toml index d7fc3421..ffae5104 100644 --- a/src/localization/locales/zh-CN.toml +++ b/src/localization/locales/zh-CN.toml @@ -364,6 +364,7 @@ copilot_window_title = "Copilot 使用量监控" "Short duration" = "简短时长" "Show context menu" = "显示上下文菜单" "Show dashboard" = "显示仪表板" +"Show pace" = "Show pace" "Show widget" = "显示小组件" "Size" = "大小" "Smaller value" = "较小的值" diff --git a/src/localization/locales/zh-TW.toml b/src/localization/locales/zh-TW.toml index 0c6eb84a..943ed82c 100644 --- a/src/localization/locales/zh-TW.toml +++ b/src/localization/locales/zh-TW.toml @@ -364,6 +364,7 @@ copilot_window_title = "Copilot 使用量監控" "Short duration" = "簡短時間" "Show context menu" = "顯示快顯功能表" "Show dashboard" = "顯示儀表板" +"Show pace" = "Show pace" "Show widget" = "顯示小工具" "Size" = "大小" "Smaller value" = "較小的值" diff --git a/src/studio_app/studio_context_menu_editor.rs b/src/studio_app/studio_context_menu_editor.rs index f0b588dc..e8cfc9b7 100644 --- a/src/studio_app/studio_context_menu_editor.rs +++ b/src/studio_app/studio_context_menu_editor.rs @@ -325,6 +325,7 @@ pub(super) fn context_menu_action_script(action: &ContextMenuAction) -> String { ContextMenuAction::ToggleAntigravityExternal => "toggle_antigravity_external()".into(), ContextMenuAction::ToggleStartup => "toggle_startup()".into(), ContextMenuAction::ToggleTaskbarLock => "toggle_taskbar_lock()".into(), + ContextMenuAction::ToggleShowPace => "toggle_show_pace()".into(), ContextMenuAction::ToggleWidget => "toggle_widget()".into(), ContextMenuAction::LegacyResetPosition => String::new(), ContextMenuAction::SetLanguage { language } => { @@ -352,6 +353,7 @@ pub(super) fn parse_context_menu_action_script(script: &str) -> Result ContextMenuAction::ToggleWidget, "toggle_startup" => ContextMenuAction::ToggleStartup, "toggle_taskbar_lock" => ContextMenuAction::ToggleTaskbarLock, + "toggle_show_pace" => ContextMenuAction::ToggleShowPace, "check_for_updates" => ContextMenuAction::CheckForUpdates, "exit" => ContextMenuAction::Exit, "set_update_frequency" => { diff --git a/src/studio_app/studio_native_menu.rs b/src/studio_app/studio_native_menu.rs index 2f450c91..647b40ae 100644 --- a/src/studio_app/studio_native_menu.rs +++ b/src/studio_app/studio_native_menu.rs @@ -575,6 +575,7 @@ pub(super) fn context_menu_preview_action_checked( ContextMenuAction::ToggleProvider { provider } => settings.provider_enabled(*provider), ContextMenuAction::ToggleStartup => startup_enabled, ContextMenuAction::ToggleTaskbarLock => settings.lock_taskbar, + ContextMenuAction::ToggleShowPace => settings.show_pace, ContextMenuAction::ToggleWidget => theme .surfaces .iter() diff --git a/src/studio_app/tests.rs b/src/studio_app/tests.rs index 22277ccf..f6a7c1c9 100644 --- a/src/studio_app/tests.rs +++ b/src/studio_app/tests.rs @@ -1016,6 +1016,7 @@ fn context_menu_action_scripts_round_trip_every_action_kind() { }, ContextMenuAction::ToggleStartup, ContextMenuAction::ToggleTaskbarLock, + ContextMenuAction::ToggleShowPace, ContextMenuAction::ToggleWidget, ContextMenuAction::SetLanguage { language: "en-US".into(), diff --git a/src/theme_engine.rs b/src/theme_engine.rs index 2cf14d63..53afc262 100644 --- a/src/theme_engine.rs +++ b/src/theme_engine.rs @@ -1349,6 +1349,7 @@ pub struct ThemeRuntime { /// always means consumption so severity thresholds keep their meaning. pub countdown: bool, pub use_windows_default_font: bool, + pub show_pace: bool, pub surface_nest: SurfaceNest, pub floating_card_opacity: u8, pub antigravity_gemini_enabled: bool, @@ -1367,6 +1368,7 @@ impl Default for ThemeRuntime { language: LanguageId::English, countdown: false, use_windows_default_font: true, + show_pace: true, surface_nest: SurfaceNest::Taskbar, floating_card_opacity: 85, antigravity_gemini_enabled: true, @@ -1402,6 +1404,7 @@ impl ThemeRuntime { language: LanguageId::English, countdown: false, use_windows_default_font: true, + show_pace: true, surface_nest: SurfaceNest::Taskbar, floating_card_opacity: 85, antigravity_gemini_enabled: agy, @@ -1456,6 +1459,11 @@ impl ThemeRuntime { self } + pub fn with_show_pace(mut self, show_pace: bool) -> Self { + self.show_pace = show_pace; + self + } + /// Supply the selected native host's 96-DPI logical dimensions. Theme /// expressions consume these as `host.width` and `host.height`. pub fn with_host_dimensions(mut self, width: u32, height: u32) -> Self { @@ -1477,6 +1485,19 @@ impl ThemeRuntime { } } +const FIVE_HOURS: std::time::Duration = std::time::Duration::from_secs(5 * 3600); +const WEEK: std::time::Duration = std::time::Duration::from_secs(7 * 24 * 3600); + +/// Percent of a window of `length` that has passed by now, given when it +/// resets; `None` when the reset is not within one window from now. +fn expected_used(resets_at: std::time::SystemTime, length: std::time::Duration) -> Option { + let left = resets_at + .duration_since(std::time::SystemTime::now()) + .ok()?; + (!length.is_zero() && left <= length) + .then(|| (1.0 - left.as_secs_f64() / length.as_secs_f64()) * 100.0) +} + impl DataContext { pub fn from_usage(data: Option<&AppUsageData>, canvas: &Canvas) -> Self { Self::from_usage_with_runtime(data, canvas, ThemeRuntime::default()) @@ -1556,6 +1577,7 @@ impl DataContext { ); } context.insert("display.countdown", runtime.countdown as u8 as f64); + context.insert("display.show_pace", runtime.show_pace as u8 as f64); context.insert( "antigravity.gemini.enabled", runtime.antigravity_gemini_enabled as u8 as f64, @@ -1750,6 +1772,26 @@ impl DataContext { display(weekly) }; self.insert(&format!("{name}.weekly.display"), shown_weekly); + // Pace: the share a window would have spent by now at an even rate. + // `expected_display` follows the countdown setting like `display`. + let five_hour_section = usage.filter(|usage| usage.session.available).map(|usage| &usage.session); + let weekly_section = usage.filter(|usage| usage.weekly.available).map(|usage| &usage.weekly); + let session_section = if use_codex_session_fallback { + weekly_section.map(|section| (section, WEEK)) + } else { + five_hour_section.map(|section| (section, FIVE_HOURS)) + }; + for (window, section) in [ + ("session", session_section), + ("five_hour", five_hour_section.map(|section| (section, FIVE_HOURS))), + ("weekly", weekly_section.map(|section| (section, WEEK))), + ] { + let expected = section.and_then(|(section, length)| expected_used(section.resets_at?, length)); + self.insert(&format!("{name}.{window}.expected_available"), expected.is_some() as u8 as f64); + let expected = expected.unwrap_or(0.0); + self.insert(&format!("{name}.{window}.expected"), expected); + self.insert(&format!("{name}.{window}.expected_display"), display(expected)); + } let monthly = usage.and_then(|usage| usage.monthly.as_ref()); self.insert_string(&format!("{name}.monthly.label"), "30d"); if let Some(monthly) = monthly { diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index 73e7c38d..31238935 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -3738,3 +3738,216 @@ fn missing_antigravity_external_pool_draws_an_empty_gauge() { assert_eq!(value("antigravity.gemini.weekly.display"), 0.0); assert_eq!(value("antigravity.external.session.display"), 0.0); } + +#[test] +fn pace_expected_used_rejects_resets_outside_the_window() { + use std::time::{Duration, SystemTime}; + + let now = SystemTime::now(); + for length in [FIVE_HOURS, WEEK] { + let half = expected_used(now + length / 2, length).unwrap(); + assert!((half - 50.0).abs() < 0.1, "{half}"); + assert_eq!(expected_used(SystemTime::UNIX_EPOCH, length), None); + assert_eq!( + expected_used(now + length + Duration::from_secs(60), length), + None + ); + } + assert_eq!(expected_used(now + FIVE_HOURS, Duration::ZERO), None); +} + +#[test] +fn pace_bindings_follow_elapsed_time_and_countdown_for_each_pool() { + use crate::models::{UsageData, UsageSection}; + use std::time::{Duration, SystemTime}; + + let now = SystemTime::now(); + let section = |length: Duration, elapsed_quarters: u32| UsageSection { + available: true, + percentage: 90.0, + resets_at: Some(now + length * (4 - elapsed_quarters) / 4), + }; + for elapsed_quarters in [1, 2] { + let provider_usage = UsageData { + session: section(FIVE_HOURS, elapsed_quarters), + weekly: section(WEEK, elapsed_quarters), + external_pool: Some(Box::new(UsageData { + session: section(FIVE_HOURS, 4 - elapsed_quarters), + weekly: section(WEEK, 4 - elapsed_quarters), + ..Default::default() + })), + ..Default::default() + }; + let usage = AppUsageData::from_iter( + [ProviderId::Claude, ProviderId::Codex, ProviderId::Antigravity] + .into_iter() + .map(|provider| (provider, provider_usage.clone())), + ); + for countdown in [false, true] { + let context = DataContext::from_usage_with_runtime( + Some(&usage), + &Canvas::default(), + ThemeRuntime::default().with_countdown(countdown), + ); + for name in ["claude", "codex", "antigravity.gemini", "antigravity.external"] { + let expected = if name == "antigravity.external" { + (4 - elapsed_quarters) as f64 * 25.0 + } else { + elapsed_quarters as f64 * 25.0 + }; + for window in ["session", "five_hour", "weekly"] { + let base = format!("{name}.{window}"); + assert_eq!( + context.get(&format!("{base}.expected_available")), + Some(1.0) + ); + let used = context.get(&format!("{base}.expected")).unwrap(); + let display = context.get(&format!("{base}.expected_display")).unwrap(); + assert!((used - expected).abs() < 0.1, "{base}: {used}"); + assert!( + (display - if countdown { 100.0 - expected } else { expected }).abs() < 0.1, + "{base}: {display}" + ); + } + } + } + } +} + +#[test] +fn pace_bindings_hide_missing_windows_and_reset_times() { + use crate::models::{UsageData, UsageSection}; + use std::time::{Duration, SystemTime}; + + let now = SystemTime::now(); + for countdown in [false, true] { + for (available, resets_at) in [ + (true, None), + (false, Some(now + Duration::from_secs(60))), + (true, Some(SystemTime::UNIX_EPOCH)), + (true, Some(now + WEEK + Duration::from_secs(60))), + ] { + let section = UsageSection { + available, + percentage: 25.0, + resets_at, + }; + let usage = UsageData { + session: section.clone(), + weekly: section, + ..Default::default() + }; + let mut context = DataContext::default(); + for data in [None, Some(&usage)] { + context.insert_provider("claude", data, false, countdown); + for window in ["session", "five_hour", "weekly"] { + assert_eq!( + context.get(&format!("claude.{window}.expected_available")), + Some(0.0) + ); + assert_eq!( + context.get(&format!("claude.{window}.expected")), + Some(0.0) + ); + } + } + } + } +} + +#[test] +fn show_pace_binding_follows_the_setting() { + let defaults = crate::app_settings::SettingsFile::default(); + assert!(defaults.show_pace); + let context = DataContext::from_usage(None, &Canvas::default()); + assert_eq!(context.get("display.show_pace"), Some(1.0)); + for show_pace in [false, true] { + let mut settings = crate::app_settings::SettingsFile::default(); + settings.show_pace = show_pace; + let runtime = ThemeRuntime::from_providers(settings.enabled_providers()) + .with_show_pace(settings.show_pace); + let context = DataContext::from_usage_with_runtime(None, &Canvas::default(), runtime); + assert_eq!(context.get("display.show_pace"), Some(show_pace as u8 as f64)); + } +} + +#[test] +fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { + let theme: ThemeDocument = + serde_json::from_str(include_str!("../themes/compact-fluent-quad.json")).unwrap(); + assert!(theme.validate().is_empty(), "{:?}", theme.validate()); + let children = &theme.surfaces[0].children; + for window in ["session", "weekly"] { + assert!(children + .iter() + .any(|object| object.id == format!("claude-{window}-segments-dark-pace"))); + } + let ticks = children + .iter() + .enumerate() + .filter(|(_, object)| object.id.ends_with("-pace")) + .collect::>(); + assert_eq!(ticks.len(), 24); + for (index, tick) in ticks { + let gauge = &children[index - 1]; + assert_eq!(tick.id, format!("{}-pace", gauge.id)); + assert!(matches!(&gauge.content, SceneContent::Progress { .. })); + assert!(matches!(&tick.content, SceneContent::None)); + assert_eq!(tick.parent, gauge.parent); + assert_eq!(tick.render.0, gauge.render.0); + let binding = tick + .visibility + .0 + .strip_prefix("(100) * ") + .unwrap() + .strip_suffix(".expected_available * display.show_pace") + .unwrap(); + let LayerBackground::Colour { colour } = &tick.background else { + panic!("{} must have a colour background", tick.id); + }; + assert!(parse_color(&colour.color).is_some()); + let expected_colour = if tick.id.ends_with("-dark-pace") { + "#FFFFFFFF" + } else { + "#1A1A1AFF" + }; + assert_eq!(colour.color, expected_colour); + let mut context = DataContext::from_usage(None, &Canvas::default()); + for count in 1..=ProviderId::ALL.len() { + context.insert("providers.count", count as f64); + let start = evaluate(&gauge.x.0, &context).unwrap(); + let end = start + evaluate(&gauge.width.0, &context).unwrap(); + assert_eq!(evaluate(&tick.width.0, &context).unwrap(), 1.0); + assert_eq!( + evaluate(&tick.y.0, &context).unwrap(), + evaluate(&gauge.y.0, &context).unwrap() - 1.0 + ); + assert_eq!( + evaluate(&tick.height.0, &context).unwrap(), + evaluate(&gauge.height.0, &context).unwrap() + 2.0 + ); + assert_eq!(evaluate(&colour.opacity.0, &context).unwrap(), 0.9); + for available in [0.0, 1.0] { + context.insert(&format!("{binding}.expected_available"), available); + for show_pace in [0.0, 1.0] { + context.insert("display.show_pace", show_pace); + assert_eq!( + evaluate(&tick.visibility.0, &context).unwrap(), + available * show_pace * 100.0 + ); + } + } + for expected in [-10.0, 0.0, 0.1, 25.0, 50.0, 99.9, 100.0, 110.0] { + context.insert(&format!("{binding}.expected_display"), expected); + let x = evaluate(&tick.x.0, &context).unwrap(); + assert!(x >= start && x + 1.0 <= end, "{}: {x}", tick.id); + assert_eq!(x.fract(), 0.0); + if expected <= 0.0 { + assert_eq!(x, start); + } else if expected >= 100.0 { + assert_eq!(x + 1.0, end); + } + } + } + } +} diff --git a/src/themes/compact-fluent-quad.json b/src/themes/compact-fluent-quad.json index 0a5a9e2e..859bbc0a 100644 --- a/src/themes/compact-fluent-quad.json +++ b/src/themes/compact-fluent-quad.json @@ -405,6 +405,32 @@ "segment_gap": "1" } }, + { + "id": "claude-session-segments-dark-pace", + "name": "dark Claude Code 5-hour segments pace tick", + "render": "system.dark", + "visibility": "(100) * claude.session.expected_available * display.show_pace", + "parent": "claude-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.session.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "claude-session-value-dark", "name": "dark Claude Code 5-hour value", @@ -473,6 +499,32 @@ "segment_gap": "1" } }, + { + "id": "claude-session-segments-light-pace", + "name": "light Claude Code 5-hour segments pace tick", + "render": "1 - system.dark", + "visibility": "(100) * claude.session.expected_available * display.show_pace", + "parent": "claude-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.session.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "claude-session-value-light", "name": "light Claude Code 5-hour value", @@ -541,6 +593,32 @@ "segment_gap": "1" } }, + { + "id": "claude-weekly-segments-dark-pace", + "name": "dark Claude Code 7-day segments pace tick", + "render": "system.dark", + "visibility": "(100) * claude.weekly.expected_available * display.show_pace", + "parent": "claude-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "claude-weekly-value-dark", "name": "dark Claude Code 7-day value", @@ -609,6 +687,32 @@ "segment_gap": "1" } }, + { + "id": "claude-weekly-segments-light-pace", + "name": "light Claude Code 7-day segments pace tick", + "render": "1 - system.dark", + "visibility": "(100) * claude.weekly.expected_available * display.show_pace", + "parent": "claude-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "claude-weekly-value-light", "name": "light Claude Code 7-day value", @@ -851,6 +955,32 @@ "segment_gap": "1" } }, + { + "id": "codex-session-segments-dark-pace", + "name": "dark Codex 5-hour segments pace tick", + "render": "system.dark", + "visibility": "(100) * codex.five_hour.expected_available * display.show_pace", + "parent": "codex-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(codex.five_hour.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-session-value-dark", "name": "dark Codex 5-hour value", @@ -919,6 +1049,32 @@ "segment_gap": "1" } }, + { + "id": "codex-session-segments-light-pace", + "name": "light Codex 5-hour segments pace tick", + "render": "1 - system.dark", + "visibility": "(100) * codex.five_hour.expected_available * display.show_pace", + "parent": "codex-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(codex.five_hour.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-session-value-light", "name": "light Codex 5-hour value", @@ -987,6 +1143,32 @@ "segment_gap": "1" } }, + { + "id": "codex-weekly-segments-dark-pace", + "name": "dark Codex 7-day segments pace tick", + "render": "system.dark", + "visibility": "(100) * codex.weekly.expected_available * display.show_pace", + "parent": "codex-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(codex.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-weekly-value-dark", "name": "dark Codex 7-day value", @@ -1055,6 +1237,32 @@ "segment_gap": "1" } }, + { + "id": "codex-weekly-segments-light-pace", + "name": "light Codex 7-day segments pace tick", + "render": "1 - system.dark", + "visibility": "(100) * codex.weekly.expected_available * display.show_pace", + "parent": "codex-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(codex.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-weekly-value-light", "name": "light Codex 7-day value", @@ -1143,6 +1351,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-session-gemini-solo-dark-pace", + "name": "dark Antigravity Gemini solo session segments pace tick", + "render": "system.dark && antigravity.gemini.enabled && !antigravity.dual_mode", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-session-gemini-solo-light", "name": "light Antigravity Gemini solo session segments", @@ -1178,6 +1412,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-session-gemini-solo-light-pace", + "name": "light Antigravity Gemini solo session segments pace tick", + "render": "(1 - system.dark) && antigravity.gemini.enabled && !antigravity.dual_mode", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-session-external-solo-dark", "name": "dark Antigravity External solo session segments", @@ -1213,6 +1473,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-session-external-solo-dark-pace", + "name": "dark Antigravity External solo session segments pace tick", + "render": "system.dark && antigravity.external.enabled && !antigravity.dual_mode", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-session-external-solo-light", "name": "light Antigravity External solo session segments", @@ -1248,6 +1534,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-session-external-solo-light-pace", + "name": "light Antigravity External solo session segments pace tick", + "render": "(1 - system.dark) && antigravity.external.enabled && !antigravity.dual_mode", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-session-gemini-dual-dark", "name": "dark Antigravity Gemini dual session segments", @@ -1283,6 +1595,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-session-gemini-dual-dark-pace", + "name": "dark Antigravity Gemini dual session segments pace tick", + "render": "system.dark && antigravity.dual_mode", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(4) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-session-gemini-dual-light", "name": "light Antigravity Gemini dual session segments", @@ -1318,6 +1656,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-session-gemini-dual-light-pace", + "name": "light Antigravity Gemini dual session segments pace tick", + "render": "(1 - system.dark) && antigravity.dual_mode", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", + "y": "(11) - 1", + "width": "1", + "height": "(4) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-session-external-dual-dark", "name": "dark Antigravity External dual session segments", @@ -1353,6 +1717,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-session-external-dual-dark-pace", + "name": "dark Antigravity External dual session segments pace tick", + "render": "system.dark && antigravity.dual_mode", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", + "y": "(16) - 1", + "width": "1", + "height": "(4) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-session-external-dual-light", "name": "light Antigravity External dual session segments", @@ -1388,6 +1778,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-session-external-dual-light-pace", + "name": "light Antigravity External dual session segments pace tick", + "render": "(1 - system.dark) && antigravity.dual_mode", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", + "y": "(16) - 1", + "width": "1", + "height": "(4) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-session-gemini-value-dark", "name": "dark Antigravity Gemini session value", @@ -1555,6 +1971,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-weekly-gemini-solo-dark-pace", + "name": "dark Antigravity Gemini solo weekly segments pace tick", + "render": "system.dark && antigravity.gemini.enabled && !antigravity.dual_mode", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-weekly-gemini-solo-light", "name": "light Antigravity Gemini solo weekly segments", @@ -1590,6 +2032,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-weekly-gemini-solo-light-pace", + "name": "light Antigravity Gemini solo weekly segments pace tick", + "render": "(1 - system.dark) && antigravity.gemini.enabled && !antigravity.dual_mode", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-weekly-external-solo-dark", "name": "dark Antigravity External solo weekly segments", @@ -1625,6 +2093,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-weekly-external-solo-dark-pace", + "name": "dark Antigravity External solo weekly segments pace tick", + "render": "system.dark && antigravity.external.enabled && !antigravity.dual_mode", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-weekly-external-solo-light", "name": "light Antigravity External solo weekly segments", @@ -1660,6 +2154,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-weekly-external-solo-light-pace", + "name": "light Antigravity External solo weekly segments pace tick", + "render": "(1 - system.dark) && antigravity.external.enabled && !antigravity.dual_mode", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-weekly-gemini-dual-dark", "name": "dark Antigravity Gemini dual weekly segments", @@ -1695,6 +2215,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-weekly-gemini-dual-dark-pace", + "name": "dark Antigravity Gemini dual weekly segments pace tick", + "render": "system.dark && antigravity.dual_mode", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(4) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-weekly-gemini-dual-light", "name": "light Antigravity Gemini dual weekly segments", @@ -1730,6 +2276,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-weekly-gemini-dual-light-pace", + "name": "light Antigravity Gemini dual weekly segments pace tick", + "render": "(1 - system.dark) && antigravity.dual_mode", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(4) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-weekly-external-dual-dark", "name": "dark Antigravity External dual weekly segments", @@ -1765,6 +2337,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-weekly-external-dual-dark-pace", + "name": "dark Antigravity External dual weekly segments pace tick", + "render": "system.dark && antigravity.dual_mode", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", + "y": "(32) - 1", + "width": "1", + "height": "(4) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-weekly-external-dual-light", "name": "light Antigravity External dual weekly segments", @@ -1800,6 +2398,32 @@ "segment_gap": "1" } }, + { + "id": "antigravity-weekly-external-dual-light-pace", + "name": "light Antigravity External dual weekly segments pace tick", + "render": "(1 - system.dark) && antigravity.dual_mode", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", + "y": "(32) - 1", + "width": "1", + "height": "(4) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "antigravity-weekly-gemini-value-dark", "name": "dark Antigravity Gemini weekly value", diff --git a/src/window.rs b/src/window.rs index 9099d1a1..ed24961b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -139,6 +139,7 @@ struct AppState { custom_theme_enabled: bool, usage_countdown: bool, use_windows_default_font: bool, + show_pace: bool, active_theme_path: Option, active_theme: Option, theme_clock_interval: Option, @@ -209,6 +210,7 @@ const IDM_FREQ_15MIN: u16 = 12; const IDM_FREQ_1HOUR: u16 = 13; const IDM_START_WITH_WINDOWS: u16 = 20; const IDM_LOCK_TASKBAR: u16 = 21; +const IDM_SHOW_PACE: u16 = 22; const IDM_VERSION_ACTION: u16 = 31; const IDM_LANG_SYSTEM: u16 = 100; const IDM_LANG_FIRST: u16 = 101; @@ -795,6 +797,7 @@ fn theme_runtime_from_state(state: &AppState) -> ThemeRuntime { .with_language(state.language) .with_countdown(state.usage_countdown) .with_windows_default_font(state.use_windows_default_font) + .with_show_pace(state.show_pace) .with_nest(nest) .with_floating_card_opacity(opacity) .with_antigravity_pools( @@ -1032,6 +1035,7 @@ fn save_state_settings() { persisted.show_antigravity_gemini = Some(antigravity_on && s.show_antigravity_gemini); persisted.show_antigravity_external = Some(antigravity_on && s.show_antigravity_external); persisted.custom_theme_enabled = s.custom_theme_enabled; + persisted.show_pace = s.show_pace; persisted.active_theme_path = s .active_theme_path .as_ref() @@ -2624,6 +2628,7 @@ pub fn run() { custom_theme_enabled, usage_countdown: settings.usage_countdown, use_windows_default_font: settings.use_windows_default_font, + show_pace: settings.show_pace, active_theme_path, active_theme, theme_clock_interval, @@ -3444,6 +3449,7 @@ fn reload_external_settings(hwnd: HWND) { state.providers = settings.enabled_providers(); state.usage_countdown = settings.usage_countdown; state.use_windows_default_font = settings.use_windows_default_font; + state.show_pace = settings.show_pace; dual_mode = state.show_antigravity_gemini && state.show_antigravity_external && state.providers.contains(ProviderId::Antigravity); diff --git a/src/window/message_loop.rs b/src/window/message_loop.rs index d49015dd..7a21f3da 100644 --- a/src/window/message_loop.rs +++ b/src/window/message_loop.rs @@ -784,6 +784,17 @@ pub(super) unsafe extern "system" fn wnd_proc( IDM_LOCK_TASKBAR => { toggle_taskbar_lock(hwnd); } + IDM_SHOW_PACE => { + { + let mut state = lock_state(); + if let Some(s) = state.as_mut() { + s.show_pace = !s.show_pace; + } + } + save_state_settings(); + render_layered(); + sync_tray_icon(hwnd); + } IDM_FREQ_1MIN | IDM_FREQ_5MIN | IDM_FREQ_15MIN | IDM_FREQ_1HOUR => { let new_interval = match id { IDM_FREQ_1MIN => POLL_1_MIN, diff --git a/src/window/placement_regression_tests.rs b/src/window/placement_regression_tests.rs index 5e7dec5d..f03664af 100644 --- a/src/window/placement_regression_tests.rs +++ b/src/window/placement_regression_tests.rs @@ -47,6 +47,7 @@ pub(super) fn state_for(theme: ThemeDocument, placement: PlacementOverride) -> A custom_theme_enabled: true, usage_countdown: false, use_windows_default_font: true, + show_pace: true, active_theme_path: None, active_theme: Some(theme), theme_clock_interval: None, diff --git a/src/window/window_context_menu.rs b/src/window/window_context_menu.rs index f92f1213..8236a49a 100644 --- a/src/window/window_context_menu.rs +++ b/src/window/window_context_menu.rs @@ -149,6 +149,7 @@ pub(super) fn context_menu_action_flags( ContextMenuAction::ToggleAntigravityExternal => state.show_antigravity_external, ContextMenuAction::ToggleStartup => is_startup_enabled(), ContextMenuAction::ToggleTaskbarLock => state.lock_taskbar, + ContextMenuAction::ToggleShowPace => state.show_pace, ContextMenuAction::ToggleWidget => state .active_theme .as_ref() @@ -273,6 +274,7 @@ pub(super) fn execute_context_menu_action( ContextMenuAction::ToggleAntigravityExternal => Some(IDM_PROVIDER_ANTIGRAVITY_EXTERNAL), ContextMenuAction::ToggleStartup => Some(IDM_START_WITH_WINDOWS), ContextMenuAction::ToggleTaskbarLock => Some(IDM_LOCK_TASKBAR), + ContextMenuAction::ToggleShowPace => Some(IDM_SHOW_PACE), ContextMenuAction::SetLanguage { language } => { if language.eq_ignore_ascii_case("system") { Some(IDM_LANG_SYSTEM) From 2a097c1fb75a33ac109d3f5e0589533adb7c9673 Mon Sep 17 00:00:00 2001 From: fix2it Date: Mon, 19 Oct 2026 13:40:38 +0300 Subject: [PATCH 02/17] fix(compact): keep the pace tick when a reset is reported slightly late Allow a minute of slack past the window length so poll latency right after a reset does not hide the tick; it sits at 0 instead. Co-Authored-By: Claude Opus 5.5 --- src/theme_engine.rs | 6 ++++-- src/theme_engine/tests.rs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/theme_engine.rs b/src/theme_engine.rs index 53afc262..d1744c03 100644 --- a/src/theme_engine.rs +++ b/src/theme_engine.rs @@ -1494,8 +1494,10 @@ fn expected_used(resets_at: std::time::SystemTime, length: std::time::Duration) let left = resets_at .duration_since(std::time::SystemTime::now()) .ok()?; - (!length.is_zero() && left <= length) - .then(|| (1.0 - left.as_secs_f64() / length.as_secs_f64()) * 100.0) + // A minute of slack for poll latency right after a reset. + (!length.is_zero() && left <= length + std::time::Duration::from_secs(60)).then(|| { + (1.0 - left.min(length).as_secs_f64() / length.as_secs_f64()) * 100.0 + }) } impl DataContext { diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index 31238935..e209880a 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -3748,8 +3748,10 @@ fn pace_expected_used_rejects_resets_outside_the_window() { let half = expected_used(now + length / 2, length).unwrap(); assert!((half - 50.0).abs() < 0.1, "{half}"); assert_eq!(expected_used(SystemTime::UNIX_EPOCH, length), None); + // A reset reported slightly past the window start still ticks at 0. + assert_eq!(expected_used(now + length + Duration::from_secs(30), length), Some(0.0)); assert_eq!( - expected_used(now + length + Duration::from_secs(60), length), + expected_used(now + length + Duration::from_secs(300), length), None ); } @@ -3825,7 +3827,7 @@ fn pace_bindings_hide_missing_windows_and_reset_times() { (true, None), (false, Some(now + Duration::from_secs(60))), (true, Some(SystemTime::UNIX_EPOCH)), - (true, Some(now + WEEK + Duration::from_secs(60))), + (true, Some(now + WEEK + Duration::from_secs(300))), ] { let section = UsageSection { available, From 4e01052f5c0bffe7dc743d738ce3e3f4d5bbe558 Mon Sep 17 00:00:00 2001 From: fix2it Date: Mon, 26 Oct 2026 01:08:38 +0200 Subject: [PATCH 03/17] style: cargo fmt --- src/context_menu.rs | 6 +++++- src/theme_engine.rs | 31 ++++++++++++++++++++--------- src/theme_engine/tests.rs | 41 ++++++++++++++++++++++++++++----------- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/context_menu.rs b/src/context_menu.rs index ee2e65a8..26130350 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -721,7 +721,11 @@ mod tests { assert!(menu.validate().is_empty()); assert!(menu.items.iter().any(|item| item.id == "refresh")); assert!(menu.items.iter().any(|item| item.id == "update-frequency")); - let settings = menu.items.iter().find(|item| item.id == "settings").unwrap(); + let settings = menu + .items + .iter() + .find(|item| item.id == "settings") + .unwrap(); let ContextMenuItemKind::Submenu { items } = &settings.kind else { panic!("Settings must be a submenu"); }; diff --git a/src/theme_engine.rs b/src/theme_engine.rs index d1744c03..826a04c2 100644 --- a/src/theme_engine.rs +++ b/src/theme_engine.rs @@ -1495,9 +1495,8 @@ fn expected_used(resets_at: std::time::SystemTime, length: std::time::Duration) .duration_since(std::time::SystemTime::now()) .ok()?; // A minute of slack for poll latency right after a reset. - (!length.is_zero() && left <= length + std::time::Duration::from_secs(60)).then(|| { - (1.0 - left.min(length).as_secs_f64() / length.as_secs_f64()) * 100.0 - }) + (!length.is_zero() && left <= length + std::time::Duration::from_secs(60)) + .then(|| (1.0 - left.min(length).as_secs_f64() / length.as_secs_f64()) * 100.0) } impl DataContext { @@ -1776,8 +1775,12 @@ impl DataContext { self.insert(&format!("{name}.weekly.display"), shown_weekly); // Pace: the share a window would have spent by now at an even rate. // `expected_display` follows the countdown setting like `display`. - let five_hour_section = usage.filter(|usage| usage.session.available).map(|usage| &usage.session); - let weekly_section = usage.filter(|usage| usage.weekly.available).map(|usage| &usage.weekly); + let five_hour_section = usage + .filter(|usage| usage.session.available) + .map(|usage| &usage.session); + let weekly_section = usage + .filter(|usage| usage.weekly.available) + .map(|usage| &usage.weekly); let session_section = if use_codex_session_fallback { weekly_section.map(|section| (section, WEEK)) } else { @@ -1785,14 +1788,24 @@ impl DataContext { }; for (window, section) in [ ("session", session_section), - ("five_hour", five_hour_section.map(|section| (section, FIVE_HOURS))), + ( + "five_hour", + five_hour_section.map(|section| (section, FIVE_HOURS)), + ), ("weekly", weekly_section.map(|section| (section, WEEK))), ] { - let expected = section.and_then(|(section, length)| expected_used(section.resets_at?, length)); - self.insert(&format!("{name}.{window}.expected_available"), expected.is_some() as u8 as f64); + let expected = + section.and_then(|(section, length)| expected_used(section.resets_at?, length)); + self.insert( + &format!("{name}.{window}.expected_available"), + expected.is_some() as u8 as f64, + ); let expected = expected.unwrap_or(0.0); self.insert(&format!("{name}.{window}.expected"), expected); - self.insert(&format!("{name}.{window}.expected_display"), display(expected)); + self.insert( + &format!("{name}.{window}.expected_display"), + display(expected), + ); } let monthly = usage.and_then(|usage| usage.monthly.as_ref()); self.insert_string(&format!("{name}.monthly.label"), "30d"); diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index e209880a..16bc045f 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -3749,7 +3749,10 @@ fn pace_expected_used_rejects_resets_outside_the_window() { assert!((half - 50.0).abs() < 0.1, "{half}"); assert_eq!(expected_used(SystemTime::UNIX_EPOCH, length), None); // A reset reported slightly past the window start still ticks at 0. - assert_eq!(expected_used(now + length + Duration::from_secs(30), length), Some(0.0)); + assert_eq!( + expected_used(now + length + Duration::from_secs(30), length), + Some(0.0) + ); assert_eq!( expected_used(now + length + Duration::from_secs(300), length), None @@ -3781,9 +3784,13 @@ fn pace_bindings_follow_elapsed_time_and_countdown_for_each_pool() { ..Default::default() }; let usage = AppUsageData::from_iter( - [ProviderId::Claude, ProviderId::Codex, ProviderId::Antigravity] - .into_iter() - .map(|provider| (provider, provider_usage.clone())), + [ + ProviderId::Claude, + ProviderId::Codex, + ProviderId::Antigravity, + ] + .into_iter() + .map(|provider| (provider, provider_usage.clone())), ); for countdown in [false, true] { let context = DataContext::from_usage_with_runtime( @@ -3791,7 +3798,12 @@ fn pace_bindings_follow_elapsed_time_and_countdown_for_each_pool() { &Canvas::default(), ThemeRuntime::default().with_countdown(countdown), ); - for name in ["claude", "codex", "antigravity.gemini", "antigravity.external"] { + for name in [ + "claude", + "codex", + "antigravity.gemini", + "antigravity.external", + ] { let expected = if name == "antigravity.external" { (4 - elapsed_quarters) as f64 * 25.0 } else { @@ -3807,7 +3819,14 @@ fn pace_bindings_follow_elapsed_time_and_countdown_for_each_pool() { let display = context.get(&format!("{base}.expected_display")).unwrap(); assert!((used - expected).abs() < 0.1, "{base}: {used}"); assert!( - (display - if countdown { 100.0 - expected } else { expected }).abs() < 0.1, + (display + - if countdown { + 100.0 - expected + } else { + expected + }) + .abs() + < 0.1, "{base}: {display}" ); } @@ -3847,10 +3866,7 @@ fn pace_bindings_hide_missing_windows_and_reset_times() { context.get(&format!("claude.{window}.expected_available")), Some(0.0) ); - assert_eq!( - context.get(&format!("claude.{window}.expected")), - Some(0.0) - ); + assert_eq!(context.get(&format!("claude.{window}.expected")), Some(0.0)); } } } @@ -3869,7 +3885,10 @@ fn show_pace_binding_follows_the_setting() { let runtime = ThemeRuntime::from_providers(settings.enabled_providers()) .with_show_pace(settings.show_pace); let context = DataContext::from_usage_with_runtime(None, &Canvas::default(), runtime); - assert_eq!(context.get("display.show_pace"), Some(show_pace as u8 as f64)); + assert_eq!( + context.get("display.show_pace"), + Some(show_pace as u8 as f64) + ); } } From 714941fd459f08b0b359cb63793d09eab4407aca Mon Sep 17 00:00:00 2001 From: fix2it Date: Tue, 3 Nov 2026 10:55:59 +0200 Subject: [PATCH 04/17] fix(compact): guard session pace ticks with claude.session.available --- src/theme_engine/tests.rs | 3 +++ src/themes/compact-fluent-quad.json | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index 16bc045f..fd57ffa9 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -920,6 +920,9 @@ fn built_in_themes_hide_absent_claude_sessions_and_keep_weekly_and_fable_limits( let mut weekly_layers = 0; let mut fable_layers = 0; for object in &theme.surfaces[0].children { + if object.id.ends_with("-pace") { + continue; + } let visible = evaluate(&object.render.0, &context).unwrap() != 0.0; if object.id.starts_with("claude-session-") { if !session_available { diff --git a/src/themes/compact-fluent-quad.json b/src/themes/compact-fluent-quad.json index 859bbc0a..a25a69b4 100644 --- a/src/themes/compact-fluent-quad.json +++ b/src/themes/compact-fluent-quad.json @@ -408,7 +408,7 @@ { "id": "claude-session-segments-dark-pace", "name": "dark Claude Code 5-hour segments pace tick", - "render": "system.dark", + "render": "(system.dark) && claude.session.available", "visibility": "(100) * claude.session.expected_available * display.show_pace", "parent": "claude-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.session.expected_display, 0, 100) / 100)", @@ -502,7 +502,7 @@ { "id": "claude-session-segments-light-pace", "name": "light Claude Code 5-hour segments pace tick", - "render": "1 - system.dark", + "render": "(1 - system.dark) && claude.session.available", "visibility": "(100) * claude.session.expected_available * display.show_pace", "parent": "claude-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.session.expected_display, 0, 100) / 100)", From e6714ad9889311f02fbbef67d21905e296809684 Mon Sep 17 00:00:00 2001 From: fix2it Date: Wed, 21 Oct 2026 19:14:39 +0300 Subject: [PATCH 05/17] feat(antigravity): reimagined pool alternating as on-demand hover crossfade Replaces the background 8s / 2s cyclical timer between Antigravity's Gemini and Claude/GPT pools with an interactive, smooth hover crossfade on the taskbar tile. - Show primary pool at rest (Gemini in dual-mode, or the enabled solo pool). - Smoothly crossfade to the secondary pool over 180 ms while hovered. - Continuous time-based animation (CrossfadeAnimation) for fluid 60 FPS transitions. - O(1) cached tile bounds hit-testing in WM_MOUSEMOVE with auto-invalidation on theme reload, DPI change, or provider count updates. - Remove obsolete periodic timer machinery and dual_mode gate leftovers. - Add comprehensive unit tests for dual and single pool crossfade modes. --- CHANGELOG.md | 7 + src/theme_engine.rs | 28 ++- src/theme_engine/tests.rs | 102 ++++++++++- src/theme_engine/theme_rendering.rs | 29 +++ src/themes/compact-fluent-quad.json | 96 +++++----- src/window.rs | 221 ++++++++++++++++------- src/window/message_loop.rs | 32 ++-- src/window/mouse.rs | 29 ++- src/window/placement_regression_tests.rs | 3 + 9 files changed, 401 insertions(+), 146 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 737ab0ad..14e9350e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Notable changes to Claude Code Usage Monitor are documented here, newest first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), with changes grouped into Added, Changed, Fixed, and Removed where applicable. +## [Unreleased] + +### Changed + +- Show Antigravity's Gemini usage labels at rest and reveal the Claude/GPT labels while the cursor is over the Antigravity tile, replacing the timed 8 s / 2 s alternation. +- With only one Antigravity pool enabled, hovering the Antigravity tile crossfades to the other pool's labels and gauges, the same way as with both pools enabled. + ## [2.23.0] - 2026-11-03 ### Added diff --git a/src/theme_engine.rs b/src/theme_engine.rs index 826a04c2..179dd529 100644 --- a/src/theme_engine.rs +++ b/src/theme_engine.rs @@ -1590,10 +1590,30 @@ impl DataContext { let agy_dual_mode = runtime.antigravity_gemini_enabled && runtime.antigravity_external_enabled; context.insert("antigravity.dual_mode", agy_dual_mode as u8 as f64); - context.insert( - "antigravity.crossfade_opacity", - runtime.crossfade_opacity.clamp(0.0, 1.0), - ); + let crossfade = runtime.crossfade_opacity.clamp(0.0, 1.0); + context.insert("antigravity.crossfade_opacity", crossfade); + let gemini_opacity = if crossfade > 0.5 { + (crossfade - 0.5) * 2.0 + } else { + 0.0 + }; + let external_opacity = if crossfade < 0.5 { + (0.5 - crossfade) * 2.0 + } else { + 0.0 + }; + // crossfade 1.0 shows the primary pool: Gemini in dual mode, otherwise + // the single enabled pool; hover fades toward the other one. + let (gemini_opacity, external_opacity) = match ( + runtime.antigravity_gemini_enabled, + runtime.antigravity_external_enabled, + ) { + (false, true) => (external_opacity, gemini_opacity), + (false, false) => (0.0, 0.0), + _ => (gemini_opacity, external_opacity), + }; + context.insert("antigravity.gemini.opacity", gemini_opacity); + context.insert("antigravity.external.opacity", external_opacity); if let Some(data) = data { for account in &data.accounts { let key = format!( diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index fd57ffa9..ecc29576 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -3615,6 +3615,49 @@ fn compact_fluent_quad_antigravity_dual_and_solo_modes() { assert!(!find_render("antigravity-session-external-value-dark")); } + // 1b. Solo Gemini while hovered: the hidden external pool fades in + { + let runtime = + ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) + .with_antigravity_pools(true, false, 0.0); + let mut context = + DataContext::from_usage_with_runtime(Some(&usage), &Canvas::default(), runtime); + context.insert("system.dark", 1.0); + let find_render = |id: &str| { + theme.surfaces[0] + .children + .iter() + .find(|o| o.id == id) + .map(|o| evaluate(&o.render.0, &context).unwrap() != 0.0) + .unwrap_or(false) + }; + assert!(!find_render("antigravity-session-gemini-solo-dark")); + assert!(find_render("antigravity-session-external-solo-dark")); + assert!(!find_render("antigravity-session-external-dual-dark")); + assert!(find_render("antigravity-session-external-value-dark")); + assert_eq!( + evaluate("antigravity.external.opacity", &context).unwrap(), + 1.0 + ); + } + + // 1c. Solo External while hovered: Gemini fades in + { + let runtime = + ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) + .with_antigravity_pools(false, true, 0.0); + let context = + DataContext::from_usage_with_runtime(Some(&usage), &Canvas::default(), runtime); + assert_eq!( + evaluate("antigravity.gemini.opacity", &context).unwrap(), + 1.0 + ); + assert_eq!( + evaluate("antigravity.external.opacity", &context).unwrap(), + 0.0 + ); + } + // 2. Solo External { let runtime = @@ -3677,7 +3720,28 @@ fn compact_fluent_quad_antigravity_dual_and_solo_modes() { assert!(find_render("antigravity-session-gemini-value-dark")); assert!(!find_render("antigravity-session-external-value-dark")); - // At opacity 0.5 (Mid-transition) + // Sequential cross-dissolve: at 0.75 Gemini is fading down, External not yet visible + let runtime_gemini_fade = + ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) + .with_antigravity_pools(true, true, 0.75); + let mut context_gemini_fade = DataContext::from_usage_with_runtime( + Some(&usage), + &Canvas::default(), + runtime_gemini_fade, + ); + context_gemini_fade.insert("system.dark", 1.0); + let find_render_gf = |id: &str| { + theme.surfaces[0] + .children + .iter() + .find(|o| o.id == id) + .map(|o| evaluate(&o.render.0, &context_gemini_fade).unwrap() != 0.0) + .unwrap_or(false) + }; + assert!(find_render_gf("antigravity-session-gemini-value-dark")); + assert!(!find_render_gf("antigravity-session-external-value-dark")); + + // At opacity 0.5 (Mid-transition clean dip: zero overlap to avoid glyph jitter) let runtime_mid = ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) .with_antigravity_pools(true, true, 0.5); @@ -3692,8 +3756,29 @@ fn compact_fluent_quad_antigravity_dual_and_solo_modes() { .map(|o| evaluate(&o.render.0, &context_mid).unwrap() != 0.0) .unwrap_or(false) }; - assert!(find_render_mid("antigravity-session-gemini-value-dark")); - assert!(find_render_mid("antigravity-session-external-value-dark")); + assert!(!find_render_mid("antigravity-session-gemini-value-dark")); + assert!(!find_render_mid("antigravity-session-external-value-dark")); + + // At 0.25 External is fading up, Gemini is hidden + let runtime_ext_fade = + ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) + .with_antigravity_pools(true, true, 0.25); + let mut context_ext_fade = DataContext::from_usage_with_runtime( + Some(&usage), + &Canvas::default(), + runtime_ext_fade, + ); + context_ext_fade.insert("system.dark", 1.0); + let find_render_ef = |id: &str| { + theme.surfaces[0] + .children + .iter() + .find(|o| o.id == id) + .map(|o| evaluate(&o.render.0, &context_ext_fade).unwrap() != 0.0) + .unwrap_or(false) + }; + assert!(!find_render_ef("antigravity-session-gemini-value-dark")); + assert!(find_render_ef("antigravity-session-external-value-dark")); // At opacity 0.0 (External holding) let runtime_ext = @@ -3919,11 +4004,16 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { assert!(matches!(&tick.content, SceneContent::None)); assert_eq!(tick.parent, gauge.parent); assert_eq!(tick.render.0, gauge.render.0); - let binding = tick + let stripped = tick .visibility .0 .strip_prefix("(100) * ") - .unwrap() + .unwrap(); + let stripped = stripped + .strip_suffix(" * antigravity.gemini.opacity") + .or_else(|| stripped.strip_suffix(" * antigravity.external.opacity")) + .unwrap_or(stripped); + let binding = stripped .strip_suffix(".expected_available * display.show_pace") .unwrap(); let LayerBackground::Colour { colour } = &tick.background else { @@ -3937,6 +4027,8 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { }; assert_eq!(colour.color, expected_colour); let mut context = DataContext::from_usage(None, &Canvas::default()); + context.insert("antigravity.gemini.opacity", 1.0); + context.insert("antigravity.external.opacity", 1.0); for count in 1..=ProviderId::ALL.len() { context.insert("providers.count", count as f64); let start = evaluate(&gauge.x.0, &context).unwrap(); diff --git a/src/theme_engine/theme_rendering.rs b/src/theme_engine/theme_rendering.rs index 7d7bea00..21da12fb 100644 --- a/src/theme_engine/theme_rendering.rs +++ b/src/theme_engine/theme_rendering.rs @@ -359,6 +359,35 @@ pub(super) fn resolve_objects_for<'a>( /// Return the topmost interactive layer at a logical canvas point. Non- /// interactive layers intentionally do not block layers beneath them, which /// lets visual decoration sit above a larger deliberate hit area. +/// Returns the rendered bounding box `(x, y, width, height)` of `object_id` on the given surface +/// in canvas logical coordinates (including horizontal surface padding), or `None` if not rendered. +pub fn object_bounds( + theme: &ThemeDocument, + surface_index: usize, + data: Option<&AppUsageData>, + runtime: ThemeRuntime, + object_id: &str, +) -> Option<(f64, f64, f64, f64)> { + let surface = theme.surfaces.get(surface_index)?; + if !surface_should_render(theme, surface_index, data, runtime) { + return None; + } + let (width, height) = resolve_object_size(surface, data, runtime, &mut Vec::new()); + let padding = surface_horizontal_padding(theme, surface_index, runtime) as f64; + let canvas = Canvas { + width, + width_expression: Some(surface.width.clone()), + height, + height_expression: Some(surface.height.clone()), + background: surface.background.canvas_paint(), + }; + let (resolved, _) = resolve_objects_for(surface, &canvas, &surface.children, data, runtime); + resolved + .into_iter() + .find(|object| object.source.id == object_id && object.width > 0.0 && object.height > 0.0) + .map(|object| (object.x + padding, object.y, object.width, object.height)) +} + pub fn hit_test_mouse_event( theme: &ThemeDocument, surface_index: usize, diff --git a/src/themes/compact-fluent-quad.json b/src/themes/compact-fluent-quad.json index a25a69b4..f9bebe8d 100644 --- a/src/themes/compact-fluent-quad.json +++ b/src/themes/compact-fluent-quad.json @@ -1319,8 +1319,8 @@ { "id": "antigravity-session-gemini-solo-dark", "name": "dark Antigravity Gemini solo session segments", - "render": "system.dark && antigravity.gemini.enabled && !antigravity.dual_mode", - "visibility": "100", + "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "100 * antigravity.gemini.opacity", "parent": "antigravity-provider", "y": "11", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", @@ -1354,8 +1354,8 @@ { "id": "antigravity-session-gemini-solo-dark-pace", "name": "dark Antigravity Gemini solo session segments pace tick", - "render": "system.dark && antigravity.gemini.enabled && !antigravity.dual_mode", - "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", + "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace * antigravity.gemini.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", "y": "(11) - 1", @@ -1380,8 +1380,8 @@ { "id": "antigravity-session-gemini-solo-light", "name": "light Antigravity Gemini solo session segments", - "render": "(1 - system.dark) && antigravity.gemini.enabled && !antigravity.dual_mode", - "visibility": "100", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "100 * antigravity.gemini.opacity", "parent": "antigravity-provider", "y": "11", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", @@ -1415,8 +1415,8 @@ { "id": "antigravity-session-gemini-solo-light-pace", "name": "light Antigravity Gemini solo session segments pace tick", - "render": "(1 - system.dark) && antigravity.gemini.enabled && !antigravity.dual_mode", - "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace * antigravity.gemini.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", "y": "(11) - 1", @@ -1441,8 +1441,8 @@ { "id": "antigravity-session-external-solo-dark", "name": "dark Antigravity External solo session segments", - "render": "system.dark && antigravity.external.enabled && !antigravity.dual_mode", - "visibility": "100", + "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "100 * antigravity.external.opacity", "parent": "antigravity-provider", "y": "11", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", @@ -1476,8 +1476,8 @@ { "id": "antigravity-session-external-solo-dark-pace", "name": "dark Antigravity External solo session segments pace tick", - "render": "system.dark && antigravity.external.enabled && !antigravity.dual_mode", - "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace", + "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace * antigravity.external.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", "y": "(11) - 1", @@ -1502,8 +1502,8 @@ { "id": "antigravity-session-external-solo-light", "name": "light Antigravity External solo session segments", - "render": "(1 - system.dark) && antigravity.external.enabled && !antigravity.dual_mode", - "visibility": "100", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "100 * antigravity.external.opacity", "parent": "antigravity-provider", "y": "11", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", @@ -1537,8 +1537,8 @@ { "id": "antigravity-session-external-solo-light-pace", "name": "light Antigravity External solo session segments pace tick", - "render": "(1 - system.dark) && antigravity.external.enabled && !antigravity.dual_mode", - "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace * antigravity.external.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", "y": "(11) - 1", @@ -1807,7 +1807,7 @@ { "id": "antigravity-session-gemini-value-dark", "name": "dark Antigravity Gemini session value", - "render": "system.dark && antigravity.gemini.enabled && (!antigravity.dual_mode || antigravity.crossfade_opacity > 0.01)", + "render": "system.dark && antigravity.gemini.opacity > 0.01", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1833,14 +1833,14 @@ "align": "left", "color": { "color": "#5EEAD4FF", - "opacity": "if(antigravity.dual_mode, antigravity.crossfade_opacity, 1.0)" + "opacity": "antigravity.gemini.opacity" } } }, { "id": "antigravity-session-gemini-value-light", "name": "light Antigravity Gemini session value", - "render": "(1 - system.dark) && antigravity.gemini.enabled && (!antigravity.dual_mode || antigravity.crossfade_opacity > 0.01)", + "render": "(1 - system.dark) && antigravity.gemini.opacity > 0.01", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1866,14 +1866,14 @@ "align": "left", "color": { "color": "#007A6CFF", - "opacity": "if(antigravity.dual_mode, antigravity.crossfade_opacity, 1.0)" + "opacity": "antigravity.gemini.opacity" } } }, { "id": "antigravity-session-external-value-dark", "name": "dark Antigravity External session value", - "render": "system.dark && antigravity.external.enabled && (!antigravity.dual_mode || (1.0 - antigravity.crossfade_opacity) > 0.01)", + "render": "system.dark && antigravity.external.opacity > 0.01", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1899,14 +1899,14 @@ "align": "left", "color": { "color": "#98D0E0FF", - "opacity": "if(antigravity.dual_mode, 1.0 - antigravity.crossfade_opacity, 1.0)" + "opacity": "antigravity.external.opacity" } } }, { "id": "antigravity-session-external-value-light", "name": "light Antigravity External session value", - "render": "(1 - system.dark) && antigravity.external.enabled && (!antigravity.dual_mode || (1.0 - antigravity.crossfade_opacity) > 0.01)", + "render": "(1 - system.dark) && antigravity.external.opacity > 0.01", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1932,15 +1932,15 @@ "align": "left", "color": { "color": "#2D6F82FF", - "opacity": "if(antigravity.dual_mode, 1.0 - antigravity.crossfade_opacity, 1.0)" + "opacity": "antigravity.external.opacity" } } }, { "id": "antigravity-weekly-gemini-solo-dark", "name": "dark Antigravity Gemini solo weekly segments", - "render": "system.dark && antigravity.gemini.enabled && !antigravity.dual_mode", - "visibility": "100", + "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "100 * antigravity.gemini.opacity", "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", @@ -1974,8 +1974,8 @@ { "id": "antigravity-weekly-gemini-solo-dark-pace", "name": "dark Antigravity Gemini solo weekly segments pace tick", - "render": "system.dark && antigravity.gemini.enabled && !antigravity.dual_mode", - "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace", + "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.gemini.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", "y": "(27) - 1", @@ -2000,8 +2000,8 @@ { "id": "antigravity-weekly-gemini-solo-light", "name": "light Antigravity Gemini solo weekly segments", - "render": "(1 - system.dark) && antigravity.gemini.enabled && !antigravity.dual_mode", - "visibility": "100", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "100 * antigravity.gemini.opacity", "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", @@ -2035,8 +2035,8 @@ { "id": "antigravity-weekly-gemini-solo-light-pace", "name": "light Antigravity Gemini solo weekly segments pace tick", - "render": "(1 - system.dark) && antigravity.gemini.enabled && !antigravity.dual_mode", - "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.gemini.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", "y": "(27) - 1", @@ -2061,8 +2061,8 @@ { "id": "antigravity-weekly-external-solo-dark", "name": "dark Antigravity External solo weekly segments", - "render": "system.dark && antigravity.external.enabled && !antigravity.dual_mode", - "visibility": "100", + "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "100 * antigravity.external.opacity", "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", @@ -2096,8 +2096,8 @@ { "id": "antigravity-weekly-external-solo-dark-pace", "name": "dark Antigravity External solo weekly segments pace tick", - "render": "system.dark && antigravity.external.enabled && !antigravity.dual_mode", - "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace", + "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.external.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", "y": "(27) - 1", @@ -2122,8 +2122,8 @@ { "id": "antigravity-weekly-external-solo-light", "name": "light Antigravity External solo weekly segments", - "render": "(1 - system.dark) && antigravity.external.enabled && !antigravity.dual_mode", - "visibility": "100", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "100 * antigravity.external.opacity", "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", @@ -2157,8 +2157,8 @@ { "id": "antigravity-weekly-external-solo-light-pace", "name": "light Antigravity External solo weekly segments pace tick", - "render": "(1 - system.dark) && antigravity.external.enabled && !antigravity.dual_mode", - "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.external.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", "y": "(27) - 1", @@ -2427,7 +2427,7 @@ { "id": "antigravity-weekly-gemini-value-dark", "name": "dark Antigravity Gemini weekly value", - "render": "system.dark && antigravity.gemini.enabled && (!antigravity.dual_mode || antigravity.crossfade_opacity > 0.01)", + "render": "system.dark && antigravity.gemini.opacity > 0.01", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -2453,14 +2453,14 @@ "align": "left", "color": { "color": "#5EEAD4FF", - "opacity": "if(antigravity.dual_mode, antigravity.crossfade_opacity, 1.0)" + "opacity": "antigravity.gemini.opacity" } } }, { "id": "antigravity-weekly-gemini-value-light", "name": "light Antigravity Gemini weekly value", - "render": "(1 - system.dark) && antigravity.gemini.enabled && (!antigravity.dual_mode || antigravity.crossfade_opacity > 0.01)", + "render": "(1 - system.dark) && antigravity.gemini.opacity > 0.01", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -2486,14 +2486,14 @@ "align": "left", "color": { "color": "#007A6CFF", - "opacity": "if(antigravity.dual_mode, antigravity.crossfade_opacity, 1.0)" + "opacity": "antigravity.gemini.opacity" } } }, { "id": "antigravity-weekly-external-value-dark", "name": "dark Antigravity External weekly value", - "render": "system.dark && antigravity.external.enabled && (!antigravity.dual_mode || (1.0 - antigravity.crossfade_opacity) > 0.01)", + "render": "system.dark && antigravity.external.opacity > 0.01", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -2519,14 +2519,14 @@ "align": "left", "color": { "color": "#98D0E0FF", - "opacity": "if(antigravity.dual_mode, 1.0 - antigravity.crossfade_opacity, 1.0)" + "opacity": "antigravity.external.opacity" } } }, { "id": "antigravity-weekly-external-value-light", "name": "light Antigravity External weekly value", - "render": "(1 - system.dark) && antigravity.external.enabled && (!antigravity.dual_mode || (1.0 - antigravity.crossfade_opacity) > 0.01)", + "render": "(1 - system.dark) && antigravity.external.opacity > 0.01", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -2552,7 +2552,7 @@ "align": "left", "color": { "color": "#2D6F82FF", - "opacity": "if(antigravity.dual_mode, 1.0 - antigravity.crossfade_opacity, 1.0)" + "opacity": "antigravity.external.opacity" } } }, diff --git a/src/window.rs b/src/window.rs index ed24961b..99df7d40 100644 --- a/src/window.rs +++ b/src/window.rs @@ -101,6 +101,9 @@ struct AppState { pub(super) show_antigravity_gemini: bool, pub(super) show_antigravity_external: bool, crossfade_opacity: f64, + antigravity_hovered: bool, + crossfade_animation: Option, + pub(super) antigravity_tile_rect: Option<(f64, f64, f64, f64)>, accounts: crate::accounts::AccountSettings, data: Option, @@ -1931,6 +1934,7 @@ fn replace_active_theme(state: &mut AppState, loaded: ThemeDocument, path: Optio clear_auto_ejection(state); } state.active_theme = Some(loaded); + state.antigravity_tile_rect = None; if path.is_some() { state.active_theme_path = path; } @@ -2594,6 +2598,9 @@ pub fn run() { show_antigravity_gemini: settings.antigravity_gemini_enabled(), show_antigravity_external: settings.antigravity_external_enabled(), crossfade_opacity: 1.0, + antigravity_hovered: false, + crossfade_animation: None, + antigravity_tile_rect: None, accounts: settings.accounts.clone(), data: None, poll_interval_ms: settings.poll_interval_ms, @@ -2664,17 +2671,6 @@ pub fn run() { // Initial render using the presenter selected by the surface nest. schedule_countdown_timer(); schedule_clock_timer(); - let startup_dual_mode = { - let state = lock_state(); - state.as_ref().is_some_and(|s| { - s.show_antigravity_gemini - && s.show_antigravity_external - && s.providers.contains(ProviderId::Antigravity) - }) - }; - if startup_dual_mode { - arm_crossfade_schedule(hwnd); - } render_layered(); if open_dashboard_on_start { @@ -2766,6 +2762,24 @@ fn render_layered() { }; let surface = &theme.surfaces[surface_index]; let surface_runtime = theme_runtime_for_surface(&theme, surface_index, runtime); + if surface_index == 0 { + let needs_rect = lock_state() + .as_ref() + .map(|s| s.antigravity_tile_rect.is_none()) + .unwrap_or(false); + if needs_rect { + let tile_rect = theme_engine::object_bounds( + &theme, + 0, + usage_data.as_ref(), + surface_runtime, + mouse::ANTIGRAVITY_HOVER_OBJECT, + ); + if let Some(state) = lock_state().as_mut() { + state.antigravity_tile_rect = tile_rect; + } + } + } let nest = surface .placement .nest @@ -3380,43 +3394,116 @@ fn check_language_change() { } } -/// All widget surfaces use the same wall-clock phase, including after a -/// settings change or a delayed timer following sleep. -fn antigravity_crossfade_frame(now_ms: u64) -> (f64, u32) { - let phase = now_ms % 20_000; - let smooth = |elapsed: u64| { - let t = elapsed as f64 / 2_000.0; - t * t * (3.0 - 2.0 * t) +const ANTIGRAVITY_CROSSFADE_DURATION_MS: u64 = 70; +const ANTIGRAVITY_CROSSFADE_TICK_MS: u32 = 16; + +#[derive(Clone, Copy, Debug)] +pub(super) struct CrossfadeAnimation { + pub(super) start_opacity: f64, + pub(super) target_opacity: f64, + pub(super) start_time: std::time::Instant, + pub(super) duration: std::time::Duration, + pub(super) last_rendered: std::time::Instant, +} + +impl CrossfadeAnimation { + pub(super) fn new( + start_opacity: f64, + target_opacity: f64, + duration: std::time::Duration, + ) -> Self { + let now = std::time::Instant::now(); + Self { + start_opacity, + target_opacity, + start_time: now, + duration, + last_rendered: now, + } + } + + pub(super) fn current_opacity(&self) -> (f64, bool) { + let elapsed = self.start_time.elapsed(); + if elapsed >= self.duration || self.duration.is_zero() { + (self.target_opacity, false) + } else { + let progress = elapsed.as_secs_f64() / self.duration.as_secs_f64(); + let opacity = + self.start_opacity + (self.target_opacity - self.start_opacity) * progress; + (opacity, true) + } + } +} + +pub(super) fn tick_antigravity_crossfade(hwnd: HWND, throttle: bool) { + let (should_render, still_animating) = { + let mut state = lock_state(); + let Some(state) = state.as_mut() else { + return; + }; + let Some(mut anim) = state.crossfade_animation else { + return; + }; + let now = std::time::Instant::now(); + if throttle && now.duration_since(anim.last_rendered) < std::time::Duration::from_millis(15) + { + return; + } + let (opacity, more) = anim.current_opacity(); + state.crossfade_opacity = opacity; + anim.last_rendered = now; + if more { + state.crossfade_animation = Some(anim); + } else { + state.crossfade_opacity = anim.target_opacity; + state.crossfade_animation = None; + } + (true, more) }; - match phase { - 0..8_000 => (1.0, (8_000 - phase) as u32), - 8_000..10_000 => (1.0 - smooth(phase - 8_000), 30), - 10_000..18_000 => (0.0, (18_000 - phase) as u32), - _ => (smooth(phase - 18_000), 30), + if !still_animating { + unsafe { + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); } + } + if should_render { + render_layered(); } } -pub(super) fn arm_crossfade_schedule(hwnd: HWND) { - let now_ms = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis() as u64) - .unwrap_or(0); - let (opacity, delay_ms) = antigravity_crossfade_frame(now_ms); - let mut state = lock_state(); - let dual_mode = state.as_ref().is_some_and(|state| { - state.show_antigravity_gemini - && state.show_antigravity_external - && state.providers.contains(ProviderId::Antigravity) - }); - if let Some(state) = state.as_mut() { - state.crossfade_opacity = if dual_mode { opacity } else { 1.0 }; +/// Starts the crossfade when the cursor enters or leaves the Antigravity tile. +pub(super) fn set_antigravity_hover(hwnd: HWND, hovered: bool) { + let mut should_animate = false; + { + let mut state = lock_state(); + let Some(state) = state.as_mut() else { + return; + }; + // Dual mode fades Gemini to Claude/GPT; a single pool fades to the + // hidden one. + let any_pool = (state.show_antigravity_gemini || state.show_antigravity_external) + && state.providers.contains(ProviderId::Antigravity); + if state.antigravity_hovered != hovered { + state.antigravity_hovered = hovered; + if any_pool { + let target = if hovered { 0.0 } else { 1.0 }; + state.crossfade_animation = Some(CrossfadeAnimation::new( + state.crossfade_opacity, + target, + std::time::Duration::from_millis(ANTIGRAVITY_CROSSFADE_DURATION_MS), + )); + should_animate = true; + } + } } - unsafe { - if dual_mode { - let _ = set_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE, delay_ms, None); - } else { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + if should_animate { + unsafe { + let _ = set_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ANTIGRAVITY_CROSSFADE_TICK_MS, + None, + ); } + render_layered(); } } @@ -3426,8 +3513,6 @@ fn reload_external_settings(hwnd: HWND) { let theme_path = settings.active_theme_path.as_ref().map(PathBuf::from); let providers_changed; let lock_enabled; - let antigravity_changed; - let dual_mode; { let mut state = lock_state(); let Some(state) = state.as_mut() else { @@ -3437,8 +3522,6 @@ fn reload_external_settings(hwnd: HWND) { state.providers != settings.enabled_providers() || state.accounts != settings.accounts; let gemini_enabled = settings.antigravity_gemini_enabled(); let external_enabled = settings.antigravity_external_enabled(); - antigravity_changed = state.show_antigravity_gemini != gemini_enabled - || state.show_antigravity_external != external_enabled; state.show_antigravity_gemini = gemini_enabled; state.show_antigravity_external = external_enabled; state.accounts = settings.accounts.clone(); @@ -3450,15 +3533,17 @@ fn reload_external_settings(hwnd: HWND) { state.usage_countdown = settings.usage_countdown; state.use_windows_default_font = settings.use_windows_default_font; state.show_pace = settings.show_pace; - dual_mode = state.show_antigravity_gemini - && state.show_antigravity_external + let any_pool = (state.show_antigravity_gemini || state.show_antigravity_external) && state.providers.contains(ProviderId::Antigravity); - if !dual_mode { + if !any_pool { unsafe { let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); } + state.antigravity_hovered = false; state.crossfade_opacity = 1.0; + state.crossfade_animation = None; } + state.antigravity_tile_rect = None; update_configured_placement( state, settings.placement_override, @@ -3470,9 +3555,6 @@ fn reload_external_settings(hwnd: HWND) { lock_enabled = state.lock_taskbar; apply_language_to_state(state, language_override); } - if dual_mode && (antigravity_changed || providers_changed) { - arm_crossfade_schedule(hwnd); - } unsafe { set_controller_timer(Some(hwnd), TIMER_POLL, settings.poll_interval_ms, None); } @@ -4302,25 +4384,24 @@ mod credential_watch_worker_tests { #[cfg(test)] mod antigravity_crossfade_tests { - use super::antigravity_crossfade_frame; + use super::CrossfadeAnimation; + use std::time::Duration; #[test] - fn pool_crossfade_follows_the_same_phase_after_startup_sleep_and_rearming() { - for cycle in [0, 20_000, 180_000] { - for (phase, opacity, delay) in [ - (0, 1.0, 8_000), - (7_000, 1.0, 1_000), - (8_000, 1.0, 30), - (9_000, 0.5, 30), - (10_000, 0.0, 8_000), - (17_000, 0.0, 1_000), - (18_000, 0.0, 30), - (19_000, 0.5, 30), - ] { - assert_eq!(antigravity_crossfade_frame(cycle + phase), (opacity, delay)); - } - } - let before_wrap = antigravity_crossfade_frame(19_999).0; - assert!((1.0 - before_wrap).abs() < 0.000_001); + fn crossfade_animation_interpolates_and_completes() { + let anim = CrossfadeAnimation::new(1.0, 0.0, Duration::from_millis(0)); + let (opacity, more) = anim.current_opacity(); + assert_eq!(opacity, 0.0); + assert!(!more); + + let anim = CrossfadeAnimation::new(0.0, 1.0, Duration::from_secs(10)); + let (opacity, more) = anim.current_opacity(); + assert!(opacity < 0.1); + assert!(more); + + let anim_return = CrossfadeAnimation::new(0.0, 1.0, Duration::from_millis(0)); + let (opacity, more) = anim_return.current_opacity(); + assert_eq!(opacity, 1.0); + assert!(!more); } } diff --git a/src/window/message_loop.rs b/src/window/message_loop.rs index 7a21f3da..b0ed56ea 100644 --- a/src/window/message_loop.rs +++ b/src/window/message_loop.rs @@ -131,8 +131,7 @@ pub(super) unsafe extern "system" fn wnd_proc( lifecycle::reconcile_surfaces(); } TIMER_CROSSFADE_SCHEDULE => { - arm_crossfade_schedule(hwnd); - render_layered(); + tick_antigravity_crossfade(hwnd, false); } _ => {} } @@ -144,6 +143,9 @@ pub(super) unsafe extern "system" fn wnd_proc( LRESULT(0) } WM_APP_USAGE_UPDATED => { + if let Some(state) = lock_state().as_mut() { + state.antigravity_tile_rect = None; + } check_theme_change(); check_language_change(); render_layered(); @@ -815,7 +817,6 @@ pub(super) unsafe extern "system" fn wnd_proc( } IDM_PROVIDER_ANTIGRAVITY_GEMINI | IDM_PROVIDER_ANTIGRAVITY_EXTERNAL => { let mut should_reposition = false; - let mut dual_mode = false; { let mut state = lock_state(); if let Some(s) = state.as_mut() { @@ -851,24 +852,24 @@ pub(super) unsafe extern "system" fn wnd_proc( s.providers.set(ProviderId::Antigravity, true); should_reposition = true; } - dual_mode = s.show_antigravity_gemini - && s.show_antigravity_external + let any_pool = (s.show_antigravity_gemini + || s.show_antigravity_external) && s.providers.contains(ProviderId::Antigravity); - if !dual_mode { + s.antigravity_tile_rect = None; + if !any_pool { unsafe { let _ = kill_controller_timer( Some(hwnd), TIMER_CROSSFADE_SCHEDULE, ); } + s.antigravity_hovered = false; s.crossfade_opacity = 1.0; + s.crossfade_animation = None; } } } } - if dual_mode { - arm_crossfade_schedule(hwnd); - } save_state_settings(); if should_reposition { position_at_taskbar(); @@ -878,13 +879,14 @@ pub(super) unsafe extern "system" fn wnd_proc( request_poll(hwnd); } id if ProviderId::from_native_menu_command_id(id).is_some() => { - let mut dual_mode = false; { let mut state = lock_state(); if let Some(s) = state.as_mut() { let provider = ProviderId::from_native_menu_command_id(id) .expect("provider menu command was matched above"); let changed = s.providers.toggle(provider); + // Tile widths depend on the provider count. + s.antigravity_tile_rect = None; if provider == ProviderId::Antigravity && changed { let enabled = s.providers.contains(ProviderId::Antigravity); s.show_antigravity_gemini = enabled; @@ -892,17 +894,13 @@ pub(super) unsafe extern "system" fn wnd_proc( if !enabled { let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); - s.crossfade_opacity = 1.0; + s.antigravity_hovered = false; s.crossfade_opacity = 1.0; + s.crossfade_animation = None; + s.antigravity_tile_rect = None; } } - dual_mode = s.show_antigravity_gemini - && s.show_antigravity_external - && s.providers.contains(ProviderId::Antigravity); } } - if dual_mode { - arm_crossfade_schedule(hwnd); - } save_state_settings(); position_at_taskbar(); render_layered(); diff --git a/src/window/mouse.rs b/src/window/mouse.rs index acc7bb88..4d7fdda6 100644 --- a/src/window/mouse.rs +++ b/src/window/mouse.rs @@ -195,11 +195,36 @@ pub(super) fn dispatch_double_click(hwnd: HWND, surface_index: usize, object_id: let _ = hwnd; } +/// Theme object whose hover reveals the Antigravity Claude/GPT pool. +pub(super) const ANTIGRAVITY_HOVER_OBJECT: &str = "antigravity-provider"; + +fn antigravity_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { + let state = lock_state(); + let Some(state) = state.as_ref() else { + return false; + }; + if surface_index_for_window(state, hwnd) != Some(0) { + return false; + } + let Some(rect) = state.antigravity_tile_rect else { + return false; + }; + let Some(theme) = state.active_theme.as_ref() else { + return false; + }; + let scale = theme_surface_scale(theme, 0).max(0.01); + let (x, y) = mouse_client_point(lparam); + let lx = x / scale; + let ly = y / scale; + lx >= rect.0 && lx < rect.0 + rect.2 && ly >= rect.1 && ly < rect.1 + rect.3 +} + pub(super) fn update_mouse_hover(hwnd: HWND, lparam: LPARAM) { track_mouse_hover_with(hwnd, widget_tooltip::visible_for(hwnd), |tracking| unsafe { TrackMouseEvent(tracking) }); - for _ in 0..4 { + set_antigravity_hover(hwnd, antigravity_hover_at(hwnd, lparam)); + super::tick_antigravity_crossfade(hwnd, true); for _ in 0..4 { let target = mouse_target_at(hwnd, lparam); let previous = { let mut state = lock_state(); @@ -261,7 +286,7 @@ fn track_mouse_hover_with( pub(super) fn clear_mouse_hover(hwnd: HWND) { widget_tooltip::hide_for(hwnd); - let previous = { + set_antigravity_hover(hwnd, false); let previous = { let mut state = lock_state(); let Some(state) = state.as_mut() else { return; diff --git a/src/window/placement_regression_tests.rs b/src/window/placement_regression_tests.rs index f03664af..dff6ec63 100644 --- a/src/window/placement_regression_tests.rs +++ b/src/window/placement_regression_tests.rs @@ -15,6 +15,9 @@ pub(super) fn state_for(theme: ThemeDocument, placement: PlacementOverride) -> A show_antigravity_gemini: true, show_antigravity_external: true, crossfade_opacity: 1.0, + antigravity_hovered: false, + crossfade_animation: None, + antigravity_tile_rect: None, accounts: Default::default(), data: None, poll_interval_ms: POLL_15_MIN, From 5cdf02dfac5f2be2703781682a04f4fcaa0153dc Mon Sep 17 00:00:00 2001 From: fix2it Date: Mon, 26 Oct 2026 01:10:52 +0200 Subject: [PATCH 06/17] style: cargo fmt --- src/theme_engine/tests.rs | 6 +----- src/window.rs | 3 ++- src/window/message_loop.rs | 3 ++- src/window/mouse.rs | 6 ++++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index ecc29576..4ad06ec7 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -4004,11 +4004,7 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { assert!(matches!(&tick.content, SceneContent::None)); assert_eq!(tick.parent, gauge.parent); assert_eq!(tick.render.0, gauge.render.0); - let stripped = tick - .visibility - .0 - .strip_prefix("(100) * ") - .unwrap(); + let stripped = tick.visibility.0.strip_prefix("(100) * ").unwrap(); let stripped = stripped .strip_suffix(" * antigravity.gemini.opacity") .or_else(|| stripped.strip_suffix(" * antigravity.external.opacity")) diff --git a/src/window.rs b/src/window.rs index 99df7d40..5704069a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3462,7 +3462,8 @@ pub(super) fn tick_antigravity_crossfade(hwnd: HWND, throttle: bool) { }; if !still_animating { unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); } + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } } if should_render { render_layered(); diff --git a/src/window/message_loop.rs b/src/window/message_loop.rs index b0ed56ea..cc6df474 100644 --- a/src/window/message_loop.rs +++ b/src/window/message_loop.rs @@ -894,7 +894,8 @@ pub(super) unsafe extern "system" fn wnd_proc( if !enabled { let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); - s.antigravity_hovered = false; s.crossfade_opacity = 1.0; + s.antigravity_hovered = false; + s.crossfade_opacity = 1.0; s.crossfade_animation = None; s.antigravity_tile_rect = None; } diff --git a/src/window/mouse.rs b/src/window/mouse.rs index 4d7fdda6..c72c33e0 100644 --- a/src/window/mouse.rs +++ b/src/window/mouse.rs @@ -224,7 +224,8 @@ pub(super) fn update_mouse_hover(hwnd: HWND, lparam: LPARAM) { TrackMouseEvent(tracking) }); set_antigravity_hover(hwnd, antigravity_hover_at(hwnd, lparam)); - super::tick_antigravity_crossfade(hwnd, true); for _ in 0..4 { + super::tick_antigravity_crossfade(hwnd, true); + for _ in 0..4 { let target = mouse_target_at(hwnd, lparam); let previous = { let mut state = lock_state(); @@ -286,7 +287,8 @@ fn track_mouse_hover_with( pub(super) fn clear_mouse_hover(hwnd: HWND) { widget_tooltip::hide_for(hwnd); - set_antigravity_hover(hwnd, false); let previous = { + set_antigravity_hover(hwnd, false); + let previous = { let mut state = lock_state(); let Some(state) = state.as_mut() else { return; From 65b2ff3f1bd8d9cc8eb4f6af6a024a8a86e2a595 Mon Sep 17 00:00:00 2001 From: fix2it Date: Wed, 28 Oct 2026 01:37:20 +0200 Subject: [PATCH 07/17] feat(codex): dual mode with 300ms hover crossfade, dynamic width, and styled o/x markers --- src/context_menu.rs | 17 +- src/localization/locales/de.toml | 2 + src/localization/locales/en.toml | 2 + src/localization/locales/es.toml | 2 + src/localization/locales/fr.toml | 2 + src/localization/locales/ja.toml | 2 + src/localization/locales/ko.toml | 2 + src/localization/locales/nl.toml | 2 + src/localization/locales/pl.toml | 2 + src/localization/locales/pt-BR.toml | 2 + src/localization/locales/ru.toml | 2 + src/localization/locales/th.toml | 2 + src/localization/locales/tr.toml | 2 + src/localization/locales/zh-CN.toml | 2 + src/localization/locales/zh-TW.toml | 2 + src/studio_app/studio_context_menu_editor.rs | 4 + src/studio_app/studio_native_menu.rs | 22 + src/theme_engine.rs | 45 + src/theme_engine/tests.rs | 24 +- src/themes/compact-fluent-quad.json | 1426 +++++++++++++++++- src/window.rs | 170 ++- src/window/message_loop.rs | 136 +- src/window/mouse.rs | 27 + src/window/placement_regression_tests.rs | 4 + src/window/window_context_menu.rs | 24 + 25 files changed, 1849 insertions(+), 78 deletions(-) diff --git a/src/context_menu.rs b/src/context_menu.rs index 26130350..6272a3b1 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -66,6 +66,8 @@ pub enum ContextMenuAction { }, ToggleAntigravityGemini, ToggleAntigravityExternal, + ToggleCodexMain, + ToggleCodexX, ToggleStartup, ToggleTaskbarLock, ToggleShowPace, @@ -312,6 +314,8 @@ pub fn rendered_label( "Exit" => language.text("Exit"), "Claude Code" => language.text("Claude Code"), "Codex" => language.text("Codex"), + "Codex (Main)" => language.text("Codex (Main)"), + "Codex X" => language.text("Codex X"), "Antigravity" => language.text("Antigravity"), "OpenCode" => language.text("OpenCode"), "Cursor" => language.text("Cursor"), @@ -382,11 +386,14 @@ pub fn classic_context_menu() -> ContextMenuDocument { }, ), ContextMenuItem::action( - "provider-codex", - "Codex", - Action::ToggleProvider { - provider: Provider::Codex, - }, + "provider-codex-main", + "Codex (Main)", + Action::ToggleCodexMain, + ), + ContextMenuItem::action( + "provider-codex-x", + "Codex X", + Action::ToggleCodexX, ), ContextMenuItem::action( "provider-antigravity-gemini", diff --git a/src/localization/locales/de.toml b/src/localization/locales/de.toml index 971127fe..0a009f56 100644 --- a/src/localization/locales/de.toml +++ b/src/localization/locales/de.toml @@ -129,6 +129,8 @@ copilot_window_title = "Copilot-Nutzungsmonitor" "Close asset library" = "Close asset library" "Close Builder" = "Baukasten schließen" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Nutzungsdaten von Anthropic abrufen" "Collect usage from GitHub" = "Nutzungsdaten von GitHub abrufen" "Collect usage from Google" = "Nutzungsdaten von Google abrufen" diff --git a/src/localization/locales/en.toml b/src/localization/locales/en.toml index 7dedde76..d651181c 100644 --- a/src/localization/locales/en.toml +++ b/src/localization/locales/en.toml @@ -129,6 +129,8 @@ copilot_window_title = "Copilot Usage Monitor" "Close asset library" = "Close asset library" "Close Builder" = "Close Builder" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Collect usage from Anthropic" "Collect usage from GitHub" = "Collect usage from GitHub" "Collect usage from Google" = "Collect usage from Google" diff --git a/src/localization/locales/es.toml b/src/localization/locales/es.toml index c6ce7f76..89a0cd87 100644 --- a/src/localization/locales/es.toml +++ b/src/localization/locales/es.toml @@ -129,6 +129,8 @@ copilot_window_title = "Monitor de uso de Copilot" "Close asset library" = "Close asset library" "Close Builder" = "Cerrar Constructor" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Recopilar uso de Anthropic" "Collect usage from GitHub" = "Recopilar uso de GitHub" "Collect usage from Google" = "Recopilar uso de Google" diff --git a/src/localization/locales/fr.toml b/src/localization/locales/fr.toml index 7fe9771a..eb1466d6 100644 --- a/src/localization/locales/fr.toml +++ b/src/localization/locales/fr.toml @@ -129,6 +129,8 @@ copilot_window_title = "Moniteur d'utilisation Copilot" "Close asset library" = "Close asset library" "Close Builder" = "Fermer le Constructeur" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Collecter l'utilisation depuis Anthropic" "Collect usage from GitHub" = "Collecter l'utilisation depuis GitHub" "Collect usage from Google" = "Collecter l'utilisation depuis Google" diff --git a/src/localization/locales/ja.toml b/src/localization/locales/ja.toml index 3fb8ddbe..15ecd04d 100644 --- a/src/localization/locales/ja.toml +++ b/src/localization/locales/ja.toml @@ -131,6 +131,8 @@ copilot_window_title = "Copilot 使用量モニター" "Close asset library" = "Close asset library" "Close Builder" = "ビルダーを閉じる" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Anthropic から使用量を取得" "Collect usage from GitHub" = "GitHub から使用量を取得" "Collect usage from Google" = "Google から使用量を取得" diff --git a/src/localization/locales/ko.toml b/src/localization/locales/ko.toml index 5ef89822..fa6047c3 100644 --- a/src/localization/locales/ko.toml +++ b/src/localization/locales/ko.toml @@ -131,6 +131,8 @@ copilot_window_title = "Copilot 사용량 모니터" "Close asset library" = "Close asset library" "Close Builder" = "빌더 닫기" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Anthropic에서 사용량 수집" "Collect usage from GitHub" = "GitHub에서 사용량 수집" "Collect usage from Google" = "Google에서 사용량 수집" diff --git a/src/localization/locales/nl.toml b/src/localization/locales/nl.toml index 57a05f38..52149bc8 100644 --- a/src/localization/locales/nl.toml +++ b/src/localization/locales/nl.toml @@ -129,6 +129,8 @@ copilot_window_title = "Copilot-gebruiksmonitor" "Close asset library" = "Mediabibliotheek sluiten" "Close Builder" = "Bouwer sluiten" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Gebruiksgegevens ophalen bij Anthropic" "Collect usage from GitHub" = "Gebruiksgegevens ophalen bij GitHub" "Collect usage from Google" = "Gebruiksgegevens ophalen bij Google" diff --git a/src/localization/locales/pl.toml b/src/localization/locales/pl.toml index 92e37d09..9e8e0861 100644 --- a/src/localization/locales/pl.toml +++ b/src/localization/locales/pl.toml @@ -129,6 +129,8 @@ copilot_window_title = "Monitor zużycia Copilot" "Close asset library" = "Zamknij bibliotekę zasobów" "Close Builder" = "Zamknij kreator" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "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 Google" = "Zbieraj dane o zużyciu z Google" diff --git a/src/localization/locales/pt-BR.toml b/src/localization/locales/pt-BR.toml index 4fb1e0a6..8232d211 100644 --- a/src/localization/locales/pt-BR.toml +++ b/src/localization/locales/pt-BR.toml @@ -129,6 +129,8 @@ copilot_window_title = "Monitor de uso do Copilot" "Close asset library" = "Close asset library" "Close Builder" = "Fechar Construtor" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Coletar uso da Anthropic" "Collect usage from GitHub" = "Coletar uso do GitHub" "Collect usage from Google" = "Coletar uso do Google" diff --git a/src/localization/locales/ru.toml b/src/localization/locales/ru.toml index ae313796..efdbe6ef 100644 --- a/src/localization/locales/ru.toml +++ b/src/localization/locales/ru.toml @@ -129,6 +129,8 @@ copilot_window_title = "Монитор использования Copilot" "Close asset library" = "Close asset library" "Close Builder" = "Закрыть конструктор" "Codex" = "Codex" +"Codex (Main)" = "Codex (Основной)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Получать данные об использовании Anthropic" "Collect usage from GitHub" = "Получать данные об использовании GitHub" "Collect usage from Google" = "Получать данные об использовании Google" diff --git a/src/localization/locales/th.toml b/src/localization/locales/th.toml index 5bf9465e..b0e82dcc 100644 --- a/src/localization/locales/th.toml +++ b/src/localization/locales/th.toml @@ -131,6 +131,8 @@ copilot_window_title = "ตัวติดตามการใช้งาน C "Close asset library" = "ปิดคลังแอสเซท" "Close Builder" = "ปิดตัวสร้าง" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "ดึงข้อมูลการใช้งานจาก Anthropic" "Collect usage from GitHub" = "ดึงข้อมูลการใช้งานจาก GitHub" "Collect usage from Google" = "ดึงข้อมูลการใช้งานจาก Google" diff --git a/src/localization/locales/tr.toml b/src/localization/locales/tr.toml index 1a82060a..7fc05f49 100644 --- a/src/localization/locales/tr.toml +++ b/src/localization/locales/tr.toml @@ -129,6 +129,8 @@ copilot_window_title = "Copilot Kullanım İzleyicisi" "Close asset library" = "Close asset library" "Close Builder" = "Oluşturucuyu kapat" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "Anthropic kullanımını topla" "Collect usage from GitHub" = "GitHub kullanımını topla" "Collect usage from Google" = "Google kullanımını topla" diff --git a/src/localization/locales/zh-CN.toml b/src/localization/locales/zh-CN.toml index ffae5104..671be669 100644 --- a/src/localization/locales/zh-CN.toml +++ b/src/localization/locales/zh-CN.toml @@ -131,6 +131,8 @@ copilot_window_title = "Copilot 使用量监控" "Close asset library" = "Close asset library" "Close Builder" = "关闭构建器" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "从 Anthropic 获取使用量" "Collect usage from GitHub" = "从 GitHub 获取使用量" "Collect usage from Google" = "从 Google 获取使用量" diff --git a/src/localization/locales/zh-TW.toml b/src/localization/locales/zh-TW.toml index 943ed82c..8b164705 100644 --- a/src/localization/locales/zh-TW.toml +++ b/src/localization/locales/zh-TW.toml @@ -131,6 +131,8 @@ copilot_window_title = "Copilot 使用量監控" "Close asset library" = "Close asset library" "Close Builder" = "關閉建構器" "Codex" = "Codex" +"Codex (Main)" = "Codex (Main)" +"Codex X" = "Codex X" "Collect usage from Anthropic" = "從 Anthropic 取得使用量" "Collect usage from GitHub" = "從 GitHub 取得使用量" "Collect usage from Google" = "從 Google 取得使用量" diff --git a/src/studio_app/studio_context_menu_editor.rs b/src/studio_app/studio_context_menu_editor.rs index e8cfc9b7..8d7ec9c4 100644 --- a/src/studio_app/studio_context_menu_editor.rs +++ b/src/studio_app/studio_context_menu_editor.rs @@ -323,6 +323,8 @@ pub(super) fn context_menu_action_script(action: &ContextMenuAction) -> String { } ContextMenuAction::ToggleAntigravityGemini => "toggle_antigravity_gemini()".into(), ContextMenuAction::ToggleAntigravityExternal => "toggle_antigravity_external()".into(), + ContextMenuAction::ToggleCodexMain => "toggle_codex_main()".into(), + ContextMenuAction::ToggleCodexX => "toggle_codex_x()".into(), ContextMenuAction::ToggleStartup => "toggle_startup()".into(), ContextMenuAction::ToggleTaskbarLock => "toggle_taskbar_lock()".into(), ContextMenuAction::ToggleShowPace => "toggle_show_pace()".into(), @@ -364,6 +366,8 @@ pub(super) fn parse_context_menu_action_script(script: &str) -> Result settings.provider_enabled(*provider), + ContextMenuAction::ToggleCodexMain => { + settings.provider_enabled(crate::providers::ProviderId::Codex) + && settings + .accounts + .codex + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true) + } + ContextMenuAction::ToggleCodexX => { + settings.provider_enabled(crate::providers::ProviderId::Codex) + && settings + .accounts + .codex + .profiles + .iter() + .find(|p| p.id == "codex_x") + .map(|p| p.enabled) + .unwrap_or(false) + } ContextMenuAction::ToggleStartup => startup_enabled, ContextMenuAction::ToggleTaskbarLock => settings.lock_taskbar, ContextMenuAction::ToggleShowPace => settings.show_pace, diff --git a/src/theme_engine.rs b/src/theme_engine.rs index 179dd529..d68aa336 100644 --- a/src/theme_engine.rs +++ b/src/theme_engine.rs @@ -1355,6 +1355,10 @@ pub struct ThemeRuntime { pub antigravity_gemini_enabled: bool, pub antigravity_external_enabled: bool, pub crossfade_opacity: f64, + pub codex_crossfade_opacity: f64, + pub codex_hovered: bool, + pub codex_main_enabled: bool, + pub codex_x_enabled: bool, host_width: u32, host_height: u32, } @@ -1374,6 +1378,10 @@ impl Default for ThemeRuntime { antigravity_gemini_enabled: true, antigravity_external_enabled: false, crossfade_opacity: 1.0, + codex_crossfade_opacity: 0.0, + codex_hovered: false, + codex_main_enabled: true, + codex_x_enabled: false, host_width: default_canvas_width(), host_height: default_canvas_height(), } @@ -1410,11 +1418,29 @@ impl ThemeRuntime { antigravity_gemini_enabled: agy, antigravity_external_enabled: false, crossfade_opacity: 1.0, + codex_crossfade_opacity: 0.0, + codex_hovered: false, + codex_main_enabled: true, + codex_x_enabled: false, host_width: default_canvas_width(), host_height: default_canvas_height(), } } + pub fn with_codex_state( + mut self, + hovered: bool, + main_enabled: bool, + x_enabled: bool, + crossfade_opacity: f64, + ) -> Self { + self.codex_hovered = hovered; + self.codex_main_enabled = main_enabled; + self.codex_x_enabled = x_enabled; + self.codex_crossfade_opacity = crossfade_opacity; + self + } + pub fn with_antigravity_pools( mut self, gemini: bool, @@ -1614,6 +1640,17 @@ impl DataContext { }; context.insert("antigravity.gemini.opacity", gemini_opacity); context.insert("antigravity.external.opacity", external_opacity); + context.insert("codex.hovered", runtime.codex_hovered as u8 as f64); + let codex_crossfade = runtime.codex_crossfade_opacity.clamp(0.0, 1.0); + context.insert("codex.crossfade_opacity", codex_crossfade); + context.insert("codex.normal.opacity", 1.0 - codex_crossfade); + context.insert("codex.hover.opacity", codex_crossfade); + context.insert("codex.main.enabled", runtime.codex_main_enabled as u8 as f64); + context.insert("codex.x.enabled", runtime.codex_x_enabled as u8 as f64); + context.insert( + "codex.dual_mode", + (runtime.codex_main_enabled && runtime.codex_x_enabled) as u8 as f64, + ); if let Some(data) = data { for account in &data.accounts { let key = format!( @@ -1634,6 +1671,14 @@ impl DataContext { runtime.countdown, ); } + if !data.accounts.iter().any(|a| a.provider == ProviderId::Codex && a.profile.id == "default") { + let codex_usage = data.get(ProviderId::Codex); + context.insert_provider("accounts.codex.default", codex_usage, true, runtime.countdown); + } + if !data.accounts.iter().any(|a| a.provider == ProviderId::Codex && a.profile.id == "codex_x") { + let codex_usage = data.get(ProviderId::Codex); + context.insert_provider("accounts.codex.codex_x", codex_usage, true, runtime.countdown); + } for descriptor in PROVIDER_DESCRIPTORS { if let Some(account) = data .accounts diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index 4ad06ec7..c83e7419 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -3996,7 +3996,7 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { .enumerate() .filter(|(_, object)| object.id.ends_with("-pace")) .collect::>(); - assert_eq!(ticks.len(), 24); + assert_eq!(ticks.len(), 32); for (index, tick) in ticks { let gauge = &children[index - 1]; assert_eq!(tick.id, format!("{}-pace", gauge.id)); @@ -4008,6 +4008,8 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { let stripped = stripped .strip_suffix(" * antigravity.gemini.opacity") .or_else(|| stripped.strip_suffix(" * antigravity.external.opacity")) + .or_else(|| stripped.strip_suffix(" * codex.normal.opacity")) + .or_else(|| stripped.strip_suffix(" * codex.hover.opacity")) .unwrap_or(stripped); let binding = stripped .strip_suffix(".expected_available * display.show_pace") @@ -4016,7 +4018,7 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { panic!("{} must have a colour background", tick.id); }; assert!(parse_color(&colour.color).is_some()); - let expected_colour = if tick.id.ends_with("-dark-pace") { + let expected_colour = if tick.id.ends_with("-dark-pace") || tick.id.contains("-dark-") { "#FFFFFFFF" } else { "#1A1A1AFF" @@ -4025,6 +4027,8 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { let mut context = DataContext::from_usage(None, &Canvas::default()); context.insert("antigravity.gemini.opacity", 1.0); context.insert("antigravity.external.opacity", 1.0); + context.insert("codex.normal.opacity", 1.0); + context.insert("codex.hover.opacity", 1.0); for count in 1..=ProviderId::ALL.len() { context.insert("providers.count", count as f64); let start = evaluate(&gauge.x.0, &context).unwrap(); @@ -4063,3 +4067,19 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { } } } + +#[test] +fn codex_dual_mode_and_crossfade_context_variables() { + let runtime = ThemeRuntime::default().with_codex_state(true, true, true, 0.75); + let canvas = Canvas::default(); + let ctx = DataContext::from_usage_with_runtime(None, &canvas, runtime); + assert_eq!(ctx.get("codex.hovered"), Some(1.0)); + assert_eq!(ctx.get("codex.main.enabled"), Some(1.0)); + assert_eq!(ctx.get("codex.x.enabled"), Some(1.0)); + assert_eq!(ctx.get("codex.dual_mode"), Some(1.0)); + assert_eq!(ctx.get("codex.crossfade_opacity"), Some(0.75)); + assert_eq!(ctx.get("codex.normal.opacity"), Some(0.25)); + assert_eq!(ctx.get("codex.hover.opacity"), Some(0.75)); +} + + diff --git a/src/themes/compact-fluent-quad.json b/src/themes/compact-fluent-quad.json index f9bebe8d..5119d4c9 100644 --- a/src/themes/compact-fluent-quad.json +++ b/src/themes/compact-fluent-quad.json @@ -21,7 +21,7 @@ "offset_x": 6, "offset_y": 0 }, - "width": "providers.count * (ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62) + max(0, providers.count - 1) * 6 + providers.opencode.enabled * 28 + providers.cursor.enabled * 18 + providers.claude.enabled * claude.model.fable.available * 96", + "width": "providers.count * (ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62) + max(0, providers.count - 1) * 6 + providers.opencode.enabled * 28 + providers.cursor.enabled * 18 + providers.claude.enabled * claude.model.fable.available * 96 + providers.codex.enabled * codex.dual_mode * 12", "height": "46", "background": { "type": "none" @@ -64,7 +64,7 @@ "render": "system.dark", "visibility": "100", "parent": "cursor-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -100,7 +100,7 @@ "visibility": "100", "parent": "cursor-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "80", "height": "13", "background": { @@ -132,7 +132,7 @@ "render": "1 - system.dark", "visibility": "100", "parent": "cursor-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -168,7 +168,7 @@ "visibility": "100", "parent": "cursor-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "80", "height": "13", "background": { @@ -376,7 +376,7 @@ "render": "(system.dark) && claude.session.available", "visibility": "100", "parent": "claude-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -412,7 +412,7 @@ "visibility": "(100) * claude.session.expected_available * display.show_pace", "parent": "claude-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.session.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(9) + 2", "background": { @@ -438,7 +438,7 @@ "visibility": "100", "parent": "claude-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -470,7 +470,7 @@ "render": "(1 - system.dark) && claude.session.available", "visibility": "100", "parent": "claude-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -506,7 +506,7 @@ "visibility": "(100) * claude.session.expected_available * display.show_pace", "parent": "claude-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.session.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(9) + 2", "background": { @@ -532,7 +532,7 @@ "visibility": "100", "parent": "claude-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -772,7 +772,7 @@ "render": "system.dark", "visibility": "100", "parent": "fable-provider", - "y": "11", + "y": "10", "width": "floor((90 + 1) / 11) * 11 - 1", "height": "9", "background": { @@ -839,7 +839,7 @@ "render": "1 - system.dark", "visibility": "100", "parent": "fable-provider", - "y": "11", + "y": "10", "width": "floor((90 + 1) / 11) * 11 - 1", "height": "9", "background": { @@ -906,7 +906,7 @@ "render": "providers.codex.enabled", "visibility": "100", "parent": "provider-row", - "width": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62 + (codex.dual_mode * 12)", "height": "46", "background": { "type": "none" @@ -923,10 +923,10 @@ { "id": "codex-session-segments-dark", "name": "dark Codex 5-hour segments", - "render": "system.dark", + "render": "(system.dark) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -958,11 +958,11 @@ { "id": "codex-session-segments-dark-pace", "name": "dark Codex 5-hour segments pace tick", - "render": "system.dark", + "render": "(system.dark) && (!codex.dual_mode)", "visibility": "(100) * codex.five_hour.expected_available * display.show_pace", "parent": "codex-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(codex.five_hour.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(9) + 2", "background": { @@ -984,11 +984,11 @@ { "id": "codex-session-value-dark", "name": "dark Codex 5-hour value", - "render": "system.dark", + "render": "(system.dark) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -1017,10 +1017,10 @@ { "id": "codex-session-segments-light", "name": "light Codex 5-hour segments", - "render": "1 - system.dark", + "render": "(1 - system.dark) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -1052,11 +1052,11 @@ { "id": "codex-session-segments-light-pace", "name": "light Codex 5-hour segments pace tick", - "render": "1 - system.dark", + "render": "(1 - system.dark) && (!codex.dual_mode)", "visibility": "(100) * codex.five_hour.expected_available * display.show_pace", "parent": "codex-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(codex.five_hour.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(9) + 2", "background": { @@ -1078,11 +1078,11 @@ { "id": "codex-session-value-light", "name": "light Codex 5-hour value", - "render": "1 - system.dark", + "render": "(1 - system.dark) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -1111,7 +1111,7 @@ { "id": "codex-weekly-segments-dark", "name": "dark Codex 7-day segments", - "render": "system.dark", + "render": "(system.dark) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "y": "27", @@ -1146,7 +1146,7 @@ { "id": "codex-weekly-segments-dark-pace", "name": "dark Codex 7-day segments pace tick", - "render": "system.dark", + "render": "(system.dark) && (!codex.dual_mode)", "visibility": "(100) * codex.weekly.expected_available * display.show_pace", "parent": "codex-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(codex.weekly.expected_display, 0, 100) / 100)", @@ -1172,7 +1172,7 @@ { "id": "codex-weekly-value-dark", "name": "dark Codex 7-day value", - "render": "(system.dark) && !codex.credits.available", + "render": "((system.dark) && !codex.credits.available) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1205,7 +1205,7 @@ { "id": "codex-weekly-segments-light", "name": "light Codex 7-day segments", - "render": "1 - system.dark", + "render": "(1 - system.dark) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "y": "27", @@ -1240,7 +1240,7 @@ { "id": "codex-weekly-segments-light-pace", "name": "light Codex 7-day segments pace tick", - "render": "1 - system.dark", + "render": "(1 - system.dark) && (!codex.dual_mode)", "visibility": "(100) * codex.weekly.expected_available * display.show_pace", "parent": "codex-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(codex.weekly.expected_display, 0, 100) / 100)", @@ -1266,7 +1266,7 @@ { "id": "codex-weekly-value-light", "name": "light Codex 7-day value", - "render": "(1 - system.dark) && !codex.credits.available", + "render": "((1 - system.dark) && !codex.credits.available) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1322,7 +1322,7 @@ "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", "visibility": "100 * antigravity.gemini.opacity", "parent": "antigravity-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -1358,7 +1358,7 @@ "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace * antigravity.gemini.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(9) + 2", "background": { @@ -1383,7 +1383,7 @@ "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", "visibility": "100 * antigravity.gemini.opacity", "parent": "antigravity-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -1419,7 +1419,7 @@ "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace * antigravity.gemini.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(9) + 2", "background": { @@ -1444,7 +1444,7 @@ "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", "visibility": "100 * antigravity.external.opacity", "parent": "antigravity-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -1480,7 +1480,7 @@ "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace * antigravity.external.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(9) + 2", "background": { @@ -1505,7 +1505,7 @@ "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.external.opacity > 0.01", "visibility": "100 * antigravity.external.opacity", "parent": "antigravity-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -1541,7 +1541,7 @@ "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace * antigravity.external.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(9) + 2", "background": { @@ -1566,7 +1566,7 @@ "render": "system.dark && antigravity.dual_mode", "visibility": "100", "parent": "antigravity-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "4", "background": { @@ -1602,7 +1602,7 @@ "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(4) + 2", "background": { @@ -1627,7 +1627,7 @@ "render": "(1 - system.dark) && antigravity.dual_mode", "visibility": "100", "parent": "antigravity-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "4", "background": { @@ -1663,7 +1663,7 @@ "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", - "y": "(11) - 1", + "y": "(10) - 1", "width": "1", "height": "(4) + 2", "background": { @@ -1811,7 +1811,7 @@ "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -1844,7 +1844,7 @@ "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -1877,7 +1877,7 @@ "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -1910,7 +1910,7 @@ "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -2582,7 +2582,7 @@ "render": "system.dark", "visibility": "100", "parent": "opencode-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -2618,7 +2618,7 @@ "visibility": "100", "parent": "opencode-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -2650,7 +2650,7 @@ "render": "1 - system.dark", "visibility": "100", "parent": "opencode-provider", - "y": "11", + "y": "10", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -2686,7 +2686,7 @@ "visibility": "100", "parent": "opencode-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "9", + "y": "8", "width": "62", "height": "13", "background": { @@ -2987,7 +2987,7 @@ { "id": "codex-credits-segments-dark", "name": "dark codex credit overlay", - "render": "(system.dark) && codex.credits.available", + "render": "((system.dark) && codex.credits.available) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "y": "27", @@ -3022,7 +3022,7 @@ { "id": "codex-credits-value-dark", "name": "dark codex credit balance", - "render": "(system.dark) && codex.credits.available", + "render": "((system.dark) && codex.credits.available) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -3055,7 +3055,7 @@ { "id": "codex-credits-segments-light", "name": "light codex credit overlay", - "render": "(1 - system.dark) && codex.credits.available", + "render": "((1 - system.dark) && codex.credits.available) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "y": "27", @@ -3090,7 +3090,7 @@ { "id": "codex-credits-value-light", "name": "light codex credit balance", - "render": "(1 - system.dark) && codex.credits.available", + "render": "((1 - system.dark) && codex.credits.available) && (!codex.dual_mode)", "visibility": "100", "parent": "codex-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -3120,6 +3120,1318 @@ } } }, + { + "id": "codex-main-marker-dark-normal", + "name": "dark Codex Main marker o (5h color)", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "0", + "y": "7", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "o", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#60A5FAFF", + "opacity": "codex.normal.opacity" + } + } + }, + { + "id": "codex-main-marker-dark-hover", + "name": "dark Codex Main marker o (7d color)", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "0", + "y": "7", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "o", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#3B82F6FF", + "opacity": "codex.hover.opacity" + } + } + }, + { + "id": "codex-main-marker-light-normal", + "name": "light Codex Main marker o (5h color)", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "0", + "y": "7", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "o", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#2563EBFF", + "opacity": "codex.normal.opacity" + } + } + }, + { + "id": "codex-main-marker-light-hover", + "name": "light Codex Main marker o (7d color)", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "0", + "y": "7", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "o", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#1E40AFFF", + "opacity": "codex.hover.opacity" + } + } + }, + { + "id": "codex-main-5h-dark-normal", + "name": "dark Codex Main 5h normal", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100 * codex.normal.opacity", + "parent": "codex-provider", + "x": "12", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.default.session.display", + "direction": "left_to_right", + "fill": { + "color": "#3B82F6FF", + "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": "codex-main-5h-dark-normal-pace", + "name": "dark Codex Main 5h normal pace tick", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "(100) * accounts.codex.default.session.expected_available * display.show_pace * codex.normal.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.default.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "codex-main-5h-dark-hover", + "name": "dark Codex Main 5h hover", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100 * codex.hover.opacity", + "parent": "codex-provider", + "x": "12", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.default.session.display", + "direction": "left_to_right", + "fill": { + "color": "#3B82F6FF", + "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": "codex-main-5h-light-normal", + "name": "light Codex Main 5h normal", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100 * codex.normal.opacity", + "parent": "codex-provider", + "x": "12", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.default.session.display", + "direction": "left_to_right", + "fill": { + "color": "#2563EBFF", + "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": "codex-main-5h-light-normal-pace", + "name": "light Codex Main 5h normal pace tick", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "(100) * accounts.codex.default.session.expected_available * display.show_pace * codex.normal.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.default.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "codex-main-5h-light-hover", + "name": "light Codex Main 5h hover", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100 * codex.hover.opacity", + "parent": "codex-provider", + "x": "12", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.default.session.display", + "direction": "left_to_right", + "fill": { + "color": "#2563EBFF", + "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": "codex-main-7d-dark-normal", + "name": "dark Codex Main 7d normal", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100 * codex.normal.opacity", + "parent": "codex-provider", + "x": "12", + "y": "16", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.default.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#1D4ED8FF", + "opacity": "1" + }, + "track": { + "color": "#333333FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "codex-main-7d-dark-hover", + "name": "dark Codex Main 7d hover", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100 * codex.hover.opacity", + "parent": "codex-provider", + "x": "12", + "y": "13", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.default.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#1D4ED8FF", + "opacity": "1" + }, + "track": { + "color": "#333333FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "codex-main-7d-dark-hover-pace", + "name": "dark Codex Main 7d hover pace tick", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "(100) * accounts.codex.default.weekly.expected_available * display.show_pace * codex.hover.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.default.weekly.expected_display, 0, 100) / 100)", + "y": "12", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "codex-main-7d-light-normal", + "name": "light Codex Main 7d normal", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100 * codex.normal.opacity", + "parent": "codex-provider", + "x": "12", + "y": "16", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.default.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#1E40AFFF", + "opacity": "1" + }, + "track": { + "color": "#999999FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "codex-main-7d-light-hover", + "name": "light Codex Main 7d hover", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100 * codex.hover.opacity", + "parent": "codex-provider", + "x": "12", + "y": "13", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.default.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#1E40AFFF", + "opacity": "1" + }, + "track": { + "color": "#999999FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "codex-main-7d-light-hover-pace", + "name": "light Codex Main 7d hover pace tick", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "(100) * accounts.codex.default.weekly.expected_available * display.show_pace * codex.hover.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.default.weekly.expected_display, 0, 100) / 100)", + "y": "12", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "codex-main-text-dark-normal", + "name": "dark Codex Main text 5h", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "7", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.codex.default.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#60A5FAFF", + "opacity": "codex.normal.opacity" + } + } + }, + { + "id": "codex-main-text-dark-hover", + "name": "dark Codex Main text 7d", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "7", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.codex.default.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#3B82F6FF", + "opacity": "codex.hover.opacity" + } + } + }, + { + "id": "codex-main-text-light-normal", + "name": "light Codex Main text 5h", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "7", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.codex.default.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#2563EBFF", + "opacity": "codex.normal.opacity" + } + } + }, + { + "id": "codex-main-text-light-hover", + "name": "light Codex Main text 7d", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "7", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.codex.default.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#1E40AFFF", + "opacity": "codex.hover.opacity" + } + } + }, + { + "id": "codex-x-marker-dark-normal", + "name": "dark Codex X marker x (5h color)", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "0", + "y": "26", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "x", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#60A5FAFF", + "opacity": "codex.normal.opacity" + } + } + }, + { + "id": "codex-x-marker-dark-hover", + "name": "dark Codex X marker x (7d color)", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "0", + "y": "26", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "x", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#3B82F6FF", + "opacity": "codex.hover.opacity" + } + } + }, + { + "id": "codex-x-marker-light-normal", + "name": "light Codex X marker x (5h color)", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "0", + "y": "26", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "x", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#2563EBFF", + "opacity": "codex.normal.opacity" + } + } + }, + { + "id": "codex-x-marker-light-hover", + "name": "light Codex X marker x (7d color)", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "0", + "y": "26", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "x", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#1E40AFFF", + "opacity": "codex.hover.opacity" + } + } + }, + { + "id": "codex-x-5h-dark-normal", + "name": "dark Codex X 5h normal", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100 * codex.normal.opacity", + "parent": "codex-provider", + "x": "12", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.codex_x.session.display", + "direction": "left_to_right", + "fill": { + "color": "#3B82F6FF", + "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": "codex-x-5h-dark-normal-pace", + "name": "dark Codex X 5h normal pace tick", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "(100) * accounts.codex.codex_x.session.expected_available * display.show_pace * codex.normal.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.codex_x.session.expected_display, 0, 100) / 100)", + "y": "26", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "codex-x-5h-dark-hover", + "name": "dark Codex X 5h hover", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100 * codex.hover.opacity", + "parent": "codex-provider", + "x": "12", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.codex_x.session.display", + "direction": "left_to_right", + "fill": { + "color": "#3B82F6FF", + "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": "codex-x-5h-light-normal", + "name": "light Codex X 5h normal", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100 * codex.normal.opacity", + "parent": "codex-provider", + "x": "12", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.codex_x.session.display", + "direction": "left_to_right", + "fill": { + "color": "#2563EBFF", + "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": "codex-x-5h-light-normal-pace", + "name": "light Codex X 5h normal pace tick", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "(100) * accounts.codex.codex_x.session.expected_available * display.show_pace * codex.normal.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.codex_x.session.expected_display, 0, 100) / 100)", + "y": "26", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "codex-x-5h-light-hover", + "name": "light Codex X 5h hover", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100 * codex.hover.opacity", + "parent": "codex-provider", + "x": "12", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.codex_x.session.display", + "direction": "left_to_right", + "fill": { + "color": "#2563EBFF", + "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": "codex-x-7d-dark-normal", + "name": "dark Codex X 7d normal", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100 * codex.normal.opacity", + "parent": "codex-provider", + "x": "12", + "y": "35", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.codex_x.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#1D4ED8FF", + "opacity": "1" + }, + "track": { + "color": "#333333FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "codex-x-7d-dark-hover", + "name": "dark Codex X 7d hover", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100 * codex.hover.opacity", + "parent": "codex-provider", + "x": "12", + "y": "32", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.codex_x.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#1D4ED8FF", + "opacity": "1" + }, + "track": { + "color": "#333333FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "codex-x-7d-dark-hover-pace", + "name": "dark Codex X 7d hover pace tick", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "(100) * accounts.codex.codex_x.weekly.expected_available * display.show_pace * codex.hover.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.codex_x.weekly.expected_display, 0, 100) / 100)", + "y": "31", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "codex-x-7d-light-normal", + "name": "light Codex X 7d normal", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100 * codex.normal.opacity", + "parent": "codex-provider", + "x": "12", + "y": "35", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.codex_x.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#1E40AFFF", + "opacity": "1" + }, + "track": { + "color": "#999999FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "codex-x-7d-light-hover", + "name": "light Codex X 7d hover", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100 * codex.hover.opacity", + "parent": "codex-provider", + "x": "12", + "y": "32", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.codex.codex_x.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#1E40AFFF", + "opacity": "1" + }, + "track": { + "color": "#999999FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "codex-x-7d-light-hover-pace", + "name": "light Codex X 7d hover pace tick", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "(100) * accounts.codex.codex_x.weekly.expected_available * display.show_pace * codex.hover.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.codex_x.weekly.expected_display, 0, 100) / 100)", + "y": "31", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "codex-x-text-dark-normal", + "name": "dark Codex X text 5h", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "26", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.codex.codex_x.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#60A5FAFF", + "opacity": "codex.normal.opacity" + } + } + }, + { + "id": "codex-x-text-dark-hover", + "name": "dark Codex X text 7d", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "26", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.codex.codex_x.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#3B82F6FF", + "opacity": "codex.hover.opacity" + } + } + }, + { + "id": "codex-x-text-light-normal", + "name": "light Codex X text 5h", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "26", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.codex.codex_x.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#2563EBFF", + "opacity": "codex.normal.opacity" + } + } + }, + { + "id": "codex-x-text-light-hover", + "name": "light Codex X text 7d", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "100", + "parent": "codex-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "26", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.codex.codex_x.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#1E40AFFF", + "opacity": "codex.hover.opacity" + } + } + }, { "id": "grok-provider", "name": "Grok provider", @@ -5876,4 +7188,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index 5704069a..141d6f35 100644 --- a/src/window.rs +++ b/src/window.rs @@ -104,6 +104,10 @@ struct AppState { antigravity_hovered: bool, crossfade_animation: Option, pub(super) antigravity_tile_rect: Option<(f64, f64, f64, f64)>, + pub(super) codex_hovered: bool, + pub(super) codex_crossfade_opacity: f64, + pub(super) codex_crossfade_animation: Option, + pub(super) codex_tile_rect: Option<(f64, f64, f64, f64)>, accounts: crate::accounts::AccountSettings, data: Option, @@ -221,6 +225,8 @@ const IDM_DASHBOARD: u16 = 71; // 62 stays the whole-provider Antigravity toggle used by saved custom menus. pub(super) const IDM_PROVIDER_ANTIGRAVITY_GEMINI: u16 = 68; pub(super) const IDM_PROVIDER_ANTIGRAVITY_EXTERNAL: u16 = 67; +pub(super) const IDM_PROVIDER_CODEX_MAIN: u16 = 69; +pub(super) const IDM_PROVIDER_CODEX_X: u16 = 70; const WM_DPICHANGED_MSG: u32 = 0x02E0; const WM_APP_UPDATE_CHECK_COMPLETE: u32 = WM_APP + 2; @@ -808,6 +814,28 @@ fn theme_runtime_from_state(state: &AppState) -> ThemeRuntime { state.show_antigravity_external, state.crossfade_opacity, ) + .with_codex_state( + state.codex_hovered, + state.providers.contains(ProviderId::Codex) + && state + .accounts + .codex + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true), + state.providers.contains(ProviderId::Codex) + && state + .accounts + .codex + .profiles + .iter() + .find(|p| p.id == "codex_x") + .map(|p| p.enabled) + .unwrap_or(false), + state.codex_crossfade_opacity, + ) } /// A transient outage can keep presenting the last real reading while its @@ -1046,6 +1074,7 @@ fn save_state_settings() { persisted.placement_override = s.placement_override.clone(); persisted.floating_card_opacity = s.floating_card_opacity; persisted.lock_taskbar = s.lock_taskbar; + persisted.accounts = s.accounts.clone(); // The dashboard process owns its dimensions, so leave the freshly // loaded values unchanged when monitor actions persist settings. if let Err(error) = save_settings(&persisted) { @@ -1935,6 +1964,7 @@ fn replace_active_theme(state: &mut AppState, loaded: ThemeDocument, path: Optio } state.active_theme = Some(loaded); state.antigravity_tile_rect = None; + state.codex_tile_rect = None; if path.is_some() { state.active_theme_path = path; } @@ -2601,6 +2631,10 @@ pub fn run() { antigravity_hovered: false, crossfade_animation: None, antigravity_tile_rect: None, + codex_hovered: false, + codex_crossfade_opacity: 0.0, + codex_crossfade_animation: None, + codex_tile_rect: None, accounts: settings.accounts.clone(), data: None, poll_interval_ms: settings.poll_interval_ms, @@ -2779,6 +2813,22 @@ fn render_layered() { state.antigravity_tile_rect = tile_rect; } } + let needs_codex_rect = lock_state() + .as_ref() + .map(|s| s.codex_tile_rect.is_none()) + .unwrap_or(false); + if needs_codex_rect { + let tile_rect = theme_engine::object_bounds( + &theme, + 0, + usage_data.as_ref(), + surface_runtime, + mouse::CODEX_HOVER_OBJECT, + ); + if let Some(state) = lock_state().as_mut() { + state.codex_tile_rect = tile_rect; + } + } } let nest = surface .placement @@ -3395,6 +3445,7 @@ fn check_language_change() { } const ANTIGRAVITY_CROSSFADE_DURATION_MS: u64 = 70; +const CODEX_CROSSFADE_DURATION_MS: u64 = 300; const ANTIGRAVITY_CROSSFADE_TICK_MS: u32 = 16; #[derive(Clone, Copy, Debug)] @@ -3461,8 +3512,14 @@ pub(super) fn tick_antigravity_crossfade(hwnd: HWND, throttle: bool) { (true, more) }; if !still_animating { - unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + let any_animating = { + let state = lock_state(); + state.as_ref().is_some_and(|s| s.crossfade_animation.is_some() || s.codex_crossfade_animation.is_some()) + }; + if !any_animating { + unsafe { + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } } } if should_render { @@ -3470,7 +3527,99 @@ pub(super) fn tick_antigravity_crossfade(hwnd: HWND, throttle: bool) { } } -/// Starts the crossfade when the cursor enters or leaves the Antigravity tile. +pub(super) fn tick_codex_crossfade(hwnd: HWND, throttle: bool) { + let (should_render, still_animating) = { + let mut state = lock_state(); + let Some(state) = state.as_mut() else { + return; + }; + let Some(mut anim) = state.codex_crossfade_animation else { + return; + }; + let now = std::time::Instant::now(); + if throttle && now.duration_since(anim.last_rendered) < std::time::Duration::from_millis(15) + { + return; + } + let (opacity, more) = anim.current_opacity(); + state.codex_crossfade_opacity = opacity; + anim.last_rendered = now; + if more { + state.codex_crossfade_animation = Some(anim); + } else { + state.codex_crossfade_opacity = anim.target_opacity; + state.codex_crossfade_animation = None; + } + (true, more) + }; + if !still_animating { + let any_animating = { + let state = lock_state(); + state.as_ref().is_some_and(|s| s.crossfade_animation.is_some() || s.codex_crossfade_animation.is_some()) + }; + if !any_animating { + unsafe { + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } + } + } + if should_render { + render_layered(); + } +} + +/// Updates the hover state when the cursor enters or leaves the Codex tile with a 300ms crossfade animation. +pub(super) fn set_codex_hover(hwnd: HWND, hovered: bool) { + let mut should_animate = false; + { + let mut state = lock_state(); + let Some(state) = state.as_mut() else { + return; + }; + let dual_codex = state.providers.contains(ProviderId::Codex) + && state + .accounts + .codex + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true) + && state + .accounts + .codex + .profiles + .iter() + .find(|p| p.id == "codex_x") + .map(|p| p.enabled) + .unwrap_or(false); + + if state.codex_hovered != hovered { + state.codex_hovered = hovered; + if dual_codex { + let target = if hovered { 1.0 } else { 0.0 }; + state.codex_crossfade_animation = Some(CrossfadeAnimation::new( + state.codex_crossfade_opacity, + target, + std::time::Duration::from_millis(CODEX_CROSSFADE_DURATION_MS), + )); + should_animate = true; + } + } + } + if should_animate { + unsafe { + let _ = set_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ANTIGRAVITY_CROSSFADE_TICK_MS, + None, + ); + } + render_layered(); + } +} + pub(super) fn set_antigravity_hover(hwnd: HWND, hovered: bool) { let mut should_animate = false; { @@ -3537,14 +3686,23 @@ fn reload_external_settings(hwnd: HWND) { let any_pool = (state.show_antigravity_gemini || state.show_antigravity_external) && state.providers.contains(ProviderId::Antigravity); if !any_pool { - unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); - } state.antigravity_hovered = false; state.crossfade_opacity = 1.0; state.crossfade_animation = None; } + let codex_active = state.providers.contains(ProviderId::Codex); + if !codex_active { + state.codex_hovered = false; + state.codex_crossfade_opacity = 0.0; + state.codex_crossfade_animation = None; + } + if !any_pool && !codex_active { + unsafe { + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } + } state.antigravity_tile_rect = None; + state.codex_tile_rect = None; update_configured_placement( state, settings.placement_override, diff --git a/src/window/message_loop.rs b/src/window/message_loop.rs index cc6df474..b30ca4dc 100644 --- a/src/window/message_loop.rs +++ b/src/window/message_loop.rs @@ -132,6 +132,15 @@ pub(super) unsafe extern "system" fn wnd_proc( } TIMER_CROSSFADE_SCHEDULE => { tick_antigravity_crossfade(hwnd, false); + tick_codex_crossfade(hwnd, false); + let any_animating = lock_state() + .as_ref() + .is_some_and(|s| s.crossfade_animation.is_some() || s.codex_crossfade_animation.is_some()); + if !any_animating { + unsafe { + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } + } } _ => {} } @@ -145,6 +154,7 @@ pub(super) unsafe extern "system" fn wnd_proc( WM_APP_USAGE_UPDATED => { if let Some(state) = lock_state().as_mut() { state.antigravity_tile_rect = None; + state.codex_tile_rect = None; } check_theme_change(); check_language_change(); @@ -856,16 +866,19 @@ pub(super) unsafe extern "system" fn wnd_proc( || s.show_antigravity_external) && s.providers.contains(ProviderId::Antigravity); s.antigravity_tile_rect = None; + s.codex_tile_rect = None; if !any_pool { - unsafe { - let _ = kill_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ); - } s.antigravity_hovered = false; s.crossfade_opacity = 1.0; s.crossfade_animation = None; + if s.codex_crossfade_animation.is_none() { + unsafe { + let _ = kill_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ); + } + } } } } @@ -878,6 +891,100 @@ pub(super) unsafe extern "system" fn wnd_proc( sync_tray_icon(hwnd); request_poll(hwnd); } + IDM_PROVIDER_CODEX_MAIN | IDM_PROVIDER_CODEX_X => { + let mut should_reposition = false; + { + let mut state = lock_state(); + if let Some(s) = state.as_mut() { + let is_main = id == IDM_PROVIDER_CODEX_MAIN; + let target_id = if is_main { "default" } else { "codex_x" }; + if is_main && !s.accounts.codex.profiles.iter().any(|p| p.id == "default") { + s.accounts.codex.profiles.push(crate::accounts::AccountProfile { + id: "default".into(), + name: "Default".into(), + config_dir: String::new(), + credentials_path: String::new(), + enabled: true, + }); + s.accounts.codex.used_ids.insert("default".into()); + } else if !is_main && !s.accounts.codex.profiles.iter().any(|p| p.id == "codex_x") { + s.accounts.codex.profiles.push(crate::accounts::AccountProfile { + id: "codex_x".into(), + name: "Codex X".into(), + config_dir: "~/.codex_x".into(), + credentials_path: String::new(), + enabled: false, + }); + s.accounts.codex.used_ids.insert("codex_x".into()); + } + let mut main_enabled = s.providers.contains(ProviderId::Codex) + && s.accounts + .codex + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true); + let mut x_enabled = s.providers.contains(ProviderId::Codex) + && s.accounts + .codex + .profiles + .iter() + .find(|p| p.id == "codex_x") + .map(|p| p.enabled) + .unwrap_or(false); + let current = if is_main { main_enabled } else { x_enabled }; + let other = if is_main { x_enabled } else { main_enabled }; + let target = !current; + if !target + && !other + && s.providers.len() <= 1 + && s.providers.contains(ProviderId::Codex) + { + // Keep at least one provider active + } else { + if let Some(p) = s + .accounts + .codex + .profiles + .iter_mut() + .find(|p| p.id == target_id) + { + p.enabled = target; + } + if is_main { + main_enabled = target; + } else { + x_enabled = target; + } + let any_codex = main_enabled || x_enabled; + if !any_codex { + s.providers.set(ProviderId::Codex, false); + s.codex_hovered = false; + s.codex_crossfade_opacity = 0.0; + s.codex_crossfade_animation = None; + if s.crossfade_animation.is_none() { + unsafe { + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } + } + should_reposition = true; + } else if !s.providers.contains(ProviderId::Codex) { + s.providers.set(ProviderId::Codex, true); + should_reposition = true; + } + s.codex_tile_rect = None; + } + } + } + save_state_settings(); + if should_reposition { + position_at_taskbar(); + } + render_layered(); + sync_tray_icon(hwnd); + request_poll(hwnd); + } id if ProviderId::from_native_menu_command_id(id).is_some() => { { let mut state = lock_state(); @@ -887,17 +994,30 @@ pub(super) unsafe extern "system" fn wnd_proc( let changed = s.providers.toggle(provider); // Tile widths depend on the provider count. s.antigravity_tile_rect = None; + s.codex_tile_rect = None; + if !s.providers.contains(ProviderId::Codex) { + s.codex_hovered = false; + s.codex_crossfade_opacity = 0.0; + s.codex_crossfade_animation = None; + if s.crossfade_animation.is_none() { + unsafe { + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } + } + } if provider == ProviderId::Antigravity && changed { let enabled = s.providers.contains(ProviderId::Antigravity); s.show_antigravity_gemini = enabled; s.show_antigravity_external = enabled; if !enabled { - let _ = - kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); s.antigravity_hovered = false; s.crossfade_opacity = 1.0; s.crossfade_animation = None; s.antigravity_tile_rect = None; + if s.codex_crossfade_animation.is_none() { + let _ = + kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } } } } diff --git a/src/window/mouse.rs b/src/window/mouse.rs index c72c33e0..fb936efb 100644 --- a/src/window/mouse.rs +++ b/src/window/mouse.rs @@ -197,6 +197,30 @@ pub(super) fn dispatch_double_click(hwnd: HWND, surface_index: usize, object_id: /// Theme object whose hover reveals the Antigravity Claude/GPT pool. pub(super) const ANTIGRAVITY_HOVER_OBJECT: &str = "antigravity-provider"; +pub(super) const CODEX_HOVER_OBJECT: &str = "codex-provider"; + +fn codex_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { + let state = lock_state(); + let Some(state) = state.as_ref() else { + return false; + }; + if surface_index_for_window(state, hwnd) != Some(0) { + return false; + } + let Some(rect) = state.codex_tile_rect else { + return false; + }; + let Some(theme) = state.active_theme.as_ref() else { + return false; + }; + let scale = theme_surface_scale(theme, 0).max(0.01); + let (x, y) = mouse_client_point(lparam); + let lx = x / scale; + let ly = y / scale; + lx >= rect.0 && lx < rect.0 + rect.2 && ly >= rect.1 && ly < rect.1 + rect.3 +} + + fn antigravity_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { let state = lock_state(); @@ -224,7 +248,9 @@ pub(super) fn update_mouse_hover(hwnd: HWND, lparam: LPARAM) { TrackMouseEvent(tracking) }); set_antigravity_hover(hwnd, antigravity_hover_at(hwnd, lparam)); + super::set_codex_hover(hwnd, codex_hover_at(hwnd, lparam)); super::tick_antigravity_crossfade(hwnd, true); + super::tick_codex_crossfade(hwnd, true); for _ in 0..4 { let target = mouse_target_at(hwnd, lparam); let previous = { @@ -288,6 +314,7 @@ fn track_mouse_hover_with( pub(super) fn clear_mouse_hover(hwnd: HWND) { widget_tooltip::hide_for(hwnd); set_antigravity_hover(hwnd, false); + super::set_codex_hover(hwnd, false); let previous = { let mut state = lock_state(); let Some(state) = state.as_mut() else { diff --git a/src/window/placement_regression_tests.rs b/src/window/placement_regression_tests.rs index dff6ec63..b231c76b 100644 --- a/src/window/placement_regression_tests.rs +++ b/src/window/placement_regression_tests.rs @@ -18,6 +18,10 @@ pub(super) fn state_for(theme: ThemeDocument, placement: PlacementOverride) -> A antigravity_hovered: false, crossfade_animation: None, antigravity_tile_rect: None, + codex_hovered: false, + codex_crossfade_opacity: 0.0, + codex_crossfade_animation: None, + codex_tile_rect: None, accounts: Default::default(), data: None, poll_interval_ms: POLL_15_MIN, diff --git a/src/window/window_context_menu.rs b/src/window/window_context_menu.rs index 8236a49a..36558260 100644 --- a/src/window/window_context_menu.rs +++ b/src/window/window_context_menu.rs @@ -147,6 +147,28 @@ pub(super) fn context_menu_action_flags( ContextMenuAction::ToggleProvider { provider } => state.providers.contains(*provider), ContextMenuAction::ToggleAntigravityGemini => state.show_antigravity_gemini, ContextMenuAction::ToggleAntigravityExternal => state.show_antigravity_external, + ContextMenuAction::ToggleCodexMain => { + state.providers.contains(ProviderId::Codex) + && state + .accounts + .codex + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true) + } + ContextMenuAction::ToggleCodexX => { + state.providers.contains(ProviderId::Codex) + && state + .accounts + .codex + .profiles + .iter() + .find(|p| p.id == "codex_x") + .map(|p| p.enabled) + .unwrap_or(false) + } ContextMenuAction::ToggleStartup => is_startup_enabled(), ContextMenuAction::ToggleTaskbarLock => state.lock_taskbar, ContextMenuAction::ToggleShowPace => state.show_pace, @@ -272,6 +294,8 @@ pub(super) fn execute_context_menu_action( } ContextMenuAction::ToggleAntigravityGemini => Some(IDM_PROVIDER_ANTIGRAVITY_GEMINI), ContextMenuAction::ToggleAntigravityExternal => Some(IDM_PROVIDER_ANTIGRAVITY_EXTERNAL), + ContextMenuAction::ToggleCodexMain => Some(IDM_PROVIDER_CODEX_MAIN), + ContextMenuAction::ToggleCodexX => Some(IDM_PROVIDER_CODEX_X), ContextMenuAction::ToggleStartup => Some(IDM_START_WITH_WINDOWS), ContextMenuAction::ToggleTaskbarLock => Some(IDM_LOCK_TASKBAR), ContextMenuAction::ToggleShowPace => Some(IDM_SHOW_PACE), From c7ed1cd92b4e0252bca58cebddeee2bc069e0f37 Mon Sep 17 00:00:00 2001 From: fix2it Date: Thu, 29 Oct 2026 01:47:54 +0200 Subject: [PATCH 08/17] feat: dual mode for Claude Code and Antigravity with 300ms hover crossfade and pace ticks - Claude Code dual profile support (Main and X) with dynamic tile width, styled o/x markers, and warm terracotta color scheme - Antigravity dual pool layout (Row 1 Gemini, Row 2 External) with 5h session and 7d weekly gauges and pace ticks - Unified 300ms hover crossfade animation across all three dual-mode providers with coordinated timer lifecycle - All 64 pace ticks aligned and verified across dark and light themes - Full 14-locale localization for Claude dual-mode menu controls --- src/context_menu.rs | 19 +- src/localization/locales/de.toml | 2 + src/localization/locales/en.toml | 2 + src/localization/locales/es.toml | 2 + src/localization/locales/fr.toml | 2 + src/localization/locales/ja.toml | 2 + src/localization/locales/ko.toml | 2 + src/localization/locales/nl.toml | 2 + src/localization/locales/pl.toml | 2 + src/localization/locales/pt-BR.toml | 2 + src/localization/locales/ru.toml | 2 + src/localization/locales/th.toml | 2 + src/localization/locales/tr.toml | 2 + src/localization/locales/zh-CN.toml | 2 + src/localization/locales/zh-TW.toml | 2 + src/studio_app/studio_context_menu_editor.rs | 4 + src/studio_app/studio_native_menu.rs | 22 + src/theme_engine.rs | 99 +- src/theme_engine/tests.rs | 151 +- src/themes/compact-fluent-quad.json | 3360 +++++++++++++++--- src/window.rs | 172 +- src/window/message_loop.rs | 215 +- src/window/mouse.rs | 27 +- src/window/placement_regression_tests.rs | 4 + src/window/window_context_menu.rs | 24 + 25 files changed, 3535 insertions(+), 590 deletions(-) diff --git a/src/context_menu.rs b/src/context_menu.rs index 6272a3b1..74f67ba0 100644 --- a/src/context_menu.rs +++ b/src/context_menu.rs @@ -66,6 +66,8 @@ pub enum ContextMenuAction { }, ToggleAntigravityGemini, ToggleAntigravityExternal, + ToggleClaudeMain, + ToggleClaudeX, ToggleCodexMain, ToggleCodexX, ToggleStartup, @@ -313,6 +315,8 @@ pub fn rendered_label( "Refresh" => language.text("Refresh"), "Exit" => language.text("Exit"), "Claude Code" => language.text("Claude Code"), + "Claude Code (Main)" => language.text("Claude Code (Main)"), + "Claude Code X" => language.text("Claude Code X"), "Codex" => language.text("Codex"), "Codex (Main)" => language.text("Codex (Main)"), "Codex X" => language.text("Codex X"), @@ -379,22 +383,17 @@ pub fn classic_context_menu() -> ContextMenuDocument { "Providers", vec![ ContextMenuItem::action( - "provider-claude", - "Claude Code", - Action::ToggleProvider { - provider: Provider::Claude, - }, + "provider-claude-main", + "Claude Code (Main)", + Action::ToggleClaudeMain, ), + ContextMenuItem::action("provider-claude-x", "Claude Code X", Action::ToggleClaudeX), ContextMenuItem::action( "provider-codex-main", "Codex (Main)", Action::ToggleCodexMain, ), - ContextMenuItem::action( - "provider-codex-x", - "Codex X", - Action::ToggleCodexX, - ), + ContextMenuItem::action("provider-codex-x", "Codex X", Action::ToggleCodexX), ContextMenuItem::action( "provider-antigravity-gemini", "Antigravity (Gemini)", diff --git a/src/localization/locales/de.toml b/src/localization/locales/de.toml index 0a009f56..bde4f66a 100644 --- a/src/localization/locales/de.toml +++ b/src/localization/locales/de.toml @@ -125,6 +125,8 @@ copilot_window_title = "Copilot-Nutzungsmonitor" "Choose asset..." = "Medium auswählen..." "Choose one action for this context menu item." = "Wählen Sie eine Aktion für diesen Kontextmenüeintrag." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "Baukasten schließen" diff --git a/src/localization/locales/en.toml b/src/localization/locales/en.toml index d651181c..6d5eef3f 100644 --- a/src/localization/locales/en.toml +++ b/src/localization/locales/en.toml @@ -125,6 +125,8 @@ copilot_window_title = "Copilot Usage Monitor" "Choose asset..." = "Choose asset..." "Choose one action for this context menu item." = "Choose one action for this context menu item." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "Close Builder" diff --git a/src/localization/locales/es.toml b/src/localization/locales/es.toml index 89a0cd87..f89531c8 100644 --- a/src/localization/locales/es.toml +++ b/src/localization/locales/es.toml @@ -125,6 +125,8 @@ copilot_window_title = "Monitor de uso de Copilot" "Choose asset..." = "Elegir recurso..." "Choose one action for this context menu item." = "Elija una acción para este elemento del menú contextual." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "Cerrar Constructor" diff --git a/src/localization/locales/fr.toml b/src/localization/locales/fr.toml index eb1466d6..be7e6008 100644 --- a/src/localization/locales/fr.toml +++ b/src/localization/locales/fr.toml @@ -125,6 +125,8 @@ copilot_window_title = "Moniteur d'utilisation Copilot" "Choose asset..." = "Choisir une ressource..." "Choose one action for this context menu item." = "Choisissez une action pour cet élément du menu contextuel." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "Fermer le Constructeur" diff --git a/src/localization/locales/ja.toml b/src/localization/locales/ja.toml index 15ecd04d..f85c2fee 100644 --- a/src/localization/locales/ja.toml +++ b/src/localization/locales/ja.toml @@ -127,6 +127,8 @@ copilot_window_title = "Copilot 使用量モニター" "Choose asset..." = "アセットを選択..." "Choose one action for this context menu item." = "このコンテキストメニュー項目のアクションを1つ選択します。" "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "ビルダーを閉じる" diff --git a/src/localization/locales/ko.toml b/src/localization/locales/ko.toml index fa6047c3..ab414b2f 100644 --- a/src/localization/locales/ko.toml +++ b/src/localization/locales/ko.toml @@ -127,6 +127,8 @@ copilot_window_title = "Copilot 사용량 모니터" "Choose asset..." = "에셋 선택..." "Choose one action for this context menu item." = "이 컨텍스트 메뉴 항목에 사용할 작업 하나를 선택하세요." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "빌더 닫기" diff --git a/src/localization/locales/nl.toml b/src/localization/locales/nl.toml index 52149bc8..79dceb84 100644 --- a/src/localization/locales/nl.toml +++ b/src/localization/locales/nl.toml @@ -125,6 +125,8 @@ copilot_window_title = "Copilot-gebruiksmonitor" "Choose asset..." = "Media kiezen..." "Choose one action for this context menu item." = "Kies één actie voor dit contextmenu-item." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Mediabibliotheek sluiten" "Close Builder" = "Bouwer sluiten" diff --git a/src/localization/locales/pl.toml b/src/localization/locales/pl.toml index 9e8e0861..a7d24a3f 100644 --- a/src/localization/locales/pl.toml +++ b/src/localization/locales/pl.toml @@ -125,6 +125,8 @@ copilot_window_title = "Monitor zużycia Copilot" "Choose asset..." = "Wybierz zasób..." "Choose one action for this context menu item." = "Wybierz jedną akcję dla tego elementu menu kontekstowego." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Kliknięcie" "Close asset library" = "Zamknij bibliotekę zasobów" "Close Builder" = "Zamknij kreator" diff --git a/src/localization/locales/pt-BR.toml b/src/localization/locales/pt-BR.toml index 8232d211..cd2a2685 100644 --- a/src/localization/locales/pt-BR.toml +++ b/src/localization/locales/pt-BR.toml @@ -125,6 +125,8 @@ copilot_window_title = "Monitor de uso do Copilot" "Choose asset..." = "Escolher recurso..." "Choose one action for this context menu item." = "Escolha uma ação para este item do menu de contexto." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "Fechar Construtor" diff --git a/src/localization/locales/ru.toml b/src/localization/locales/ru.toml index efdbe6ef..554f6f9b 100644 --- a/src/localization/locales/ru.toml +++ b/src/localization/locales/ru.toml @@ -125,6 +125,8 @@ copilot_window_title = "Монитор использования Copilot" "Choose asset..." = "Выбрать ресурс..." "Choose one action for this context menu item." = "Выберите одно действие для этого пункта контекстного меню." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Основной)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "Закрыть конструктор" diff --git a/src/localization/locales/th.toml b/src/localization/locales/th.toml index b0e82dcc..fdc7cf08 100644 --- a/src/localization/locales/th.toml +++ b/src/localization/locales/th.toml @@ -127,6 +127,8 @@ copilot_window_title = "ตัวติดตามการใช้งาน C "Choose asset..." = "เลือกแอสเซท..." "Choose one action for this context menu item." = "เลือกหนึ่งแอ็กชันสำหรับรายการเมนูนี้" "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "คลิก" "Close asset library" = "ปิดคลังแอสเซท" "Close Builder" = "ปิดตัวสร้าง" diff --git a/src/localization/locales/tr.toml b/src/localization/locales/tr.toml index 7fc05f49..713d200a 100644 --- a/src/localization/locales/tr.toml +++ b/src/localization/locales/tr.toml @@ -125,6 +125,8 @@ copilot_window_title = "Copilot Kullanım İzleyicisi" "Choose asset..." = "Varlık seç..." "Choose one action for this context menu item." = "Bu bağlam menüsü öğesi için bir eylem seçin." "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "Oluşturucuyu kapat" diff --git a/src/localization/locales/zh-CN.toml b/src/localization/locales/zh-CN.toml index 671be669..b23d2332 100644 --- a/src/localization/locales/zh-CN.toml +++ b/src/localization/locales/zh-CN.toml @@ -127,6 +127,8 @@ copilot_window_title = "Copilot 使用量监控" "Choose asset..." = "选择资源..." "Choose one action for this context menu item." = "为此上下文菜单项选择一个操作。" "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "关闭构建器" diff --git a/src/localization/locales/zh-TW.toml b/src/localization/locales/zh-TW.toml index 8b164705..17e92689 100644 --- a/src/localization/locales/zh-TW.toml +++ b/src/localization/locales/zh-TW.toml @@ -127,6 +127,8 @@ copilot_window_title = "Copilot 使用量監控" "Choose asset..." = "選擇資源..." "Choose one action for this context menu item." = "為此快顯功能表項目選擇一個動作。" "Claude Code" = "Claude Code" +"Claude Code (Main)" = "Claude Code (Main)" +"Claude Code X" = "Claude Code X" "Click" = "Click" "Close asset library" = "Close asset library" "Close Builder" = "關閉建構器" diff --git a/src/studio_app/studio_context_menu_editor.rs b/src/studio_app/studio_context_menu_editor.rs index 8d7ec9c4..bac680ae 100644 --- a/src/studio_app/studio_context_menu_editor.rs +++ b/src/studio_app/studio_context_menu_editor.rs @@ -323,6 +323,8 @@ pub(super) fn context_menu_action_script(action: &ContextMenuAction) -> String { } ContextMenuAction::ToggleAntigravityGemini => "toggle_antigravity_gemini()".into(), ContextMenuAction::ToggleAntigravityExternal => "toggle_antigravity_external()".into(), + ContextMenuAction::ToggleClaudeMain => "toggle_claude_main()".into(), + ContextMenuAction::ToggleClaudeX => "toggle_claude_x()".into(), ContextMenuAction::ToggleCodexMain => "toggle_codex_main()".into(), ContextMenuAction::ToggleCodexX => "toggle_codex_x()".into(), ContextMenuAction::ToggleStartup => "toggle_startup()".into(), @@ -366,6 +368,8 @@ pub(super) fn parse_context_menu_action_script(script: &str) -> Result settings.provider_enabled(*provider), + ContextMenuAction::ToggleClaudeMain => { + settings.provider_enabled(crate::providers::ProviderId::Claude) + && settings + .accounts + .claude + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true) + } + ContextMenuAction::ToggleClaudeX => { + settings.provider_enabled(crate::providers::ProviderId::Claude) + && settings + .accounts + .claude + .profiles + .iter() + .find(|p| p.id == "claude_x") + .map(|p| p.enabled) + .unwrap_or(false) + } ContextMenuAction::ToggleCodexMain => { settings.provider_enabled(crate::providers::ProviderId::Codex) && settings diff --git a/src/theme_engine.rs b/src/theme_engine.rs index d68aa336..b5f304be 100644 --- a/src/theme_engine.rs +++ b/src/theme_engine.rs @@ -1355,6 +1355,10 @@ pub struct ThemeRuntime { pub antigravity_gemini_enabled: bool, pub antigravity_external_enabled: bool, pub crossfade_opacity: f64, + pub claude_crossfade_opacity: f64, + pub claude_hovered: bool, + pub claude_main_enabled: bool, + pub claude_x_enabled: bool, pub codex_crossfade_opacity: f64, pub codex_hovered: bool, pub codex_main_enabled: bool, @@ -1378,6 +1382,10 @@ impl Default for ThemeRuntime { antigravity_gemini_enabled: true, antigravity_external_enabled: false, crossfade_opacity: 1.0, + claude_crossfade_opacity: 0.0, + claude_hovered: false, + claude_main_enabled: true, + claude_x_enabled: false, codex_crossfade_opacity: 0.0, codex_hovered: false, codex_main_enabled: true, @@ -1418,6 +1426,10 @@ impl ThemeRuntime { antigravity_gemini_enabled: agy, antigravity_external_enabled: false, crossfade_opacity: 1.0, + claude_crossfade_opacity: 0.0, + claude_hovered: false, + claude_main_enabled: true, + claude_x_enabled: false, codex_crossfade_opacity: 0.0, codex_hovered: false, codex_main_enabled: true, @@ -1427,6 +1439,20 @@ impl ThemeRuntime { } } + pub fn with_claude_state( + mut self, + hovered: bool, + main_enabled: bool, + x_enabled: bool, + crossfade_opacity: f64, + ) -> Self { + self.claude_hovered = hovered; + self.claude_main_enabled = main_enabled; + self.claude_x_enabled = x_enabled; + self.claude_crossfade_opacity = crossfade_opacity; + self + } + pub fn with_codex_state( mut self, hovered: bool, @@ -1618,6 +1644,8 @@ impl DataContext { context.insert("antigravity.dual_mode", agy_dual_mode as u8 as f64); let crossfade = runtime.crossfade_opacity.clamp(0.0, 1.0); context.insert("antigravity.crossfade_opacity", crossfade); + context.insert("antigravity.normal.opacity", crossfade); + context.insert("antigravity.hover.opacity", 1.0 - crossfade); let gemini_opacity = if crossfade > 0.5 { (crossfade - 0.5) * 2.0 } else { @@ -1640,12 +1668,29 @@ impl DataContext { }; context.insert("antigravity.gemini.opacity", gemini_opacity); context.insert("antigravity.external.opacity", external_opacity); + context.insert("claude.hovered", runtime.claude_hovered as u8 as f64); + let claude_crossfade = runtime.claude_crossfade_opacity.clamp(0.0, 1.0); + context.insert("claude.crossfade_opacity", claude_crossfade); + context.insert("claude.normal.opacity", 1.0 - claude_crossfade); + context.insert("claude.hover.opacity", claude_crossfade); + context.insert( + "claude.main.enabled", + runtime.claude_main_enabled as u8 as f64, + ); + context.insert("claude.x.enabled", runtime.claude_x_enabled as u8 as f64); + context.insert( + "claude.dual_mode", + (runtime.claude_main_enabled && runtime.claude_x_enabled) as u8 as f64, + ); context.insert("codex.hovered", runtime.codex_hovered as u8 as f64); let codex_crossfade = runtime.codex_crossfade_opacity.clamp(0.0, 1.0); context.insert("codex.crossfade_opacity", codex_crossfade); context.insert("codex.normal.opacity", 1.0 - codex_crossfade); context.insert("codex.hover.opacity", codex_crossfade); - context.insert("codex.main.enabled", runtime.codex_main_enabled as u8 as f64); + context.insert( + "codex.main.enabled", + runtime.codex_main_enabled as u8 as f64, + ); context.insert("codex.x.enabled", runtime.codex_x_enabled as u8 as f64); context.insert( "codex.dual_mode", @@ -1671,13 +1716,57 @@ impl DataContext { runtime.countdown, ); } - if !data.accounts.iter().any(|a| a.provider == ProviderId::Codex && a.profile.id == "default") { + if !data + .accounts + .iter() + .any(|a| a.provider == ProviderId::Claude && a.profile.id == "default") + { + let claude_usage = data.get(ProviderId::Claude); + context.insert_provider( + "accounts.claude.default", + claude_usage, + false, + runtime.countdown, + ); + } + if !data + .accounts + .iter() + .any(|a| a.provider == ProviderId::Claude && a.profile.id == "claude_x") + { + let claude_usage = data.get(ProviderId::Claude); + context.insert_provider( + "accounts.claude.claude_x", + claude_usage, + false, + runtime.countdown, + ); + } + if !data + .accounts + .iter() + .any(|a| a.provider == ProviderId::Codex && a.profile.id == "default") + { let codex_usage = data.get(ProviderId::Codex); - context.insert_provider("accounts.codex.default", codex_usage, true, runtime.countdown); + context.insert_provider( + "accounts.codex.default", + codex_usage, + true, + runtime.countdown, + ); } - if !data.accounts.iter().any(|a| a.provider == ProviderId::Codex && a.profile.id == "codex_x") { + if !data + .accounts + .iter() + .any(|a| a.provider == ProviderId::Codex && a.profile.id == "codex_x") + { let codex_usage = data.get(ProviderId::Codex); - context.insert_provider("accounts.codex.codex_x", codex_usage, true, runtime.countdown); + context.insert_provider( + "accounts.codex.codex_x", + codex_usage, + true, + runtime.countdown, + ); } for descriptor in PROVIDER_DESCRIPTORS { if let Some(account) = data diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index c83e7419..fb1bdf2b 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -3609,8 +3609,10 @@ fn compact_fluent_quad_antigravity_dual_and_solo_modes() { assert!(find_render("antigravity-session-gemini-solo-dark")); assert!(!find_render("antigravity-session-external-solo-dark")); - assert!(!find_render("antigravity-session-gemini-dual-dark")); - assert!(!find_render("antigravity-session-external-dual-dark")); + assert!(!find_render("antigravity-gemini-5h-dark-normal")); + assert!(!find_render("antigravity-gemini-5h-dark-hover")); + assert!(!find_render("antigravity-external-5h-dark-normal")); + assert!(!find_render("antigravity-external-5h-dark-hover")); assert!(find_render("antigravity-session-gemini-value-dark")); assert!(!find_render("antigravity-session-external-value-dark")); } @@ -3633,7 +3635,10 @@ fn compact_fluent_quad_antigravity_dual_and_solo_modes() { }; assert!(!find_render("antigravity-session-gemini-solo-dark")); assert!(find_render("antigravity-session-external-solo-dark")); - assert!(!find_render("antigravity-session-external-dual-dark")); + assert!(!find_render("antigravity-gemini-5h-dark-normal")); + assert!(!find_render("antigravity-gemini-5h-dark-hover")); + assert!(!find_render("antigravity-external-5h-dark-normal")); + assert!(!find_render("antigravity-external-5h-dark-hover")); assert!(find_render("antigravity-session-external-value-dark")); assert_eq!( evaluate("antigravity.external.opacity", &context).unwrap(), @@ -3687,15 +3692,17 @@ fn compact_fluent_quad_antigravity_dual_and_solo_modes() { assert!(!find_render("antigravity-session-gemini-solo-dark")); assert!(find_render("antigravity-session-external-solo-dark")); - assert!(!find_render("antigravity-session-gemini-dual-dark")); - assert!(!find_render("antigravity-session-external-dual-dark")); + assert!(!find_render("antigravity-gemini-5h-dark-normal")); + assert!(!find_render("antigravity-gemini-5h-dark-hover")); + assert!(!find_render("antigravity-external-5h-dark-normal")); + assert!(!find_render("antigravity-external-5h-dark-hover")); assert!(!find_render("antigravity-session-gemini-value-dark")); assert!(find_render("antigravity-session-external-value-dark")); } - // 3. Dual Mode Crossfade + // 3. Dual Mode Crossfade (Variant A) { - // At opacity 1.0 (Gemini holding) + // At opacity 1.0 (Normal: Gemini 6px, External 3px, Gemini text) let runtime = ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) .with_antigravity_pools(true, true, 1.0); @@ -3715,72 +3722,20 @@ fn compact_fluent_quad_antigravity_dual_and_solo_modes() { assert!(!find_render("antigravity-session-gemini-solo-dark")); assert!(!find_render("antigravity-session-external-solo-dark")); - assert!(find_render("antigravity-session-gemini-dual-dark")); - assert!(find_render("antigravity-session-external-dual-dark")); - assert!(find_render("antigravity-session-gemini-value-dark")); - assert!(!find_render("antigravity-session-external-value-dark")); - - // Sequential cross-dissolve: at 0.75 Gemini is fading down, External not yet visible - let runtime_gemini_fade = - ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) - .with_antigravity_pools(true, true, 0.75); - let mut context_gemini_fade = DataContext::from_usage_with_runtime( - Some(&usage), - &Canvas::default(), - runtime_gemini_fade, - ); - context_gemini_fade.insert("system.dark", 1.0); - let find_render_gf = |id: &str| { - theme.surfaces[0] - .children - .iter() - .find(|o| o.id == id) - .map(|o| evaluate(&o.render.0, &context_gemini_fade).unwrap() != 0.0) - .unwrap_or(false) - }; - assert!(find_render_gf("antigravity-session-gemini-value-dark")); - assert!(!find_render_gf("antigravity-session-external-value-dark")); - - // At opacity 0.5 (Mid-transition clean dip: zero overlap to avoid glyph jitter) - let runtime_mid = - ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) - .with_antigravity_pools(true, true, 0.5); - let mut context_mid = - DataContext::from_usage_with_runtime(Some(&usage), &Canvas::default(), runtime_mid); - context_mid.insert("system.dark", 1.0); - let find_render_mid = |id: &str| { - theme.surfaces[0] - .children - .iter() - .find(|o| o.id == id) - .map(|o| evaluate(&o.render.0, &context_mid).unwrap() != 0.0) - .unwrap_or(false) - }; - assert!(!find_render_mid("antigravity-session-gemini-value-dark")); - assert!(!find_render_mid("antigravity-session-external-value-dark")); - - // At 0.25 External is fading up, Gemini is hidden - let runtime_ext_fade = - ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) - .with_antigravity_pools(true, true, 0.25); - let mut context_ext_fade = DataContext::from_usage_with_runtime( - Some(&usage), - &Canvas::default(), - runtime_ext_fade, - ); - context_ext_fade.insert("system.dark", 1.0); - let find_render_ef = |id: &str| { - theme.surfaces[0] - .children - .iter() - .find(|o| o.id == id) - .map(|o| evaluate(&o.render.0, &context_ext_fade).unwrap() != 0.0) - .unwrap_or(false) - }; - assert!(!find_render_ef("antigravity-session-gemini-value-dark")); - assert!(find_render_ef("antigravity-session-external-value-dark")); - - // At opacity 0.0 (External holding) + assert!(find_render("antigravity-gemini-5h-dark-normal")); + assert!(find_render("antigravity-gemini-weekly-dark-normal")); + assert!(find_render("antigravity-external-5h-dark-normal")); + assert!(find_render("antigravity-external-weekly-dark-normal")); + assert!(!find_render("antigravity-gemini-5h-dark-hover")); + assert!(!find_render("antigravity-gemini-weekly-dark-hover")); + assert!(!find_render("antigravity-external-5h-dark-hover")); + assert!(!find_render("antigravity-external-weekly-dark-hover")); + assert!(find_render("antigravity-gemini-text-dark-normal")); + assert!(!find_render("antigravity-gemini-text-dark-hover")); + assert!(find_render("antigravity-external-text-dark-normal")); + assert!(!find_render("antigravity-external-text-dark-hover")); + + // At opacity 0.0 (Hover: Gemini 3px, External 6px, External text) let runtime_ext = ThemeRuntime::from_providers(ProviderSet::from_enabled([ProviderId::Antigravity])) .with_antigravity_pools(true, true, 0.0); @@ -3795,8 +3750,18 @@ fn compact_fluent_quad_antigravity_dual_and_solo_modes() { .map(|o| evaluate(&o.render.0, &context_ext).unwrap() != 0.0) .unwrap_or(false) }; - assert!(!find_render_ext("antigravity-session-gemini-value-dark")); - assert!(find_render_ext("antigravity-session-external-value-dark")); + assert!(!find_render_ext("antigravity-gemini-5h-dark-normal")); + assert!(!find_render_ext("antigravity-gemini-weekly-dark-normal")); + assert!(!find_render_ext("antigravity-external-5h-dark-normal")); + assert!(!find_render_ext("antigravity-external-weekly-dark-normal")); + assert!(find_render_ext("antigravity-gemini-5h-dark-hover")); + assert!(find_render_ext("antigravity-gemini-weekly-dark-hover")); + assert!(find_render_ext("antigravity-external-5h-dark-hover")); + assert!(find_render_ext("antigravity-external-weekly-dark-hover")); + assert!(!find_render_ext("antigravity-gemini-text-dark-normal")); + assert!(find_render_ext("antigravity-gemini-text-dark-hover")); + assert!(!find_render_ext("antigravity-external-text-dark-normal")); + assert!(find_render_ext("antigravity-external-text-dark-hover")); } } @@ -3996,7 +3961,7 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { .enumerate() .filter(|(_, object)| object.id.ends_with("-pace")) .collect::>(); - assert_eq!(ticks.len(), 32); + assert_eq!(ticks.len(), 64); for (index, tick) in ticks { let gauge = &children[index - 1]; assert_eq!(tick.id, format!("{}-pace", gauge.id)); @@ -4008,6 +3973,10 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { let stripped = stripped .strip_suffix(" * antigravity.gemini.opacity") .or_else(|| stripped.strip_suffix(" * antigravity.external.opacity")) + .or_else(|| stripped.strip_suffix(" * antigravity.normal.opacity")) + .or_else(|| stripped.strip_suffix(" * antigravity.hover.opacity")) + .or_else(|| stripped.strip_suffix(" * claude.normal.opacity")) + .or_else(|| stripped.strip_suffix(" * claude.hover.opacity")) .or_else(|| stripped.strip_suffix(" * codex.normal.opacity")) .or_else(|| stripped.strip_suffix(" * codex.hover.opacity")) .unwrap_or(stripped); @@ -4027,6 +3996,10 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { let mut context = DataContext::from_usage(None, &Canvas::default()); context.insert("antigravity.gemini.opacity", 1.0); context.insert("antigravity.external.opacity", 1.0); + context.insert("antigravity.normal.opacity", 1.0); + context.insert("antigravity.hover.opacity", 1.0); + context.insert("claude.normal.opacity", 1.0); + context.insert("claude.hover.opacity", 1.0); context.insert("codex.normal.opacity", 1.0); context.insert("codex.hover.opacity", 1.0); for count in 1..=ProviderId::ALL.len() { @@ -4068,6 +4041,30 @@ fn compact_pace_ticks_validate_and_stay_inside_their_gauges() { } } +#[test] +fn claude_dual_mode_and_crossfade_context_variables() { + let runtime = ThemeRuntime::default().with_claude_state(true, true, true, 0.75); + let canvas = Canvas::default(); + let ctx = DataContext::from_usage_with_runtime(None, &canvas, runtime); + assert_eq!(ctx.get("claude.hovered"), Some(1.0)); + assert_eq!(ctx.get("claude.main.enabled"), Some(1.0)); + assert_eq!(ctx.get("claude.x.enabled"), Some(1.0)); + assert_eq!(ctx.get("claude.dual_mode"), Some(1.0)); + assert_eq!(ctx.get("claude.crossfade_opacity"), Some(0.75)); + assert_eq!(ctx.get("claude.normal.opacity"), Some(0.25)); + assert_eq!(ctx.get("claude.hover.opacity"), Some(0.75)); +} + +#[test] +fn antigravity_normal_and_hover_opacity_context_variables() { + let runtime = ThemeRuntime::default().with_antigravity_pools(true, true, 0.3); + let canvas = Canvas::default(); + let ctx = DataContext::from_usage_with_runtime(None, &canvas, runtime); + assert_eq!(ctx.get("antigravity.crossfade_opacity"), Some(0.3)); + assert_eq!(ctx.get("antigravity.normal.opacity"), Some(0.3)); + assert_eq!(ctx.get("antigravity.hover.opacity"), Some(0.7)); +} + #[test] fn codex_dual_mode_and_crossfade_context_variables() { let runtime = ThemeRuntime::default().with_codex_state(true, true, true, 0.75); @@ -4081,5 +4078,3 @@ fn codex_dual_mode_and_crossfade_context_variables() { assert_eq!(ctx.get("codex.normal.opacity"), Some(0.25)); assert_eq!(ctx.get("codex.hover.opacity"), Some(0.75)); } - - diff --git a/src/themes/compact-fluent-quad.json b/src/themes/compact-fluent-quad.json index 5119d4c9..ddde65fa 100644 --- a/src/themes/compact-fluent-quad.json +++ b/src/themes/compact-fluent-quad.json @@ -21,7 +21,7 @@ "offset_x": 6, "offset_y": 0 }, - "width": "providers.count * (ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62) + max(0, providers.count - 1) * 6 + providers.opencode.enabled * 28 + providers.cursor.enabled * 18 + providers.claude.enabled * claude.model.fable.available * 96 + providers.codex.enabled * codex.dual_mode * 12", + "width": "providers.count * (ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62) + max(0, providers.count - 1) * 6 + providers.opencode.enabled * 28 + providers.cursor.enabled * 18 + providers.claude.enabled * claude.model.fable.available * 96 + providers.codex.enabled * codex.dual_mode * 12 + providers.claude.enabled * claude.dual_mode * 12", "height": "46", "background": { "type": "none" @@ -356,7 +356,7 @@ "render": "providers.claude.enabled", "visibility": "100", "parent": "provider-row", - "width": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 62 + (claude.dual_mode * 12)", "height": "46", "background": { "type": "none" @@ -373,7 +373,7 @@ { "id": "claude-session-segments-dark", "name": "dark Claude Code 5-hour segments", - "render": "(system.dark) && claude.session.available", + "render": "(system.dark) && (!claude.dual_mode) && claude.session.available", "visibility": "100", "parent": "claude-provider", "y": "10", @@ -408,7 +408,7 @@ { "id": "claude-session-segments-dark-pace", "name": "dark Claude Code 5-hour segments pace tick", - "render": "(system.dark) && claude.session.available", + "render": "(system.dark) && (!claude.dual_mode) && claude.session.available", "visibility": "(100) * claude.session.expected_available * display.show_pace", "parent": "claude-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.session.expected_display, 0, 100) / 100)", @@ -434,7 +434,7 @@ { "id": "claude-session-value-dark", "name": "dark Claude Code 5-hour value", - "render": "(system.dark) && claude.session.available", + "render": "(system.dark) && (!claude.dual_mode) && claude.session.available", "visibility": "100", "parent": "claude-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -467,7 +467,7 @@ { "id": "claude-session-segments-light", "name": "light Claude Code 5-hour segments", - "render": "(1 - system.dark) && claude.session.available", + "render": "(1 - system.dark) && (!claude.dual_mode) && claude.session.available", "visibility": "100", "parent": "claude-provider", "y": "10", @@ -502,7 +502,7 @@ { "id": "claude-session-segments-light-pace", "name": "light Claude Code 5-hour segments pace tick", - "render": "(1 - system.dark) && claude.session.available", + "render": "(1 - system.dark) && (!claude.dual_mode) && claude.session.available", "visibility": "(100) * claude.session.expected_available * display.show_pace", "parent": "claude-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.session.expected_display, 0, 100) / 100)", @@ -528,7 +528,7 @@ { "id": "claude-session-value-light", "name": "light Claude Code 5-hour value", - "render": "(1 - system.dark) && claude.session.available", + "render": "(1 - system.dark) && (!claude.dual_mode) && claude.session.available", "visibility": "100", "parent": "claude-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -561,7 +561,7 @@ { "id": "claude-weekly-segments-dark", "name": "dark Claude Code 7-day segments", - "render": "system.dark", + "render": "(system.dark) && (!claude.dual_mode)", "visibility": "100", "parent": "claude-provider", "y": "27", @@ -596,7 +596,7 @@ { "id": "claude-weekly-segments-dark-pace", "name": "dark Claude Code 7-day segments pace tick", - "render": "system.dark", + "render": "(system.dark) && (!claude.dual_mode)", "visibility": "(100) * claude.weekly.expected_available * display.show_pace", "parent": "claude-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.weekly.expected_display, 0, 100) / 100)", @@ -622,7 +622,7 @@ { "id": "claude-weekly-value-dark", "name": "dark Claude Code 7-day value", - "render": "(system.dark) && !claude.credits.available", + "render": "((system.dark) && !claude.credits.available) && (!claude.dual_mode)", "visibility": "100", "parent": "claude-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -655,7 +655,7 @@ { "id": "claude-weekly-segments-light", "name": "light Claude Code 7-day segments", - "render": "1 - system.dark", + "render": "(1 - system.dark) && (!claude.dual_mode)", "visibility": "100", "parent": "claude-provider", "y": "27", @@ -690,7 +690,7 @@ { "id": "claude-weekly-segments-light-pace", "name": "light Claude Code 7-day segments pace tick", - "render": "1 - system.dark", + "render": "(1 - system.dark) && (!claude.dual_mode)", "visibility": "(100) * claude.weekly.expected_available * display.show_pace", "parent": "claude-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(claude.weekly.expected_display, 0, 100) / 100)", @@ -716,7 +716,7 @@ { "id": "claude-weekly-value-light", "name": "light Claude Code 7-day value", - "render": "(1 - system.dark) && !claude.credits.available", + "render": "((1 - system.dark) && !claude.credits.available) && (!claude.dual_mode)", "visibility": "100", "parent": "claude-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1561,14 +1561,14 @@ } }, { - "id": "antigravity-session-gemini-dual-dark", - "name": "dark Antigravity Gemini dual session segments", - "render": "system.dark && antigravity.dual_mode", - "visibility": "100", + "id": "antigravity-gemini-5h-dark-normal", + "name": "dark Antigravity Gemini 5h normal", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "100 * antigravity.normal.opacity", "parent": "antigravity-provider", - "y": "10", + "y": "8", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "4", + "height": "6", "background": { "type": "none" }, @@ -1582,7 +1582,7 @@ "value": "antigravity.gemini.session.display", "direction": "left_to_right", "fill": { - "color": "#00D2B4FF", + "color": "#5EEAD4FF", "opacity": "1" }, "track": { @@ -1596,15 +1596,15 @@ } }, { - "id": "antigravity-session-gemini-dual-dark-pace", - "name": "dark Antigravity Gemini dual session segments pace tick", - "render": "system.dark && antigravity.dual_mode", - "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", + "id": "antigravity-gemini-5h-dark-normal-pace", + "name": "dark Antigravity Gemini 5h normal pace", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace * antigravity.normal.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", - "y": "(10) - 1", + "y": "7", "width": "1", - "height": "(4) + 2", + "height": "8", "background": { "type": "colour", "colour": { @@ -1616,20 +1616,75 @@ "corner_radius": "0", "layout": "freeform", "align": "start", + "gap": "0" + }, + { + "id": "antigravity-gemini-5h-dark-hover", + "name": "dark Antigravity Gemini 5h hover", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "100 * antigravity.hover.opacity", + "parent": "antigravity-provider", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", "gap": "0", "content": { - "type": "none" + "type": "progress", + "value": "antigravity.gemini.session.display", + "direction": "left_to_right", + "fill": { + "color": "#5EEAD4FF", + "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": "antigravity-session-gemini-dual-light", - "name": "light Antigravity Gemini dual session segments", - "render": "(1 - system.dark) && antigravity.dual_mode", - "visibility": "100", + "id": "antigravity-gemini-5h-dark-hover-pace", + "name": "dark Antigravity Gemini 5h hover pace", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace * antigravity.hover.opacity", "parent": "antigravity-provider", - "y": "10", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0" + }, + { + "id": "antigravity-gemini-5h-light-normal", + "name": "light Antigravity Gemini 5h normal", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "100 * antigravity.normal.opacity", + "parent": "antigravity-provider", + "y": "8", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "4", + "height": "6", "background": { "type": "none" }, @@ -1643,7 +1698,7 @@ "value": "antigravity.gemini.session.display", "direction": "left_to_right", "fill": { - "color": "#00A896FF", + "color": "#007A6CFF", "opacity": "1" }, "track": { @@ -1657,15 +1712,15 @@ } }, { - "id": "antigravity-session-gemini-dual-light-pace", - "name": "light Antigravity Gemini dual session segments pace tick", - "render": "(1 - system.dark) && antigravity.dual_mode", - "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace", + "id": "antigravity-gemini-5h-light-normal-pace", + "name": "light Antigravity Gemini 5h normal pace", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace * antigravity.normal.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", - "y": "(10) - 1", + "y": "7", "width": "1", - "height": "(4) + 2", + "height": "8", "background": { "type": "colour", "colour": { @@ -1677,20 +1732,75 @@ "corner_radius": "0", "layout": "freeform", "align": "start", + "gap": "0" + }, + { + "id": "antigravity-gemini-5h-light-hover", + "name": "light Antigravity Gemini 5h hover", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "100 * antigravity.hover.opacity", + "parent": "antigravity-provider", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", "gap": "0", "content": { - "type": "none" + "type": "progress", + "value": "antigravity.gemini.session.display", + "direction": "left_to_right", + "fill": { + "color": "#007A6CFF", + "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": "antigravity-session-external-dual-dark", - "name": "dark Antigravity External dual session segments", - "render": "system.dark && antigravity.dual_mode", - "visibility": "100", + "id": "antigravity-gemini-5h-light-hover-pace", + "name": "light Antigravity Gemini 5h hover pace", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "(100) * antigravity.gemini.session.expected_available * display.show_pace * antigravity.hover.opacity", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0" + }, + { + "id": "antigravity-gemini-weekly-dark-normal", + "name": "dark Antigravity Gemini weekly normal", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "100 * antigravity.normal.opacity", "parent": "antigravity-provider", "y": "16", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "4", + "height": "3", "background": { "type": "none" }, @@ -1701,14 +1811,14 @@ "gap": "0", "content": { "type": "progress", - "value": "antigravity.external.session.display", + "value": "antigravity.gemini.weekly.display", "direction": "left_to_right", "fill": { - "color": "#6EB4C8FF", + "color": "#0D9488FF", "opacity": "1" }, "track": { - "color": "#444444FF", + "color": "#333333FF", "opacity": "1" }, "corner_radius": "1.5", @@ -1718,15 +1828,15 @@ } }, { - "id": "antigravity-session-external-dual-dark-pace", - "name": "dark Antigravity External dual session segments pace tick", - "render": "system.dark && antigravity.dual_mode", - "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace", + "id": "antigravity-gemini-weekly-dark-normal-pace", + "name": "dark Antigravity Gemini weekly normal pace", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.normal.opacity", "parent": "antigravity-provider", - "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", - "y": "(16) - 1", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "15", "width": "1", - "height": "(4) + 2", + "height": "5", "background": { "type": "colour", "colour": { @@ -1738,20 +1848,75 @@ "corner_radius": "0", "layout": "freeform", "align": "start", + "gap": "0" + }, + { + "id": "antigravity-gemini-weekly-dark-hover", + "name": "dark Antigravity Gemini weekly hover", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "100 * antigravity.hover.opacity", + "parent": "antigravity-provider", + "y": "13", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", "gap": "0", "content": { - "type": "none" + "type": "progress", + "value": "antigravity.gemini.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#0D9488FF", + "opacity": "1" + }, + "track": { + "color": "#333333FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" } }, { - "id": "antigravity-session-external-dual-light", - "name": "light Antigravity External dual session segments", - "render": "(1 - system.dark) && antigravity.dual_mode", - "visibility": "100", + "id": "antigravity-gemini-weekly-dark-hover-pace", + "name": "dark Antigravity Gemini weekly hover pace", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.hover.opacity", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "12", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0" + }, + { + "id": "antigravity-gemini-weekly-light-normal", + "name": "light Antigravity Gemini weekly normal", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "100 * antigravity.normal.opacity", "parent": "antigravity-provider", "y": "16", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "4", + "height": "3", "background": { "type": "none" }, @@ -1762,14 +1927,14 @@ "gap": "0", "content": { "type": "progress", - "value": "antigravity.external.session.display", + "value": "antigravity.gemini.weekly.display", "direction": "left_to_right", "fill": { - "color": "#4A8B9EFF", + "color": "#004D40FF", "opacity": "1" }, "track": { - "color": "#AAAAAAFF", + "color": "#CCCCCCFF", "opacity": "1" }, "corner_radius": "1.5", @@ -1779,15 +1944,15 @@ } }, { - "id": "antigravity-session-external-dual-light-pace", - "name": "light Antigravity External dual session segments pace tick", - "render": "(1 - system.dark) && antigravity.dual_mode", - "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace", + "id": "antigravity-gemini-weekly-light-normal-pace", + "name": "light Antigravity Gemini weekly normal pace", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.normal.opacity", "parent": "antigravity-provider", - "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", - "y": "(16) - 1", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "15", "width": "1", - "height": "(4) + 2", + "height": "5", "background": { "type": "colour", "colour": { @@ -1799,15 +1964,70 @@ "corner_radius": "0", "layout": "freeform", "align": "start", + "gap": "0" + }, + { + "id": "antigravity-gemini-weekly-light-hover", + "name": "light Antigravity Gemini weekly hover", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "100 * antigravity.hover.opacity", + "parent": "antigravity-provider", + "y": "13", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", "gap": "0", "content": { - "type": "none" + "type": "progress", + "value": "antigravity.gemini.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#004D40FF", + "opacity": "1" + }, + "track": { + "color": "#CCCCCCFF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" } }, { - "id": "antigravity-session-gemini-value-dark", - "name": "dark Antigravity Gemini session value", - "render": "system.dark && antigravity.gemini.opacity > 0.01", + "id": "antigravity-gemini-weekly-light-hover-pace", + "name": "light Antigravity Gemini weekly hover pace", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.hover.opacity", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "12", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0" + }, + { + "id": "antigravity-gemini-text-dark-normal", + "name": "dark Antigravity Gemini text normal", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1833,14 +2053,14 @@ "align": "left", "color": { "color": "#5EEAD4FF", - "opacity": "antigravity.gemini.opacity" + "opacity": "antigravity.normal.opacity" } } }, { - "id": "antigravity-session-gemini-value-light", - "name": "light Antigravity Gemini session value", - "render": "(1 - system.dark) && antigravity.gemini.opacity > 0.01", + "id": "antigravity-gemini-text-dark-hover", + "name": "dark Antigravity Gemini text hover", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1857,7 +2077,7 @@ "gap": "0", "content": { "type": "text", - "template": "{antigravity.gemini.session.display:usage_line}", + "template": "{antigravity.gemini.weekly.display:usage_line}", "font_family": "Segoe UI", "font_size": "12", "weight": "medium", @@ -1865,15 +2085,15 @@ "contrast": "1", "align": "left", "color": { - "color": "#007A6CFF", - "opacity": "antigravity.gemini.opacity" + "color": "#0D9488FF", + "opacity": "antigravity.hover.opacity" } } }, { - "id": "antigravity-session-external-value-dark", - "name": "dark Antigravity External session value", - "render": "system.dark && antigravity.external.opacity > 0.01", + "id": "antigravity-gemini-text-light-normal", + "name": "light Antigravity Gemini text normal", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1890,7 +2110,7 @@ "gap": "0", "content": { "type": "text", - "template": "{antigravity.external.session.display:usage_line}", + "template": "{antigravity.gemini.session.display:usage_line}", "font_family": "Segoe UI", "font_size": "12", "weight": "medium", @@ -1898,15 +2118,15 @@ "contrast": "1", "align": "left", "color": { - "color": "#98D0E0FF", - "opacity": "antigravity.external.opacity" + "color": "#007A6CFF", + "opacity": "antigravity.normal.opacity" } } }, { - "id": "antigravity-session-external-value-light", - "name": "light Antigravity External session value", - "render": "(1 - system.dark) && antigravity.external.opacity > 0.01", + "id": "antigravity-gemini-text-light-hover", + "name": "light Antigravity Gemini text hover", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -1923,7 +2143,7 @@ "gap": "0", "content": { "type": "text", - "template": "{antigravity.external.session.display:usage_line}", + "template": "{antigravity.gemini.weekly.display:usage_line}", "font_family": "Segoe UI", "font_size": "12", "weight": "medium", @@ -1931,20 +2151,20 @@ "contrast": "1", "align": "left", "color": { - "color": "#2D6F82FF", - "opacity": "antigravity.external.opacity" + "color": "#004D40FF", + "opacity": "antigravity.hover.opacity" } } }, { - "id": "antigravity-weekly-gemini-solo-dark", - "name": "dark Antigravity Gemini solo weekly segments", - "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", - "visibility": "100 * antigravity.gemini.opacity", + "id": "antigravity-external-5h-dark-normal", + "name": "dark Antigravity External 5h normal", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "100 * antigravity.normal.opacity", "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "9", + "height": "6", "background": { "type": "none" }, @@ -1955,10 +2175,10 @@ "gap": "0", "content": { "type": "progress", - "value": "antigravity.gemini.weekly.display", + "value": "antigravity.external.session.display", "direction": "left_to_right", "fill": { - "color": "#00D2B4FF", + "color": "#98D0E0FF", "opacity": "1" }, "track": { @@ -1972,15 +2192,15 @@ } }, { - "id": "antigravity-weekly-gemini-solo-dark-pace", - "name": "dark Antigravity Gemini solo weekly segments pace tick", - "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", - "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.gemini.opacity", + "id": "antigravity-external-5h-dark-normal-pace", + "name": "dark Antigravity External 5h normal pace", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace * antigravity.normal.opacity", "parent": "antigravity-provider", - "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", - "y": "(27) - 1", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", + "y": "26", "width": "1", - "height": "(9) + 2", + "height": "8", "background": { "type": "colour", "colour": { @@ -1992,20 +2212,17 @@ "corner_radius": "0", "layout": "freeform", "align": "start", - "gap": "0", - "content": { - "type": "none" - } + "gap": "0" }, { - "id": "antigravity-weekly-gemini-solo-light", - "name": "light Antigravity Gemini solo weekly segments", - "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", - "visibility": "100 * antigravity.gemini.opacity", + "id": "antigravity-external-5h-dark-hover", + "name": "dark Antigravity External 5h hover", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "100 * antigravity.hover.opacity", "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "9", + "height": "3", "background": { "type": "none" }, @@ -2016,14 +2233,14 @@ "gap": "0", "content": { "type": "progress", - "value": "antigravity.gemini.weekly.display", + "value": "antigravity.external.session.display", "direction": "left_to_right", "fill": { - "color": "#00A896FF", + "color": "#98D0E0FF", "opacity": "1" }, "track": { - "color": "#AAAAAAFF", + "color": "#444444FF", "opacity": "1" }, "corner_radius": "1.5", @@ -2033,19 +2250,19 @@ } }, { - "id": "antigravity-weekly-gemini-solo-light-pace", - "name": "light Antigravity Gemini solo weekly segments pace tick", - "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", - "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.gemini.opacity", + "id": "antigravity-external-5h-dark-hover-pace", + "name": "dark Antigravity External 5h hover pace", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace * antigravity.hover.opacity", "parent": "antigravity-provider", - "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", - "y": "(27) - 1", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", + "y": "26", "width": "1", - "height": "(9) + 2", + "height": "5", "background": { "type": "colour", "colour": { - "color": "#1A1A1AFF", + "color": "#FFFFFFFF", "opacity": "0.9" } }, @@ -2053,20 +2270,17 @@ "corner_radius": "0", "layout": "freeform", "align": "start", - "gap": "0", - "content": { - "type": "none" - } + "gap": "0" }, { - "id": "antigravity-weekly-external-solo-dark", - "name": "dark Antigravity External solo weekly segments", - "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", - "visibility": "100 * antigravity.external.opacity", + "id": "antigravity-external-5h-light-normal", + "name": "light Antigravity External 5h normal", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "100 * antigravity.normal.opacity", "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "9", + "height": "6", "background": { "type": "none" }, @@ -2077,14 +2291,14 @@ "gap": "0", "content": { "type": "progress", - "value": "antigravity.external.weekly.display", + "value": "antigravity.external.session.display", "direction": "left_to_right", "fill": { - "color": "#6EB4C8FF", + "color": "#2D6F82FF", "opacity": "1" }, "track": { - "color": "#444444FF", + "color": "#AAAAAAFF", "opacity": "1" }, "corner_radius": "1.5", @@ -2094,19 +2308,19 @@ } }, { - "id": "antigravity-weekly-external-solo-dark-pace", - "name": "dark Antigravity External solo weekly segments pace tick", - "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", - "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.external.opacity", + "id": "antigravity-external-5h-light-normal-pace", + "name": "light Antigravity External 5h normal pace", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace * antigravity.normal.opacity", "parent": "antigravity-provider", - "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", - "y": "(27) - 1", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", + "y": "26", "width": "1", - "height": "(9) + 2", + "height": "8", "background": { "type": "colour", "colour": { - "color": "#FFFFFFFF", + "color": "#1A1A1AFF", "opacity": "0.9" } }, @@ -2114,20 +2328,17 @@ "corner_radius": "0", "layout": "freeform", "align": "start", - "gap": "0", - "content": { - "type": "none" - } + "gap": "0" }, { - "id": "antigravity-weekly-external-solo-light", - "name": "light Antigravity External solo weekly segments", - "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.external.opacity > 0.01", - "visibility": "100 * antigravity.external.opacity", + "id": "antigravity-external-5h-light-hover", + "name": "light Antigravity External 5h hover", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "100 * antigravity.hover.opacity", "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "9", + "height": "3", "background": { "type": "none" }, @@ -2138,10 +2349,10 @@ "gap": "0", "content": { "type": "progress", - "value": "antigravity.external.weekly.display", + "value": "antigravity.external.session.display", "direction": "left_to_right", "fill": { - "color": "#4A8B9EFF", + "color": "#2D6F82FF", "opacity": "1" }, "track": { @@ -2155,15 +2366,15 @@ } }, { - "id": "antigravity-weekly-external-solo-light-pace", - "name": "light Antigravity External solo weekly segments pace tick", - "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.external.opacity > 0.01", - "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.external.opacity", + "id": "antigravity-external-5h-light-hover-pace", + "name": "light Antigravity External 5h hover pace", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "(100) * antigravity.external.session.expected_available * display.show_pace * antigravity.hover.opacity", "parent": "antigravity-provider", - "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", - "y": "(27) - 1", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.session.expected_display, 0, 100) / 100)", + "y": "26", "width": "1", - "height": "(9) + 2", + "height": "5", "background": { "type": "colour", "colour": { @@ -2175,20 +2386,17 @@ "corner_radius": "0", "layout": "freeform", "align": "start", - "gap": "0", - "content": { - "type": "none" - } + "gap": "0" }, { - "id": "antigravity-weekly-gemini-dual-dark", - "name": "dark Antigravity Gemini dual weekly segments", - "render": "system.dark && antigravity.dual_mode", - "visibility": "100", + "id": "antigravity-external-weekly-dark-normal", + "name": "dark Antigravity External weekly normal", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "100 * antigravity.normal.opacity", "parent": "antigravity-provider", - "y": "27", + "y": "35", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "4", + "height": "3", "background": { "type": "none" }, @@ -2199,14 +2407,14 @@ "gap": "0", "content": { "type": "progress", - "value": "antigravity.gemini.weekly.display", + "value": "antigravity.external.weekly.display", "direction": "left_to_right", "fill": { - "color": "#00D2B4FF", + "color": "#4A8B9EFF", "opacity": "1" }, "track": { - "color": "#444444FF", + "color": "#333333FF", "opacity": "1" }, "corner_radius": "1.5", @@ -2216,15 +2424,15 @@ } }, { - "id": "antigravity-weekly-gemini-dual-dark-pace", - "name": "dark Antigravity Gemini dual weekly segments pace tick", - "render": "system.dark && antigravity.dual_mode", - "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace", + "id": "antigravity-external-weekly-dark-normal-pace", + "name": "dark Antigravity External weekly normal pace", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.normal.opacity", "parent": "antigravity-provider", - "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", - "y": "(27) - 1", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", + "y": "34", "width": "1", - "height": "(4) + 2", + "height": "5", "background": { "type": "colour", "colour": { @@ -2236,20 +2444,17 @@ "corner_radius": "0", "layout": "freeform", "align": "start", - "gap": "0", - "content": { - "type": "none" - } + "gap": "0" }, { - "id": "antigravity-weekly-gemini-dual-light", - "name": "light Antigravity Gemini dual weekly segments", - "render": "(1 - system.dark) && antigravity.dual_mode", - "visibility": "100", + "id": "antigravity-external-weekly-dark-hover", + "name": "dark Antigravity External weekly hover", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "100 * antigravity.hover.opacity", "parent": "antigravity-provider", - "y": "27", + "y": "32", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "4", + "height": "6", "background": { "type": "none" }, @@ -2260,14 +2465,14 @@ "gap": "0", "content": { "type": "progress", - "value": "antigravity.gemini.weekly.display", + "value": "antigravity.external.weekly.display", "direction": "left_to_right", "fill": { - "color": "#00A896FF", + "color": "#4A8B9EFF", "opacity": "1" }, "track": { - "color": "#AAAAAAFF", + "color": "#333333FF", "opacity": "1" }, "corner_radius": "1.5", @@ -2277,19 +2482,19 @@ } }, { - "id": "antigravity-weekly-gemini-dual-light-pace", - "name": "light Antigravity Gemini dual weekly segments pace tick", - "render": "(1 - system.dark) && antigravity.dual_mode", - "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace", + "id": "antigravity-external-weekly-dark-hover-pace", + "name": "dark Antigravity External weekly hover pace", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.hover.opacity", "parent": "antigravity-provider", - "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", - "y": "(27) - 1", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", + "y": "31", "width": "1", - "height": "(4) + 2", + "height": "8", "background": { "type": "colour", "colour": { - "color": "#1A1A1AFF", + "color": "#FFFFFFFF", "opacity": "0.9" } }, @@ -2297,20 +2502,17 @@ "corner_radius": "0", "layout": "freeform", "align": "start", - "gap": "0", - "content": { - "type": "none" - } + "gap": "0" }, { - "id": "antigravity-weekly-external-dual-dark", - "name": "dark Antigravity External dual weekly segments", - "render": "system.dark && antigravity.dual_mode", - "visibility": "100", + "id": "antigravity-external-weekly-light-normal", + "name": "light Antigravity External weekly normal", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "100 * antigravity.normal.opacity", "parent": "antigravity-provider", - "y": "32", + "y": "35", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "4", + "height": "3", "background": { "type": "none" }, @@ -2324,11 +2526,11 @@ "value": "antigravity.external.weekly.display", "direction": "left_to_right", "fill": { - "color": "#6EB4C8FF", + "color": "#1F4E5BFF", "opacity": "1" }, "track": { - "color": "#444444FF", + "color": "#CCCCCCFF", "opacity": "1" }, "corner_radius": "1.5", @@ -2338,19 +2540,19 @@ } }, { - "id": "antigravity-weekly-external-dual-dark-pace", - "name": "dark Antigravity External dual weekly segments pace tick", - "render": "system.dark && antigravity.dual_mode", - "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace", + "id": "antigravity-external-weekly-light-normal-pace", + "name": "light Antigravity External weekly normal pace", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.normal.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", - "y": "(32) - 1", + "y": "34", "width": "1", - "height": "(4) + 2", + "height": "5", "background": { "type": "colour", "colour": { - "color": "#FFFFFFFF", + "color": "#1A1A1AFF", "opacity": "0.9" } }, @@ -2358,20 +2560,17 @@ "corner_radius": "0", "layout": "freeform", "align": "start", - "gap": "0", - "content": { - "type": "none" - } + "gap": "0" }, { - "id": "antigravity-weekly-external-dual-light", - "name": "light Antigravity External dual weekly segments", - "render": "(1 - system.dark) && antigravity.dual_mode", - "visibility": "100", + "id": "antigravity-external-weekly-light-hover", + "name": "light Antigravity External weekly hover", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "100 * antigravity.hover.opacity", "parent": "antigravity-provider", "y": "32", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "4", + "height": "6", "background": { "type": "none" }, @@ -2385,11 +2584,11 @@ "value": "antigravity.external.weekly.display", "direction": "left_to_right", "fill": { - "color": "#4A8B9EFF", + "color": "#1F4E5BFF", "opacity": "1" }, "track": { - "color": "#AAAAAAFF", + "color": "#CCCCCCFF", "opacity": "1" }, "corner_radius": "1.5", @@ -2399,15 +2598,15 @@ } }, { - "id": "antigravity-weekly-external-dual-light-pace", - "name": "light Antigravity External dual weekly segments pace tick", - "render": "(1 - system.dark) && antigravity.dual_mode", - "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace", + "id": "antigravity-external-weekly-light-hover-pace", + "name": "light Antigravity External weekly hover pace", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.hover.opacity", "parent": "antigravity-provider", "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", - "y": "(32) - 1", + "y": "31", "width": "1", - "height": "(4) + 2", + "height": "8", "background": { "type": "colour", "colour": { @@ -2419,15 +2618,12 @@ "corner_radius": "0", "layout": "freeform", "align": "start", - "gap": "0", - "content": { - "type": "none" - } + "gap": "0" }, { - "id": "antigravity-weekly-gemini-value-dark", - "name": "dark Antigravity Gemini weekly value", - "render": "system.dark && antigravity.gemini.opacity > 0.01", + "id": "antigravity-external-text-dark-normal", + "name": "dark Antigravity External text normal", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -2444,7 +2640,7 @@ "gap": "0", "content": { "type": "text", - "template": "{antigravity.gemini.weekly.display:usage_line}", + "template": "{antigravity.external.session.display:usage_line}", "font_family": "Segoe UI", "font_size": "12", "weight": "medium", @@ -2452,15 +2648,15 @@ "contrast": "1", "align": "left", "color": { - "color": "#5EEAD4FF", - "opacity": "antigravity.gemini.opacity" + "color": "#98D0E0FF", + "opacity": "antigravity.normal.opacity" } } }, { - "id": "antigravity-weekly-gemini-value-light", - "name": "light Antigravity Gemini weekly value", - "render": "(1 - system.dark) && antigravity.gemini.opacity > 0.01", + "id": "antigravity-external-text-dark-hover", + "name": "dark Antigravity External text hover", + "render": "(system.dark) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -2477,7 +2673,7 @@ "gap": "0", "content": { "type": "text", - "template": "{antigravity.gemini.weekly.display:usage_line}", + "template": "{antigravity.external.weekly.display:usage_line}", "font_family": "Segoe UI", "font_size": "12", "weight": "medium", @@ -2485,15 +2681,15 @@ "contrast": "1", "align": "left", "color": { - "color": "#007A6CFF", - "opacity": "antigravity.gemini.opacity" + "color": "#4A8B9EFF", + "opacity": "antigravity.hover.opacity" } } }, { - "id": "antigravity-weekly-external-value-dark", - "name": "dark Antigravity External weekly value", - "render": "system.dark && antigravity.external.opacity > 0.01", + "id": "antigravity-external-text-light-normal", + "name": "light Antigravity External text normal", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.normal.opacity > 0.01)", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -2510,7 +2706,7 @@ "gap": "0", "content": { "type": "text", - "template": "{antigravity.external.weekly.display:usage_line}", + "template": "{antigravity.external.session.display:usage_line}", "font_family": "Segoe UI", "font_size": "12", "weight": "medium", @@ -2518,15 +2714,15 @@ "contrast": "1", "align": "left", "color": { - "color": "#98D0E0FF", - "opacity": "antigravity.external.opacity" + "color": "#2D6F82FF", + "opacity": "antigravity.normal.opacity" } } }, { - "id": "antigravity-weekly-external-value-light", - "name": "light Antigravity External weekly value", - "render": "(1 - system.dark) && antigravity.external.opacity > 0.01", + "id": "antigravity-external-text-light-hover", + "name": "light Antigravity External text hover", + "render": "((1 - system.dark)) && (antigravity.dual_mode) && (antigravity.hover.opacity > 0.01)", "visibility": "100", "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", @@ -2551,19 +2747,21 @@ "contrast": "1", "align": "left", "color": { - "color": "#2D6F82FF", - "opacity": "antigravity.external.opacity" + "color": "#1F4E5BFF", + "opacity": "antigravity.hover.opacity" } } }, { - "id": "opencode-provider", - "name": "OpenCode provider", - "render": "providers.opencode.enabled", + "id": "antigravity-session-gemini-value-dark", + "name": "dark Antigravity Gemini session value", + "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", "visibility": "100", - "parent": "provider-row", - "width": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 90", - "height": "46", + "parent": "antigravity-provider", + "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "8", + "width": "62", + "height": "13", "background": { "type": "none" }, @@ -2573,18 +2771,30 @@ "align": "start", "gap": "0", "content": { - "type": "none" + "type": "text", + "template": "{antigravity.gemini.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#5EEAD4FF", + "opacity": "antigravity.gemini.opacity" + } } }, { - "id": "opencode-session-segments-dark", - "name": "dark OpenCode 5-hour segments", - "render": "system.dark", + "id": "antigravity-session-gemini-value-light", + "name": "light Antigravity Gemini session value", + "render": "(1 - system.dark && !antigravity.dual_mode) && antigravity.gemini.opacity > 0.01", "visibility": "100", - "parent": "opencode-provider", - "y": "10", - "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "9", + "parent": "antigravity-provider", + "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "8", + "width": "62", + "height": "13", "background": { "type": "none" }, @@ -2594,29 +2804,26 @@ "align": "start", "gap": "0", "content": { - "type": "progress", - "value": "opencode.session.display", - "direction": "left_to_right", - "fill": { - "color": "#4CAF50FF", - "opacity": "1" - }, - "track": { - "color": "#444444FF", - "opacity": "1" - }, - "corner_radius": "1.5", - "segments": 10, - "segments_expression": "ceil(10 / max(1, providers.count))", - "segment_gap": "1" + "type": "text", + "template": "{antigravity.gemini.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#007A6CFF", + "opacity": "antigravity.gemini.opacity" + } } }, { - "id": "opencode-session-value-dark", - "name": "dark OpenCode 5-hour value", - "render": "system.dark", + "id": "antigravity-session-external-value-dark", + "name": "dark Antigravity External session value", + "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", "visibility": "100", - "parent": "opencode-provider", + "parent": "antigravity-provider", "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", "y": "8", "width": "62", @@ -2631,7 +2838,7 @@ "gap": "0", "content": { "type": "text", - "template": "{opencode.session.display:usage_line}", + "template": "{antigravity.external.session.display:usage_line}", "font_family": "Segoe UI", "font_size": "12", "weight": "medium", @@ -2639,18 +2846,51 @@ "contrast": "1", "align": "left", "color": { - "color": "#81C784FF", - "opacity": "1" + "color": "#98D0E0FF", + "opacity": "antigravity.external.opacity" } } }, { - "id": "opencode-session-segments-light", - "name": "light OpenCode 5-hour segments", - "render": "1 - system.dark", + "id": "antigravity-session-external-value-light", + "name": "light Antigravity External session value", + "render": "(1 - system.dark && !antigravity.dual_mode) && antigravity.external.opacity > 0.01", "visibility": "100", - "parent": "opencode-provider", - "y": "10", + "parent": "antigravity-provider", + "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "8", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{antigravity.external.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#2D6F82FF", + "opacity": "antigravity.external.opacity" + } + } + }, + { + "id": "antigravity-weekly-gemini-solo-dark", + "name": "dark Antigravity Gemini solo weekly segments", + "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "100 * antigravity.gemini.opacity", + "parent": "antigravity-provider", + "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", "background": { @@ -2663,14 +2903,14 @@ "gap": "0", "content": { "type": "progress", - "value": "opencode.session.display", + "value": "antigravity.gemini.weekly.display", "direction": "left_to_right", "fill": { - "color": "#2E7D32FF", + "color": "#00D2B4FF", "opacity": "1" }, "track": { - "color": "#AAAAAAFF", + "color": "#444444FF", "opacity": "1" }, "corner_radius": "1.5", @@ -2680,17 +2920,21 @@ } }, { - "id": "opencode-session-value-light", - "name": "light OpenCode 5-hour value", - "render": "1 - system.dark", - "visibility": "100", - "parent": "opencode-provider", - "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "8", - "width": "62", - "height": "13", + "id": "antigravity-weekly-gemini-solo-dark-pace", + "name": "dark Antigravity Gemini solo weekly segments pace tick", + "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.gemini.opacity", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", "background": { - "type": "none" + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } }, "border": null, "corner_radius": "0", @@ -2698,26 +2942,15 @@ "align": "start", "gap": "0", "content": { - "type": "text", - "template": "{opencode.session.display:usage_line}", - "font_family": "Segoe UI", - "font_size": "12", - "weight": "medium", - "rendering": "clear_type", - "contrast": "1", - "align": "left", - "color": { - "color": "#1B5E20FF", - "opacity": "1" - } + "type": "none" } }, { - "id": "opencode-weekly-segments-dark", - "name": "dark OpenCode long-window segments", - "render": "system.dark", - "visibility": "100", - "parent": "opencode-provider", + "id": "antigravity-weekly-gemini-solo-light", + "name": "light Antigravity Gemini solo weekly segments", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "100 * antigravity.gemini.opacity", + "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", @@ -2731,14 +2964,14 @@ "gap": "0", "content": { "type": "progress", - "value": "opencode.weekly.display", + "value": "antigravity.gemini.weekly.display", "direction": "left_to_right", "fill": { - "color": "#4CAF50FF", + "color": "#00A896FF", "opacity": "1" }, "track": { - "color": "#444444FF", + "color": "#AAAAAAFF", "opacity": "1" }, "corner_radius": "1.5", @@ -2748,17 +2981,21 @@ } }, { - "id": "opencode-weekly-value-dark", - "name": "dark OpenCode long-window value", - "render": "system.dark", - "visibility": "100", - "parent": "opencode-provider", - "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "25", - "width": "90", - "height": "13", + "id": "antigravity-weekly-gemini-solo-light-pace", + "name": "light Antigravity Gemini solo weekly segments pace tick", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "(100) * antigravity.gemini.weekly.expected_available * display.show_pace * antigravity.gemini.opacity", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.gemini.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", "background": { - "type": "none" + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } }, "border": null, "corner_radius": "0", @@ -2766,26 +3003,15 @@ "align": "start", "gap": "0", "content": { - "type": "text", - "template": "{opencode.weekly.label} {opencode.weekly.display:usage_line}", - "font_family": "Segoe UI", - "font_size": "12", - "weight": "medium", - "rendering": "clear_type", - "contrast": "1", - "align": "left", - "color": { - "color": "#81C784FF", - "opacity": "1" - } + "type": "none" } }, { - "id": "opencode-weekly-segments-light", - "name": "light OpenCode long-window segments", - "render": "1 - system.dark", - "visibility": "100", - "parent": "opencode-provider", + "id": "antigravity-weekly-external-solo-dark", + "name": "dark Antigravity External solo weekly segments", + "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "100 * antigravity.external.opacity", + "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", @@ -2799,14 +3025,14 @@ "gap": "0", "content": { "type": "progress", - "value": "opencode.weekly.display", + "value": "antigravity.external.weekly.display", "direction": "left_to_right", "fill": { - "color": "#2E7D32FF", + "color": "#6EB4C8FF", "opacity": "1" }, "track": { - "color": "#AAAAAAFF", + "color": "#444444FF", "opacity": "1" }, "corner_radius": "1.5", @@ -2816,17 +3042,21 @@ } }, { - "id": "opencode-weekly-value-light", - "name": "light OpenCode long-window value", - "render": "1 - system.dark", - "visibility": "100", - "parent": "opencode-provider", - "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "25", - "width": "90", - "height": "13", + "id": "antigravity-weekly-external-solo-dark-pace", + "name": "dark Antigravity External solo weekly segments pace tick", + "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.external.opacity", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", "background": { - "type": "none" + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } }, "border": null, "corner_radius": "0", @@ -2834,26 +3064,15 @@ "align": "start", "gap": "0", "content": { - "type": "text", - "template": "{opencode.weekly.label} {opencode.weekly.display:usage_line}", - "font_family": "Segoe UI", - "font_size": "12", - "weight": "medium", - "rendering": "clear_type", - "contrast": "1", - "align": "left", - "color": { - "color": "#1B5E20FF", - "opacity": "1" - } + "type": "none" } }, { - "id": "claude-credits-segments-dark", - "name": "dark claude credit overlay", - "render": "(system.dark) && claude.credits.available", - "visibility": "100", - "parent": "claude-provider", + "id": "antigravity-weekly-external-solo-light", + "name": "light Antigravity External solo weekly segments", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "100 * antigravity.external.opacity", + "parent": "antigravity-provider", "y": "27", "width": "ceil(10 / max(1, providers.count)) * 11 - 1", "height": "9", @@ -2867,15 +3086,15 @@ "gap": "0", "content": { "type": "progress", - "value": "claude.credits.display", + "value": "antigravity.external.weekly.display", "direction": "left_to_right", "fill": { - "color": "#F5A623FF", + "color": "#4A8B9EFF", "opacity": "1" }, "track": { - "color": "#00000000", - "opacity": "0" + "color": "#AAAAAAFF", + "opacity": "1" }, "corner_radius": "1.5", "segments": 10, @@ -2884,13 +3103,2020 @@ } }, { - "id": "claude-credits-value-dark", - "name": "dark claude credit balance", - "render": "(system.dark) && claude.credits.available", + "id": "antigravity-weekly-external-solo-light-pace", + "name": "light Antigravity External solo weekly segments pace tick", + "render": "(1 - system.dark) && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "(100) * antigravity.external.weekly.expected_available * display.show_pace * antigravity.external.opacity", + "parent": "antigravity-provider", + "x": "round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(antigravity.external.weekly.expected_display, 0, 100) / 100)", + "y": "(27) - 1", + "width": "1", + "height": "(9) + 2", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "antigravity-weekly-gemini-value-dark", + "name": "dark Antigravity Gemini weekly value", + "render": "system.dark && !antigravity.dual_mode && antigravity.gemini.opacity > 0.01", + "visibility": "100", + "parent": "antigravity-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": "{antigravity.gemini.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#5EEAD4FF", + "opacity": "antigravity.gemini.opacity" + } + } + }, + { + "id": "antigravity-weekly-gemini-value-light", + "name": "light Antigravity Gemini weekly value", + "render": "(1 - system.dark && !antigravity.dual_mode) && antigravity.gemini.opacity > 0.01", + "visibility": "100", + "parent": "antigravity-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": "{antigravity.gemini.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#007A6CFF", + "opacity": "antigravity.gemini.opacity" + } + } + }, + { + "id": "antigravity-weekly-external-value-dark", + "name": "dark Antigravity External weekly value", + "render": "system.dark && !antigravity.dual_mode && antigravity.external.opacity > 0.01", + "visibility": "100", + "parent": "antigravity-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": "{antigravity.external.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#98D0E0FF", + "opacity": "antigravity.external.opacity" + } + } + }, + { + "id": "antigravity-weekly-external-value-light", + "name": "light Antigravity External weekly value", + "render": "(1 - system.dark && !antigravity.dual_mode) && antigravity.external.opacity > 0.01", + "visibility": "100", + "parent": "antigravity-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": "{antigravity.external.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#2D6F82FF", + "opacity": "antigravity.external.opacity" + } + } + }, + { + "id": "opencode-provider", + "name": "OpenCode provider", + "render": "providers.opencode.enabled", + "visibility": "100", + "parent": "provider-row", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4 + 90", + "height": "46", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "opencode-session-segments-dark", + "name": "dark OpenCode 5-hour segments", + "render": "system.dark", + "visibility": "100", + "parent": "opencode-provider", + "y": "10", + "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": "opencode.session.display", + "direction": "left_to_right", + "fill": { + "color": "#4CAF50FF", + "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": "opencode-session-value-dark", + "name": "dark OpenCode 5-hour value", + "render": "system.dark", + "visibility": "100", + "parent": "opencode-provider", + "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "8", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{opencode.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#81C784FF", + "opacity": "1" + } + } + }, + { + "id": "opencode-session-segments-light", + "name": "light OpenCode 5-hour segments", + "render": "1 - system.dark", + "visibility": "100", + "parent": "opencode-provider", + "y": "10", + "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": "opencode.session.display", + "direction": "left_to_right", + "fill": { + "color": "#2E7D32FF", + "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": "opencode-session-value-light", + "name": "light OpenCode 5-hour value", + "render": "1 - system.dark", + "visibility": "100", + "parent": "opencode-provider", + "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "8", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{opencode.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#1B5E20FF", + "opacity": "1" + } + } + }, + { + "id": "opencode-weekly-segments-dark", + "name": "dark OpenCode long-window segments", + "render": "system.dark", + "visibility": "100", + "parent": "opencode-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": "opencode.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#4CAF50FF", + "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": "opencode-weekly-value-dark", + "name": "dark OpenCode long-window value", + "render": "system.dark", + "visibility": "100", + "parent": "opencode-provider", + "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "25", + "width": "90", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{opencode.weekly.label} {opencode.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#81C784FF", + "opacity": "1" + } + } + }, + { + "id": "opencode-weekly-segments-light", + "name": "light OpenCode long-window segments", + "render": "1 - system.dark", + "visibility": "100", + "parent": "opencode-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": "opencode.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#2E7D32FF", + "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": "opencode-weekly-value-light", + "name": "light OpenCode long-window value", + "render": "1 - system.dark", + "visibility": "100", + "parent": "opencode-provider", + "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "25", + "width": "90", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{opencode.weekly.label} {opencode.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#1B5E20FF", + "opacity": "1" + } + } + }, + { + "id": "claude-credits-segments-dark", + "name": "dark claude credit overlay", + "render": "((system.dark) && claude.credits.available) && (!claude.dual_mode)", + "visibility": "100", + "parent": "claude-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": "claude.credits.display", + "direction": "left_to_right", + "fill": { + "color": "#F5A623FF", + "opacity": "1" + }, + "track": { + "color": "#00000000", + "opacity": "0" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-credits-value-dark", + "name": "dark claude credit balance", + "render": "((system.dark) && claude.credits.available) && (!claude.dual_mode)", + "visibility": "100", + "parent": "claude-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": "${claude.credits.balance:0.00}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#F5A623FF", + "opacity": "1" + } + } + }, + { + "id": "claude-credits-segments-light", + "name": "light claude credit overlay", + "render": "((1 - system.dark) && claude.credits.available) && (!claude.dual_mode)", + "visibility": "100", + "parent": "claude-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": "claude.credits.display", + "direction": "left_to_right", + "fill": { + "color": "#C77700FF", + "opacity": "1" + }, + "track": { + "color": "#00000000", + "opacity": "0" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-credits-value-light", + "name": "light claude credit balance", + "render": "((1 - system.dark) && claude.credits.available) && (!claude.dual_mode)", + "visibility": "100", + "parent": "claude-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": "${claude.credits.balance:0.00}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#C77700FF", + "opacity": "1" + } + } + }, + { + "id": "claude-main-marker-dark-normal", + "name": "dark Claude Main marker o normal", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "0", + "y": "7", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "o", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#D97757FF", + "opacity": "claude.normal.opacity" + } + } + }, + { + "id": "claude-main-marker-dark-hover", + "name": "dark Claude Main marker o hover", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "0", + "y": "7", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "o", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#C45A38FF", + "opacity": "claude.hover.opacity" + } + } + }, + { + "id": "claude-main-marker-light-normal", + "name": "light Claude Main marker o normal", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "0", + "y": "7", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "o", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#A94F32FF", + "opacity": "claude.normal.opacity" + } + } + }, + { + "id": "claude-main-marker-light-hover", + "name": "light Claude Main marker o hover", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "0", + "y": "7", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "o", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#7C2D12FF", + "opacity": "claude.hover.opacity" + } + } + }, + { + "id": "claude-main-5h-dark-normal", + "name": "dark Claude Main 5h normal", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100 * claude.normal.opacity", + "parent": "claude-provider", + "x": "12", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.default.session.display", + "direction": "left_to_right", + "fill": { + "color": "#D97757FF", + "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": "claude-main-5h-dark-normal-pace", + "name": "dark Claude Main 5h normal pace tick", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "(100) * accounts.claude.default.session.expected_available * display.show_pace * claude.normal.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.default.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-main-5h-dark-hover", + "name": "dark Claude Main 5h hover", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100 * claude.hover.opacity", + "parent": "claude-provider", + "x": "12", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.default.session.display", + "direction": "left_to_right", + "fill": { + "color": "#D97757FF", + "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": "claude-main-5h-dark-hover-pace", + "name": "dark Claude Main 5h hover pace tick", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "(100) * accounts.claude.default.session.expected_available * display.show_pace * claude.hover.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.default.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-main-5h-light-normal", + "name": "light Claude Main 5h normal", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100 * claude.normal.opacity", + "parent": "claude-provider", + "x": "12", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.default.session.display", + "direction": "left_to_right", + "fill": { + "color": "#D97757FF", + "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": "claude-main-5h-light-normal-pace", + "name": "light Claude Main 5h normal pace tick", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "(100) * accounts.claude.default.session.expected_available * display.show_pace * claude.normal.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.default.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-main-5h-light-hover", + "name": "light Claude Main 5h hover", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100 * claude.hover.opacity", + "parent": "claude-provider", + "x": "12", + "y": "8", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.default.session.display", + "direction": "left_to_right", + "fill": { + "color": "#D97757FF", + "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": "claude-main-5h-light-hover-pace", + "name": "light Claude Main 5h hover pace tick", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "(100) * accounts.claude.default.session.expected_available * display.show_pace * claude.hover.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.default.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-main-7d-dark-normal", + "name": "dark Claude Main 7d normal", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100 * claude.normal.opacity", + "parent": "claude-provider", + "x": "12", + "y": "16", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.default.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#A44525FF", + "opacity": "1" + }, + "track": { + "color": "#333333FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-main-7d-dark-normal-pace", + "name": "dark Claude Main 7d normal pace tick", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "(100) * accounts.claude.default.weekly.expected_available * display.show_pace * claude.normal.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.default.weekly.expected_display, 0, 100) / 100)", + "y": "15", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-main-7d-dark-hover", + "name": "dark Claude Main 7d hover", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100 * claude.hover.opacity", + "parent": "claude-provider", + "x": "12", + "y": "13", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.default.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#A44525FF", + "opacity": "1" + }, + "track": { + "color": "#333333FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-main-7d-dark-hover-pace", + "name": "dark Claude Main 7d hover pace tick", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "(100) * accounts.claude.default.weekly.expected_available * display.show_pace * claude.hover.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.default.weekly.expected_display, 0, 100) / 100)", + "y": "12", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-main-7d-light-normal", + "name": "light Claude Main 7d normal", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100 * claude.normal.opacity", + "parent": "claude-provider", + "x": "12", + "y": "16", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.default.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#7C2D12FF", + "opacity": "1" + }, + "track": { + "color": "#CCCCCCFF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-main-7d-light-normal-pace", + "name": "light Claude Main 7d normal pace tick", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "(100) * accounts.claude.default.weekly.expected_available * display.show_pace * claude.normal.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.default.weekly.expected_display, 0, 100) / 100)", + "y": "15", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-main-7d-light-hover", + "name": "light Claude Main 7d hover", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100 * claude.hover.opacity", + "parent": "claude-provider", + "x": "12", + "y": "13", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.default.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#7C2D12FF", + "opacity": "1" + }, + "track": { + "color": "#CCCCCCFF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-main-7d-light-hover-pace", + "name": "light Claude Main 7d hover pace tick", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "(100) * accounts.claude.default.weekly.expected_available * display.show_pace * claude.hover.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.default.weekly.expected_display, 0, 100) / 100)", + "y": "12", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-main-text-dark-normal", + "name": "dark Claude Main text 5h", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "7", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.claude.default.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#F09A7AFF", + "opacity": "claude.normal.opacity" + } + } + }, + { + "id": "claude-main-text-dark-hover", + "name": "dark Claude Main text 7d", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "7", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.claude.default.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#D97757FF", + "opacity": "claude.hover.opacity" + } + } + }, + { + "id": "claude-main-text-light-normal", + "name": "light Claude Main text 5h", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "7", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.claude.default.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#A94F32FF", + "opacity": "claude.normal.opacity" + } + } + }, + { + "id": "claude-main-text-light-hover", + "name": "light Claude Main text 7d", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "7", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.claude.default.weekly.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#7C2D12FF", + "opacity": "claude.hover.opacity" + } + } + }, + { + "id": "claude-x-marker-dark-normal", + "name": "dark Claude X marker x normal", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "0", + "y": "26", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "x", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#D97757FF", + "opacity": "claude.normal.opacity" + } + } + }, + { + "id": "claude-x-marker-dark-hover", + "name": "dark Claude X marker x hover", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "0", + "y": "26", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "x", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#C45A38FF", + "opacity": "claude.hover.opacity" + } + } + }, + { + "id": "claude-x-marker-light-normal", + "name": "light Claude X marker x normal", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "0", + "y": "26", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "x", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#A94F32FF", + "opacity": "claude.normal.opacity" + } + } + }, + { + "id": "claude-x-marker-light-hover", + "name": "light Claude X marker x hover", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "0", + "y": "26", + "width": "10", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "x", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#7C2D12FF", + "opacity": "claude.hover.opacity" + } + } + }, + { + "id": "claude-x-5h-dark-normal", + "name": "dark Claude X 5h normal", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100 * claude.normal.opacity", + "parent": "claude-provider", + "x": "12", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.claude_x.session.display", + "direction": "left_to_right", + "fill": { + "color": "#D97757FF", + "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": "claude-x-5h-dark-normal-pace", + "name": "dark Claude X 5h normal pace tick", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "(100) * accounts.claude.claude_x.session.expected_available * display.show_pace * claude.normal.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.claude_x.session.expected_display, 0, 100) / 100)", + "y": "26", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-x-5h-dark-hover", + "name": "dark Claude X 5h hover", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100 * claude.hover.opacity", + "parent": "claude-provider", + "x": "12", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.claude_x.session.display", + "direction": "left_to_right", + "fill": { + "color": "#D97757FF", + "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": "claude-x-5h-dark-hover-pace", + "name": "dark Claude X 5h hover pace tick", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "(100) * accounts.claude.claude_x.session.expected_available * display.show_pace * claude.hover.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.claude_x.session.expected_display, 0, 100) / 100)", + "y": "26", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-x-5h-light-normal", + "name": "light Claude X 5h normal", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100 * claude.normal.opacity", + "parent": "claude-provider", + "x": "12", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.claude_x.session.display", + "direction": "left_to_right", + "fill": { + "color": "#D97757FF", + "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": "claude-x-5h-light-normal-pace", + "name": "light Claude X 5h normal pace tick", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "(100) * accounts.claude.claude_x.session.expected_available * display.show_pace * claude.normal.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.claude_x.session.expected_display, 0, 100) / 100)", + "y": "26", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-x-5h-light-hover", + "name": "light Claude X 5h hover", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100 * claude.hover.opacity", + "parent": "claude-provider", + "x": "12", + "y": "27", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.claude_x.session.display", + "direction": "left_to_right", + "fill": { + "color": "#D97757FF", + "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": "claude-x-5h-light-hover-pace", + "name": "light Claude X 5h hover pace tick", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "(100) * accounts.claude.claude_x.session.expected_available * display.show_pace * claude.hover.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.claude_x.session.expected_display, 0, 100) / 100)", + "y": "26", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-x-7d-dark-normal", + "name": "dark Claude X 7d normal", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100 * claude.normal.opacity", + "parent": "claude-provider", + "x": "12", + "y": "35", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.claude_x.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#A44525FF", + "opacity": "1" + }, + "track": { + "color": "#333333FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-x-7d-dark-normal-pace", + "name": "dark Claude X 7d normal pace tick", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "(100) * accounts.claude.claude_x.weekly.expected_available * display.show_pace * claude.normal.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.claude_x.weekly.expected_display, 0, 100) / 100)", + "y": "34", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-x-7d-dark-hover", + "name": "dark Claude X 7d hover", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100 * claude.hover.opacity", + "parent": "claude-provider", + "x": "12", + "y": "32", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.claude_x.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#A44525FF", + "opacity": "1" + }, + "track": { + "color": "#333333FF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-x-7d-dark-hover-pace", + "name": "dark Claude X 7d hover pace tick", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "(100) * accounts.claude.claude_x.weekly.expected_available * display.show_pace * claude.hover.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.claude_x.weekly.expected_display, 0, 100) / 100)", + "y": "31", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-x-7d-light-normal", + "name": "light Claude X 7d normal", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100 * claude.normal.opacity", + "parent": "claude-provider", + "x": "12", + "y": "35", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "3", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.claude_x.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#7C2D12FF", + "opacity": "1" + }, + "track": { + "color": "#CCCCCCFF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-x-7d-light-normal-pace", + "name": "light Claude X 7d normal pace tick", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "(100) * accounts.claude.claude_x.weekly.expected_available * display.show_pace * claude.normal.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.claude_x.weekly.expected_display, 0, 100) / 100)", + "y": "34", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-x-7d-light-hover", + "name": "light Claude X 7d hover", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "100 * claude.hover.opacity", + "parent": "claude-provider", + "x": "12", + "y": "32", + "width": "ceil(10 / max(1, providers.count)) * 11 - 1", + "height": "6", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "progress", + "value": "accounts.claude.claude_x.weekly.display", + "direction": "left_to_right", + "fill": { + "color": "#7C2D12FF", + "opacity": "1" + }, + "track": { + "color": "#CCCCCCFF", + "opacity": "1" + }, + "corner_radius": "1.5", + "segments": 10, + "segments_expression": "ceil(10 / max(1, providers.count))", + "segment_gap": "1" + } + }, + { + "id": "claude-x-7d-light-hover-pace", + "name": "light Claude X 7d hover pace tick", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", + "visibility": "(100) * accounts.claude.claude_x.weekly.expected_available * display.show_pace * claude.hover.opacity", + "parent": "claude-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.claude.claude_x.weekly.expected_display, 0, 100) / 100)", + "y": "31", + "width": "1", + "height": "8", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, + { + "id": "claude-x-text-dark-normal", + "name": "dark Claude X text 5h", + "render": "(system.dark) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", + "visibility": "100", + "parent": "claude-provider", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "26", + "width": "62", + "height": "13", + "background": { + "type": "none" + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "text", + "template": "{accounts.claude.claude_x.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#F09A7AFF", + "opacity": "claude.normal.opacity" + } + } + }, + { + "id": "claude-x-text-dark-hover", + "name": "dark Claude X text 7d", + "render": "(system.dark) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", "visibility": "100", "parent": "claude-provider", - "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "25", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "26", "width": "62", "height": "13", "background": { @@ -2903,7 +5129,7 @@ "gap": "0", "content": { "type": "text", - "template": "${claude.credits.balance:0.00}", + "template": "{accounts.claude.claude_x.weekly.display:usage_line}", "font_family": "Segoe UI", "font_size": "12", "weight": "medium", @@ -2911,20 +5137,21 @@ "contrast": "1", "align": "left", "color": { - "color": "#F5A623FF", - "opacity": "1" + "color": "#D97757FF", + "opacity": "claude.hover.opacity" } } }, { - "id": "claude-credits-segments-light", - "name": "light claude credit overlay", - "render": "(1 - system.dark) && claude.credits.available", + "id": "claude-x-text-light-normal", + "name": "light Claude X text 5h", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.normal.opacity > 0.01)", "visibility": "100", "parent": "claude-provider", - "y": "27", - "width": "ceil(10 / max(1, providers.count)) * 11 - 1", - "height": "9", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "26", + "width": "62", + "height": "13", "background": { "type": "none" }, @@ -2934,31 +5161,28 @@ "align": "start", "gap": "0", "content": { - "type": "progress", - "value": "claude.credits.display", - "direction": "left_to_right", - "fill": { - "color": "#C77700FF", - "opacity": "1" - }, - "track": { - "color": "#00000000", - "opacity": "0" - }, - "corner_radius": "1.5", - "segments": 10, - "segments_expression": "ceil(10 / max(1, providers.count))", - "segment_gap": "1" + "type": "text", + "template": "{accounts.claude.claude_x.session.display:usage_line}", + "font_family": "Segoe UI", + "font_size": "12", + "weight": "medium", + "rendering": "clear_type", + "contrast": "1", + "align": "left", + "color": { + "color": "#A94F32FF", + "opacity": "claude.normal.opacity" + } } }, { - "id": "claude-credits-value-light", - "name": "light claude credit balance", - "render": "(1 - system.dark) && claude.credits.available", + "id": "claude-x-text-light-hover", + "name": "light Claude X text 7d", + "render": "((1 - system.dark)) && (claude.dual_mode) && (claude.hover.opacity > 0.01)", "visibility": "100", "parent": "claude-provider", - "x": "ceil(10 / max(1, providers.count)) * 11 - 1 + 4", - "y": "25", + "x": "12 + ceil(10 / max(1, providers.count)) * 11 - 1 + 4", + "y": "26", "width": "62", "height": "13", "background": { @@ -2971,7 +5195,7 @@ "gap": "0", "content": { "type": "text", - "template": "${claude.credits.balance:0.00}", + "template": "{accounts.claude.claude_x.weekly.display:usage_line}", "font_family": "Segoe UI", "font_size": "12", "weight": "medium", @@ -2979,8 +5203,8 @@ "contrast": "1", "align": "left", "color": { - "color": "#C77700FF", - "opacity": "1" + "color": "#7C2D12FF", + "opacity": "claude.hover.opacity" } } }, @@ -3122,7 +5346,7 @@ }, { "id": "codex-main-marker-dark-normal", - "name": "dark Codex Main marker o (5h color)", + "name": "dark Codex Main marker o normal", "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", "visibility": "100", "parent": "codex-provider", @@ -3155,7 +5379,7 @@ }, { "id": "codex-main-marker-dark-hover", - "name": "dark Codex Main marker o (7d color)", + "name": "dark Codex Main marker o hover", "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", "visibility": "100", "parent": "codex-provider", @@ -3188,7 +5412,7 @@ }, { "id": "codex-main-marker-light-normal", - "name": "light Codex Main marker o (5h color)", + "name": "light Codex Main marker o normal", "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", "visibility": "100", "parent": "codex-provider", @@ -3221,7 +5445,7 @@ }, { "id": "codex-main-marker-light-hover", - "name": "light Codex Main marker o (7d color)", + "name": "light Codex Main marker o hover", "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", "visibility": "100", "parent": "codex-provider", @@ -3247,7 +5471,7 @@ "contrast": "1", "align": "left", "color": { - "color": "#1E40AFFF", + "color": "#1D4ED8FF", "opacity": "codex.hover.opacity" } } @@ -3275,7 +5499,7 @@ "value": "accounts.codex.default.session.display", "direction": "left_to_right", "fill": { - "color": "#3B82F6FF", + "color": "#60A5FAFF", "opacity": "1" }, "track": { @@ -3337,7 +5561,7 @@ "value": "accounts.codex.default.session.display", "direction": "left_to_right", "fill": { - "color": "#3B82F6FF", + "color": "#60A5FAFF", "opacity": "1" }, "track": { @@ -3350,6 +5574,32 @@ "segment_gap": "1" } }, + { + "id": "codex-main-5h-dark-hover-pace", + "name": "dark Codex Main 5h hover pace tick", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "(100) * accounts.codex.default.session.expected_available * display.show_pace * codex.hover.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.default.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-main-5h-light-normal", "name": "light Codex Main 5h normal", @@ -3448,6 +5698,32 @@ "segment_gap": "1" } }, + { + "id": "codex-main-5h-light-hover-pace", + "name": "light Codex Main 5h hover pace tick", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "(100) * accounts.codex.default.session.expected_available * display.show_pace * codex.hover.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.default.session.expected_display, 0, 100) / 100)", + "y": "7", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-main-7d-dark-normal", "name": "dark Codex Main 7d normal", @@ -3484,6 +5760,32 @@ "segment_gap": "1" } }, + { + "id": "codex-main-7d-dark-normal-pace", + "name": "dark Codex Main 7d normal pace tick", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "(100) * accounts.codex.default.weekly.expected_available * display.show_pace * codex.normal.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.default.weekly.expected_display, 0, 100) / 100)", + "y": "15", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-main-7d-dark-hover", "name": "dark Codex Main 7d hover", @@ -3573,7 +5875,7 @@ "opacity": "1" }, "track": { - "color": "#999999FF", + "color": "#CCCCCCFF", "opacity": "1" }, "corner_radius": "1.5", @@ -3582,6 +5884,32 @@ "segment_gap": "1" } }, + { + "id": "codex-main-7d-light-normal-pace", + "name": "light Codex Main 7d normal pace tick", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "(100) * accounts.codex.default.weekly.expected_available * display.show_pace * codex.normal.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.default.weekly.expected_display, 0, 100) / 100)", + "y": "15", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-main-7d-light-hover", "name": "light Codex Main 7d hover", @@ -3609,7 +5937,7 @@ "opacity": "1" }, "track": { - "color": "#999999FF", + "color": "#CCCCCCFF", "opacity": "1" }, "corner_radius": "1.5", @@ -3771,14 +6099,14 @@ "contrast": "1", "align": "left", "color": { - "color": "#1E40AFFF", + "color": "#1D4ED8FF", "opacity": "codex.hover.opacity" } } }, { "id": "codex-x-marker-dark-normal", - "name": "dark Codex X marker x (5h color)", + "name": "dark Codex X marker x normal", "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", "visibility": "100", "parent": "codex-provider", @@ -3811,7 +6139,7 @@ }, { "id": "codex-x-marker-dark-hover", - "name": "dark Codex X marker x (7d color)", + "name": "dark Codex X marker x hover", "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", "visibility": "100", "parent": "codex-provider", @@ -3844,7 +6172,7 @@ }, { "id": "codex-x-marker-light-normal", - "name": "light Codex X marker x (5h color)", + "name": "light Codex X marker x normal", "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", "visibility": "100", "parent": "codex-provider", @@ -3877,7 +6205,7 @@ }, { "id": "codex-x-marker-light-hover", - "name": "light Codex X marker x (7d color)", + "name": "light Codex X marker x hover", "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", "visibility": "100", "parent": "codex-provider", @@ -3903,7 +6231,7 @@ "contrast": "1", "align": "left", "color": { - "color": "#1E40AFFF", + "color": "#1D4ED8FF", "opacity": "codex.hover.opacity" } } @@ -3931,7 +6259,7 @@ "value": "accounts.codex.codex_x.session.display", "direction": "left_to_right", "fill": { - "color": "#3B82F6FF", + "color": "#60A5FAFF", "opacity": "1" }, "track": { @@ -3993,7 +6321,7 @@ "value": "accounts.codex.codex_x.session.display", "direction": "left_to_right", "fill": { - "color": "#3B82F6FF", + "color": "#60A5FAFF", "opacity": "1" }, "track": { @@ -4006,6 +6334,32 @@ "segment_gap": "1" } }, + { + "id": "codex-x-5h-dark-hover-pace", + "name": "dark Codex X 5h hover pace tick", + "render": "(system.dark) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "(100) * accounts.codex.codex_x.session.expected_available * display.show_pace * codex.hover.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.codex_x.session.expected_display, 0, 100) / 100)", + "y": "26", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-x-5h-light-normal", "name": "light Codex X 5h normal", @@ -4104,6 +6458,32 @@ "segment_gap": "1" } }, + { + "id": "codex-x-5h-light-hover-pace", + "name": "light Codex X 5h hover pace tick", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.hover.opacity > 0.01)", + "visibility": "(100) * accounts.codex.codex_x.session.expected_available * display.show_pace * codex.hover.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.codex_x.session.expected_display, 0, 100) / 100)", + "y": "26", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-x-7d-dark-normal", "name": "dark Codex X 7d normal", @@ -4140,6 +6520,32 @@ "segment_gap": "1" } }, + { + "id": "codex-x-7d-dark-normal-pace", + "name": "dark Codex X 7d normal pace tick", + "render": "(system.dark) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "(100) * accounts.codex.codex_x.weekly.expected_available * display.show_pace * codex.normal.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.codex_x.weekly.expected_display, 0, 100) / 100)", + "y": "34", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#FFFFFFFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-x-7d-dark-hover", "name": "dark Codex X 7d hover", @@ -4229,7 +6635,7 @@ "opacity": "1" }, "track": { - "color": "#999999FF", + "color": "#CCCCCCFF", "opacity": "1" }, "corner_radius": "1.5", @@ -4238,6 +6644,32 @@ "segment_gap": "1" } }, + { + "id": "codex-x-7d-light-normal-pace", + "name": "light Codex X 7d normal pace tick", + "render": "((1 - system.dark)) && (codex.dual_mode) && (codex.normal.opacity > 0.01)", + "visibility": "(100) * accounts.codex.codex_x.weekly.expected_available * display.show_pace * codex.normal.opacity", + "parent": "codex-provider", + "x": "12 + round((ceil(10 / max(1, providers.count)) * 11 - 2) * clamp(accounts.codex.codex_x.weekly.expected_display, 0, 100) / 100)", + "y": "34", + "width": "1", + "height": "5", + "background": { + "type": "colour", + "colour": { + "color": "#1A1A1AFF", + "opacity": "0.9" + } + }, + "border": null, + "corner_radius": "0", + "layout": "freeform", + "align": "start", + "gap": "0", + "content": { + "type": "none" + } + }, { "id": "codex-x-7d-light-hover", "name": "light Codex X 7d hover", @@ -4265,7 +6697,7 @@ "opacity": "1" }, "track": { - "color": "#999999FF", + "color": "#CCCCCCFF", "opacity": "1" }, "corner_radius": "1.5", @@ -4427,7 +6859,7 @@ "contrast": "1", "align": "left", "color": { - "color": "#1E40AFFF", + "color": "#1D4ED8FF", "opacity": "codex.hover.opacity" } } diff --git a/src/window.rs b/src/window.rs index 141d6f35..f563a384 100644 --- a/src/window.rs +++ b/src/window.rs @@ -104,6 +104,10 @@ struct AppState { antigravity_hovered: bool, crossfade_animation: Option, pub(super) antigravity_tile_rect: Option<(f64, f64, f64, f64)>, + pub(super) claude_hovered: bool, + pub(super) claude_crossfade_opacity: f64, + pub(super) claude_crossfade_animation: Option, + pub(super) claude_tile_rect: Option<(f64, f64, f64, f64)>, pub(super) codex_hovered: bool, pub(super) codex_crossfade_opacity: f64, pub(super) codex_crossfade_animation: Option, @@ -227,6 +231,8 @@ pub(super) const IDM_PROVIDER_ANTIGRAVITY_GEMINI: u16 = 68; pub(super) const IDM_PROVIDER_ANTIGRAVITY_EXTERNAL: u16 = 67; pub(super) const IDM_PROVIDER_CODEX_MAIN: u16 = 69; pub(super) const IDM_PROVIDER_CODEX_X: u16 = 70; +pub(super) const IDM_PROVIDER_CLAUDE_MAIN: u16 = 72; +pub(super) const IDM_PROVIDER_CLAUDE_X: u16 = 73; const WM_DPICHANGED_MSG: u32 = 0x02E0; const WM_APP_UPDATE_CHECK_COMPLETE: u32 = WM_APP + 2; @@ -814,6 +820,28 @@ fn theme_runtime_from_state(state: &AppState) -> ThemeRuntime { state.show_antigravity_external, state.crossfade_opacity, ) + .with_claude_state( + state.claude_hovered, + state.providers.contains(ProviderId::Claude) + && state + .accounts + .claude + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true), + state.providers.contains(ProviderId::Claude) + && state + .accounts + .claude + .profiles + .iter() + .find(|p| p.id == "claude_x") + .map(|p| p.enabled) + .unwrap_or(false), + state.claude_crossfade_opacity, + ) .with_codex_state( state.codex_hovered, state.providers.contains(ProviderId::Codex) @@ -1964,6 +1992,7 @@ fn replace_active_theme(state: &mut AppState, loaded: ThemeDocument, path: Optio } state.active_theme = Some(loaded); state.antigravity_tile_rect = None; + state.claude_tile_rect = None; state.codex_tile_rect = None; if path.is_some() { state.active_theme_path = path; @@ -2631,6 +2660,10 @@ pub fn run() { antigravity_hovered: false, crossfade_animation: None, antigravity_tile_rect: None, + claude_hovered: false, + claude_crossfade_opacity: 0.0, + claude_crossfade_animation: None, + claude_tile_rect: None, codex_hovered: false, codex_crossfade_opacity: 0.0, codex_crossfade_animation: None, @@ -2813,6 +2846,22 @@ fn render_layered() { state.antigravity_tile_rect = tile_rect; } } + let needs_claude_rect = lock_state() + .as_ref() + .map(|s| s.claude_tile_rect.is_none()) + .unwrap_or(false); + if needs_claude_rect { + let tile_rect = theme_engine::object_bounds( + &theme, + 0, + usage_data.as_ref(), + surface_runtime, + mouse::CLAUDE_HOVER_OBJECT, + ); + if let Some(state) = lock_state().as_mut() { + state.claude_tile_rect = tile_rect; + } + } let needs_codex_rect = lock_state() .as_ref() .map(|s| s.codex_tile_rect.is_none()) @@ -3444,8 +3493,9 @@ fn check_language_change() { } } -const ANTIGRAVITY_CROSSFADE_DURATION_MS: u64 = 70; +const ANTIGRAVITY_CROSSFADE_DURATION_MS: u64 = 300; const CODEX_CROSSFADE_DURATION_MS: u64 = 300; +const CLAUDE_CROSSFADE_DURATION_MS: u64 = 300; const ANTIGRAVITY_CROSSFADE_TICK_MS: u32 = 16; #[derive(Clone, Copy, Debug)] @@ -3514,7 +3564,56 @@ pub(super) fn tick_antigravity_crossfade(hwnd: HWND, throttle: bool) { if !still_animating { let any_animating = { let state = lock_state(); - state.as_ref().is_some_and(|s| s.crossfade_animation.is_some() || s.codex_crossfade_animation.is_some()) + state.as_ref().is_some_and(|s| { + s.crossfade_animation.is_some() + || s.codex_crossfade_animation.is_some() + || s.claude_crossfade_animation.is_some() + }) + }; + if !any_animating { + unsafe { + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } + } + } + if should_render { + render_layered(); + } +} + +pub(super) fn tick_claude_crossfade(hwnd: HWND, throttle: bool) { + let (should_render, still_animating) = { + let mut state = lock_state(); + let Some(state) = state.as_mut() else { + return; + }; + let Some(mut anim) = state.claude_crossfade_animation else { + return; + }; + let now = std::time::Instant::now(); + if throttle && now.duration_since(anim.last_rendered) < std::time::Duration::from_millis(15) + { + return; + } + let (opacity, more) = anim.current_opacity(); + state.claude_crossfade_opacity = opacity; + anim.last_rendered = now; + if more { + state.claude_crossfade_animation = Some(anim); + } else { + state.claude_crossfade_opacity = anim.target_opacity; + state.claude_crossfade_animation = None; + } + (true, more) + }; + if !still_animating { + let any_animating = { + let state = lock_state(); + state.as_ref().is_some_and(|s| { + s.crossfade_animation.is_some() + || s.codex_crossfade_animation.is_some() + || s.claude_crossfade_animation.is_some() + }) }; if !any_animating { unsafe { @@ -3527,6 +3626,57 @@ pub(super) fn tick_antigravity_crossfade(hwnd: HWND, throttle: bool) { } } +pub(super) fn set_claude_hover(hwnd: HWND, hovered: bool) { + let mut should_animate = false; + { + let mut state = lock_state(); + let Some(state) = state.as_mut() else { + return; + }; + let dual_claude = state.providers.contains(ProviderId::Claude) + && state + .accounts + .claude + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true) + && state + .accounts + .claude + .profiles + .iter() + .find(|p| p.id == "claude_x") + .map(|p| p.enabled) + .unwrap_or(false); + + if state.claude_hovered != hovered { + state.claude_hovered = hovered; + if dual_claude { + let target = if hovered { 1.0 } else { 0.0 }; + state.claude_crossfade_animation = Some(CrossfadeAnimation::new( + state.claude_crossfade_opacity, + target, + std::time::Duration::from_millis(CLAUDE_CROSSFADE_DURATION_MS), + )); + should_animate = true; + } + } + } + if should_animate { + unsafe { + let _ = set_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ANTIGRAVITY_CROSSFADE_TICK_MS, + None, + ); + } + render_layered(); + } +} + pub(super) fn tick_codex_crossfade(hwnd: HWND, throttle: bool) { let (should_render, still_animating) = { let mut state = lock_state(); @@ -3555,7 +3705,11 @@ pub(super) fn tick_codex_crossfade(hwnd: HWND, throttle: bool) { if !still_animating { let any_animating = { let state = lock_state(); - state.as_ref().is_some_and(|s| s.crossfade_animation.is_some() || s.codex_crossfade_animation.is_some()) + state.as_ref().is_some_and(|s| { + s.crossfade_animation.is_some() + || s.codex_crossfade_animation.is_some() + || s.claude_crossfade_animation.is_some() + }) }; if !any_animating { unsafe { @@ -3696,12 +3850,22 @@ fn reload_external_settings(hwnd: HWND) { state.codex_crossfade_opacity = 0.0; state.codex_crossfade_animation = None; } - if !any_pool && !codex_active { + let claude_active = state.providers.contains(ProviderId::Claude); + if !claude_active { + state.claude_hovered = false; + state.claude_crossfade_opacity = 0.0; + state.claude_crossfade_animation = None; + } + let any_animating = state.crossfade_animation.is_some() + || state.codex_crossfade_animation.is_some() + || state.claude_crossfade_animation.is_some(); + if !any_animating { unsafe { let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); } } state.antigravity_tile_rect = None; + state.claude_tile_rect = None; state.codex_tile_rect = None; update_configured_placement( state, diff --git a/src/window/message_loop.rs b/src/window/message_loop.rs index b30ca4dc..eac823db 100644 --- a/src/window/message_loop.rs +++ b/src/window/message_loop.rs @@ -132,10 +132,13 @@ pub(super) unsafe extern "system" fn wnd_proc( } TIMER_CROSSFADE_SCHEDULE => { tick_antigravity_crossfade(hwnd, false); + tick_claude_crossfade(hwnd, false); tick_codex_crossfade(hwnd, false); - let any_animating = lock_state() - .as_ref() - .is_some_and(|s| s.crossfade_animation.is_some() || s.codex_crossfade_animation.is_some()); + let any_animating = lock_state().as_ref().is_some_and(|s| { + s.crossfade_animation.is_some() + || s.claude_crossfade_animation.is_some() + || s.codex_crossfade_animation.is_some() + }); if !any_animating { unsafe { let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); @@ -154,6 +157,7 @@ pub(super) unsafe extern "system" fn wnd_proc( WM_APP_USAGE_UPDATED => { if let Some(state) = lock_state().as_mut() { state.antigravity_tile_rect = None; + state.claude_tile_rect = None; state.codex_tile_rect = None; } check_theme_change(); @@ -866,12 +870,124 @@ pub(super) unsafe extern "system" fn wnd_proc( || s.show_antigravity_external) && s.providers.contains(ProviderId::Antigravity); s.antigravity_tile_rect = None; + s.claude_tile_rect = None; s.codex_tile_rect = None; if !any_pool { s.antigravity_hovered = false; s.crossfade_opacity = 1.0; s.crossfade_animation = None; - if s.codex_crossfade_animation.is_none() { + if s.claude_crossfade_animation.is_none() + && s.codex_crossfade_animation.is_none() + { + unsafe { + let _ = kill_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ); + } + } + } + } + } + } + save_state_settings(); + if should_reposition { + position_at_taskbar(); + } + render_layered(); + sync_tray_icon(hwnd); + request_poll(hwnd); + } + IDM_PROVIDER_CLAUDE_MAIN | IDM_PROVIDER_CLAUDE_X => { + let mut should_reposition = false; + { + let mut state = lock_state(); + if let Some(s) = state.as_mut() { + let is_main = id == IDM_PROVIDER_CLAUDE_MAIN; + let target_id = if is_main { "default" } else { "claude_x" }; + if is_main + && !s.accounts.claude.profiles.iter().any(|p| p.id == "default") + { + s.accounts + .claude + .profiles + .push(crate::accounts::AccountProfile { + id: "default".into(), + name: "Default".into(), + config_dir: String::new(), + credentials_path: String::new(), + enabled: true, + }); + s.accounts.claude.used_ids.insert("default".into()); + } else if !is_main + && !s + .accounts + .claude + .profiles + .iter() + .any(|p| p.id == "claude_x") + { + s.accounts + .claude + .profiles + .push(crate::accounts::AccountProfile { + id: "claude_x".into(), + name: "Claude Code X".into(), + config_dir: "~/.claude_x".into(), + credentials_path: String::new(), + enabled: false, + }); + s.accounts.claude.used_ids.insert("claude_x".into()); + } + let mut main_enabled = s.providers.contains(ProviderId::Claude) + && s.accounts + .claude + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true); + let mut x_enabled = s.providers.contains(ProviderId::Claude) + && s.accounts + .claude + .profiles + .iter() + .find(|p| p.id == "claude_x") + .map(|p| p.enabled) + .unwrap_or(false); + let current = if is_main { main_enabled } else { x_enabled }; + let other = if is_main { x_enabled } else { main_enabled }; + let target = !current; + if !target + && !other + && s.providers.len() <= 1 + && s.providers.contains(ProviderId::Claude) + { + // Keep at least one provider active + } else { + if let Some(p) = s + .accounts + .claude + .profiles + .iter_mut() + .find(|p| p.id == target_id) + { + p.enabled = target; + } + if is_main { + main_enabled = target; + } else { + x_enabled = target; + } + let any_claude = main_enabled || x_enabled; + if !any_claude { + s.providers.set(ProviderId::Claude, false); + s.claude_hovered = false; + s.claude_crossfade_opacity = 0.0; + s.claude_crossfade_animation = None; + if s.crossfade_animation.is_none() + && s.codex_crossfade_animation.is_none() + { unsafe { let _ = kill_controller_timer( Some(hwnd), @@ -879,7 +995,12 @@ pub(super) unsafe extern "system" fn wnd_proc( ); } } + should_reposition = true; + } else if !s.providers.contains(ProviderId::Claude) { + s.providers.set(ProviderId::Claude, true); + should_reposition = true; } + s.claude_tile_rect = None; } } } @@ -898,23 +1019,33 @@ pub(super) unsafe extern "system" fn wnd_proc( if let Some(s) = state.as_mut() { let is_main = id == IDM_PROVIDER_CODEX_MAIN; let target_id = if is_main { "default" } else { "codex_x" }; - if is_main && !s.accounts.codex.profiles.iter().any(|p| p.id == "default") { - s.accounts.codex.profiles.push(crate::accounts::AccountProfile { - id: "default".into(), - name: "Default".into(), - config_dir: String::new(), - credentials_path: String::new(), - enabled: true, - }); + if is_main + && !s.accounts.codex.profiles.iter().any(|p| p.id == "default") + { + s.accounts + .codex + .profiles + .push(crate::accounts::AccountProfile { + id: "default".into(), + name: "Default".into(), + config_dir: String::new(), + credentials_path: String::new(), + enabled: true, + }); s.accounts.codex.used_ids.insert("default".into()); - } else if !is_main && !s.accounts.codex.profiles.iter().any(|p| p.id == "codex_x") { - s.accounts.codex.profiles.push(crate::accounts::AccountProfile { - id: "codex_x".into(), - name: "Codex X".into(), - config_dir: "~/.codex_x".into(), - credentials_path: String::new(), - enabled: false, - }); + } else if !is_main + && !s.accounts.codex.profiles.iter().any(|p| p.id == "codex_x") + { + s.accounts + .codex + .profiles + .push(crate::accounts::AccountProfile { + id: "codex_x".into(), + name: "Codex X".into(), + config_dir: "~/.codex_x".into(), + credentials_path: String::new(), + enabled: false, + }); s.accounts.codex.used_ids.insert("codex_x".into()); } let mut main_enabled = s.providers.contains(ProviderId::Codex) @@ -963,9 +1094,14 @@ pub(super) unsafe extern "system" fn wnd_proc( s.codex_hovered = false; s.codex_crossfade_opacity = 0.0; s.codex_crossfade_animation = None; - if s.crossfade_animation.is_none() { + if s.crossfade_animation.is_none() + && s.claude_crossfade_animation.is_none() + { unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + let _ = kill_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ); } } should_reposition = true; @@ -994,14 +1130,35 @@ pub(super) unsafe extern "system" fn wnd_proc( let changed = s.providers.toggle(provider); // Tile widths depend on the provider count. s.antigravity_tile_rect = None; + s.claude_tile_rect = None; s.codex_tile_rect = None; + if !s.providers.contains(ProviderId::Claude) { + s.claude_hovered = false; + s.claude_crossfade_opacity = 0.0; + s.claude_crossfade_animation = None; + if s.crossfade_animation.is_none() + && s.codex_crossfade_animation.is_none() + { + unsafe { + let _ = kill_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ); + } + } + } if !s.providers.contains(ProviderId::Codex) { s.codex_hovered = false; s.codex_crossfade_opacity = 0.0; s.codex_crossfade_animation = None; - if s.crossfade_animation.is_none() { + if s.crossfade_animation.is_none() + && s.claude_crossfade_animation.is_none() + { unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + let _ = kill_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ); } } } @@ -1014,9 +1171,13 @@ pub(super) unsafe extern "system" fn wnd_proc( s.crossfade_opacity = 1.0; s.crossfade_animation = None; s.antigravity_tile_rect = None; - if s.codex_crossfade_animation.is_none() { - let _ = - kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + if s.claude_crossfade_animation.is_none() + && s.codex_crossfade_animation.is_none() + { + let _ = kill_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ); } } } diff --git a/src/window/mouse.rs b/src/window/mouse.rs index fb936efb..86fae3b3 100644 --- a/src/window/mouse.rs +++ b/src/window/mouse.rs @@ -197,8 +197,30 @@ pub(super) fn dispatch_double_click(hwnd: HWND, surface_index: usize, object_id: /// Theme object whose hover reveals the Antigravity Claude/GPT pool. pub(super) const ANTIGRAVITY_HOVER_OBJECT: &str = "antigravity-provider"; +pub(super) const CLAUDE_HOVER_OBJECT: &str = "claude-provider"; pub(super) const CODEX_HOVER_OBJECT: &str = "codex-provider"; +fn claude_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { + let state = lock_state(); + let Some(state) = state.as_ref() else { + return false; + }; + if surface_index_for_window(state, hwnd) != Some(0) { + return false; + } + let Some(rect) = state.claude_tile_rect else { + return false; + }; + let Some(theme) = state.active_theme.as_ref() else { + return false; + }; + let scale = theme_surface_scale(theme, 0).max(0.01); + let (x, y) = mouse_client_point(lparam); + let lx = x / scale; + let ly = y / scale; + lx >= rect.0 && lx < rect.0 + rect.2 && ly >= rect.1 && ly < rect.1 + rect.3 +} + fn codex_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { let state = lock_state(); let Some(state) = state.as_ref() else { @@ -220,8 +242,6 @@ fn codex_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { lx >= rect.0 && lx < rect.0 + rect.2 && ly >= rect.1 && ly < rect.1 + rect.3 } - - fn antigravity_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { let state = lock_state(); let Some(state) = state.as_ref() else { @@ -248,8 +268,10 @@ pub(super) fn update_mouse_hover(hwnd: HWND, lparam: LPARAM) { TrackMouseEvent(tracking) }); set_antigravity_hover(hwnd, antigravity_hover_at(hwnd, lparam)); + super::set_claude_hover(hwnd, claude_hover_at(hwnd, lparam)); super::set_codex_hover(hwnd, codex_hover_at(hwnd, lparam)); super::tick_antigravity_crossfade(hwnd, true); + super::tick_claude_crossfade(hwnd, true); super::tick_codex_crossfade(hwnd, true); for _ in 0..4 { let target = mouse_target_at(hwnd, lparam); @@ -314,6 +336,7 @@ fn track_mouse_hover_with( pub(super) fn clear_mouse_hover(hwnd: HWND) { widget_tooltip::hide_for(hwnd); set_antigravity_hover(hwnd, false); + super::set_claude_hover(hwnd, false); super::set_codex_hover(hwnd, false); let previous = { let mut state = lock_state(); diff --git a/src/window/placement_regression_tests.rs b/src/window/placement_regression_tests.rs index b231c76b..b819a1b7 100644 --- a/src/window/placement_regression_tests.rs +++ b/src/window/placement_regression_tests.rs @@ -18,6 +18,10 @@ pub(super) fn state_for(theme: ThemeDocument, placement: PlacementOverride) -> A antigravity_hovered: false, crossfade_animation: None, antigravity_tile_rect: None, + claude_hovered: false, + claude_crossfade_opacity: 0.0, + claude_crossfade_animation: None, + claude_tile_rect: None, codex_hovered: false, codex_crossfade_opacity: 0.0, codex_crossfade_animation: None, diff --git a/src/window/window_context_menu.rs b/src/window/window_context_menu.rs index 36558260..06383a26 100644 --- a/src/window/window_context_menu.rs +++ b/src/window/window_context_menu.rs @@ -147,6 +147,28 @@ pub(super) fn context_menu_action_flags( ContextMenuAction::ToggleProvider { provider } => state.providers.contains(*provider), ContextMenuAction::ToggleAntigravityGemini => state.show_antigravity_gemini, ContextMenuAction::ToggleAntigravityExternal => state.show_antigravity_external, + ContextMenuAction::ToggleClaudeMain => { + state.providers.contains(ProviderId::Claude) + && state + .accounts + .claude + .profiles + .iter() + .find(|p| p.id == "default") + .map(|p| p.enabled) + .unwrap_or(true) + } + ContextMenuAction::ToggleClaudeX => { + state.providers.contains(ProviderId::Claude) + && state + .accounts + .claude + .profiles + .iter() + .find(|p| p.id == "claude_x") + .map(|p| p.enabled) + .unwrap_or(false) + } ContextMenuAction::ToggleCodexMain => { state.providers.contains(ProviderId::Codex) && state @@ -294,6 +316,8 @@ pub(super) fn execute_context_menu_action( } ContextMenuAction::ToggleAntigravityGemini => Some(IDM_PROVIDER_ANTIGRAVITY_GEMINI), ContextMenuAction::ToggleAntigravityExternal => Some(IDM_PROVIDER_ANTIGRAVITY_EXTERNAL), + ContextMenuAction::ToggleClaudeMain => Some(IDM_PROVIDER_CLAUDE_MAIN), + ContextMenuAction::ToggleClaudeX => Some(IDM_PROVIDER_CLAUDE_X), ContextMenuAction::ToggleCodexMain => Some(IDM_PROVIDER_CODEX_MAIN), ContextMenuAction::ToggleCodexX => Some(IDM_PROVIDER_CODEX_X), ContextMenuAction::ToggleStartup => Some(IDM_START_WITH_WINDOWS), From 52a0de0fecc0f0aa807ac4f6072289d89e82b9c2 Mon Sep 17 00:00:00 2001 From: fix2it Date: Thu, 29 Oct 2026 12:44:27 +0200 Subject: [PATCH 09/17] feat: asymmetric hover crossfade animation with fast fade-out and lead-in --- src/theme_engine.rs | 44 ++++++++++++++++++++++---- src/theme_engine/tests.rs | 65 +++++++++++++++++++++++++++++++++++---- src/window.rs | 8 ++--- 3 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/theme_engine.rs b/src/theme_engine.rs index b5f304be..1383f4cf 100644 --- a/src/theme_engine.rs +++ b/src/theme_engine.rs @@ -1551,6 +1551,32 @@ fn expected_used(resets_at: std::time::SystemTime, length: std::time::Duration) .then(|| (1.0 - left.min(length).as_secs_f64() / length.as_secs_f64()) * 100.0) } +/// Computes asymmetric crossfade opacities `(normal_opacity, hover_opacity)` +/// for hover transitions. +/// +/// Progress `p` runs from 0.0 (unhovered/normal state) to 1.0 (hovered state). +/// - The outgoing normal state fades out rapidly between 0.0 and 0.35: +/// `normal_opacity = (1.0 - p / 0.35).clamp(0.0, 1.0)`. +/// - The incoming hover state fades in with a slight lead starting at 0.25: +/// `hover_opacity = ((p - 0.25) / 0.75).clamp(0.0, 1.0)`. +/// +/// This eliminates double-exposure ghosting / overlapping images while avoiding +/// a jarring empty dip in brightness. +pub fn asymmetric_crossfade_opacities(progress_to_hover: f64) -> (f64, f64) { + let p = progress_to_hover.clamp(0.0, 1.0); + let normal_opacity = if p < 0.35 { + (1.0 - p / 0.35).clamp(0.0, 1.0) + } else { + 0.0 + }; + let hover_opacity = if p > 0.25 { + ((p - 0.25) / 0.75).clamp(0.0, 1.0) + } else { + 0.0 + }; + (normal_opacity, hover_opacity) +} + impl DataContext { pub fn from_usage(data: Option<&AppUsageData>, canvas: &Canvas) -> Self { Self::from_usage_with_runtime(data, canvas, ThemeRuntime::default()) @@ -1644,8 +1670,10 @@ impl DataContext { context.insert("antigravity.dual_mode", agy_dual_mode as u8 as f64); let crossfade = runtime.crossfade_opacity.clamp(0.0, 1.0); context.insert("antigravity.crossfade_opacity", crossfade); - context.insert("antigravity.normal.opacity", crossfade); - context.insert("antigravity.hover.opacity", 1.0 - crossfade); + let (agy_normal_opacity, agy_hover_opacity) = + asymmetric_crossfade_opacities(1.0 - crossfade); + context.insert("antigravity.normal.opacity", agy_normal_opacity); + context.insert("antigravity.hover.opacity", agy_hover_opacity); let gemini_opacity = if crossfade > 0.5 { (crossfade - 0.5) * 2.0 } else { @@ -1671,8 +1699,10 @@ impl DataContext { context.insert("claude.hovered", runtime.claude_hovered as u8 as f64); let claude_crossfade = runtime.claude_crossfade_opacity.clamp(0.0, 1.0); context.insert("claude.crossfade_opacity", claude_crossfade); - context.insert("claude.normal.opacity", 1.0 - claude_crossfade); - context.insert("claude.hover.opacity", claude_crossfade); + let (claude_normal_opacity, claude_hover_opacity) = + asymmetric_crossfade_opacities(claude_crossfade); + context.insert("claude.normal.opacity", claude_normal_opacity); + context.insert("claude.hover.opacity", claude_hover_opacity); context.insert( "claude.main.enabled", runtime.claude_main_enabled as u8 as f64, @@ -1685,8 +1715,10 @@ impl DataContext { context.insert("codex.hovered", runtime.codex_hovered as u8 as f64); let codex_crossfade = runtime.codex_crossfade_opacity.clamp(0.0, 1.0); context.insert("codex.crossfade_opacity", codex_crossfade); - context.insert("codex.normal.opacity", 1.0 - codex_crossfade); - context.insert("codex.hover.opacity", codex_crossfade); + let (codex_normal_opacity, codex_hover_opacity) = + asymmetric_crossfade_opacities(codex_crossfade); + context.insert("codex.normal.opacity", codex_normal_opacity); + context.insert("codex.hover.opacity", codex_hover_opacity); context.insert( "codex.main.enabled", runtime.codex_main_enabled as u8 as f64, diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index fb1bdf2b..b37ff2df 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -4051,8 +4051,9 @@ fn claude_dual_mode_and_crossfade_context_variables() { assert_eq!(ctx.get("claude.x.enabled"), Some(1.0)); assert_eq!(ctx.get("claude.dual_mode"), Some(1.0)); assert_eq!(ctx.get("claude.crossfade_opacity"), Some(0.75)); - assert_eq!(ctx.get("claude.normal.opacity"), Some(0.25)); - assert_eq!(ctx.get("claude.hover.opacity"), Some(0.75)); + assert_eq!(ctx.get("claude.normal.opacity"), Some(0.0)); + let hover_opacity = ctx.get("claude.hover.opacity").expect("hover opacity"); + assert!((hover_opacity - 2.0 / 3.0).abs() < 1e-6); } #[test] @@ -4061,8 +4062,9 @@ fn antigravity_normal_and_hover_opacity_context_variables() { let canvas = Canvas::default(); let ctx = DataContext::from_usage_with_runtime(None, &canvas, runtime); assert_eq!(ctx.get("antigravity.crossfade_opacity"), Some(0.3)); - assert_eq!(ctx.get("antigravity.normal.opacity"), Some(0.3)); - assert_eq!(ctx.get("antigravity.hover.opacity"), Some(0.7)); + assert_eq!(ctx.get("antigravity.normal.opacity"), Some(0.0)); + let hover_opacity = ctx.get("antigravity.hover.opacity").expect("hover opacity"); + assert!((hover_opacity - 0.6).abs() < 1e-6); } #[test] @@ -4075,6 +4077,57 @@ fn codex_dual_mode_and_crossfade_context_variables() { assert_eq!(ctx.get("codex.x.enabled"), Some(1.0)); assert_eq!(ctx.get("codex.dual_mode"), Some(1.0)); assert_eq!(ctx.get("codex.crossfade_opacity"), Some(0.75)); - assert_eq!(ctx.get("codex.normal.opacity"), Some(0.25)); - assert_eq!(ctx.get("codex.hover.opacity"), Some(0.75)); + assert_eq!(ctx.get("codex.normal.opacity"), Some(0.0)); + let hover_opacity = ctx.get("codex.hover.opacity").expect("hover opacity"); + assert!((hover_opacity - 2.0 / 3.0).abs() < 1e-6); +} + +#[test] +fn test_asymmetric_crossfade_opacities_boundaries_and_minimal_overlap() { + use crate::theme_engine::asymmetric_crossfade_opacities; + + // Normal steady state + let (normal, hover) = asymmetric_crossfade_opacities(0.0); + assert_eq!(normal, 1.0); + assert_eq!(hover, 0.0); + + // Halfway through normal fade out (0.175) + let (normal, hover) = asymmetric_crossfade_opacities(0.175); + assert!((normal - 0.5).abs() < 1e-6); + assert_eq!(hover, 0.0); + + // Lead-in threshold for hover (0.25) + let (normal, hover) = asymmetric_crossfade_opacities(0.25); + assert!((normal - (1.0 - 0.25 / 0.35)).abs() < 1e-6); + assert_eq!(hover, 0.0); + + // Handover midpoint (0.30) + let (normal, hover) = asymmetric_crossfade_opacities(0.30); + assert!((normal - (1.0 - 0.30 / 0.35)).abs() < 1e-6); + assert!((hover - (0.05 / 0.75)).abs() < 1e-6); + + // Normal completely faded (0.35) + let (normal, hover) = asymmetric_crossfade_opacities(0.35); + assert_eq!(normal, 0.0); + assert!((hover - (0.10 / 0.75)).abs() < 1e-6); + + // Hover steady state + let (normal, hover) = asymmetric_crossfade_opacities(1.0); + assert_eq!(normal, 0.0); + assert_eq!(hover, 1.0); + + // Defensive clamping + assert_eq!(asymmetric_crossfade_opacities(-0.5), (1.0, 0.0)); + assert_eq!(asymmetric_crossfade_opacities(1.5), (0.0, 1.0)); + + // Overlap verification: at no point during transition do both layers have high visibility (no double-exposure) + for step in 0..=100 { + let p = step as f64 / 100.0; + let (n, h) = asymmetric_crossfade_opacities(p); + let min_overlap = n.min(h); + assert!( + min_overlap < 0.15, + "Excessive overlap at p = {p}: normal = {n}, hover = {h}, min = {min_overlap}" + ); + } } diff --git a/src/window.rs b/src/window.rs index f563a384..4a22858b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3493,9 +3493,9 @@ fn check_language_change() { } } -const ANTIGRAVITY_CROSSFADE_DURATION_MS: u64 = 300; -const CODEX_CROSSFADE_DURATION_MS: u64 = 300; -const CLAUDE_CROSSFADE_DURATION_MS: u64 = 300; +const ANTIGRAVITY_CROSSFADE_DURATION_MS: u64 = 200; +const CODEX_CROSSFADE_DURATION_MS: u64 = 200; +const CLAUDE_CROSSFADE_DURATION_MS: u64 = 200; const ANTIGRAVITY_CROSSFADE_TICK_MS: u32 = 16; #[derive(Clone, Copy, Debug)] @@ -3722,7 +3722,7 @@ pub(super) fn tick_codex_crossfade(hwnd: HWND, throttle: bool) { } } -/// Updates the hover state when the cursor enters or leaves the Codex tile with a 300ms crossfade animation. +/// Updates the hover state when the cursor enters or leaves the Codex tile with a 200ms crossfade animation. pub(super) fn set_codex_hover(hwnd: HWND, hovered: bool) { let mut should_animate = false; { From 2598321411f75028418ddd332f339798805f8030 Mon Sep 17 00:00:00 2001 From: fix2it Date: Thu, 29 Oct 2026 15:24:08 +0200 Subject: [PATCH 10/17] refactor(animation): unify crossfade loop, state checks, and context bindings --- src/theme_engine.rs | 37 +++---- src/theme_engine/tests.rs | 5 + src/window.rs | 197 ++++++++++++++----------------------- src/window/message_loop.rs | 75 ++------------ 4 files changed, 101 insertions(+), 213 deletions(-) diff --git a/src/theme_engine.rs b/src/theme_engine.rs index 1383f4cf..214dca3b 100644 --- a/src/theme_engine.rs +++ b/src/theme_engine.rs @@ -1674,35 +1674,20 @@ impl DataContext { asymmetric_crossfade_opacities(1.0 - crossfade); context.insert("antigravity.normal.opacity", agy_normal_opacity); context.insert("antigravity.hover.opacity", agy_hover_opacity); - let gemini_opacity = if crossfade > 0.5 { - (crossfade - 0.5) * 2.0 - } else { - 0.0 - }; - let external_opacity = if crossfade < 0.5 { - (0.5 - crossfade) * 2.0 - } else { - 0.0 - }; // crossfade 1.0 shows the primary pool: Gemini in dual mode, otherwise // the single enabled pool; hover fades toward the other one. let (gemini_opacity, external_opacity) = match ( runtime.antigravity_gemini_enabled, runtime.antigravity_external_enabled, ) { - (false, true) => (external_opacity, gemini_opacity), + (false, true) => (agy_hover_opacity, agy_normal_opacity), (false, false) => (0.0, 0.0), - _ => (gemini_opacity, external_opacity), + _ => (agy_normal_opacity, agy_hover_opacity), }; context.insert("antigravity.gemini.opacity", gemini_opacity); context.insert("antigravity.external.opacity", external_opacity); context.insert("claude.hovered", runtime.claude_hovered as u8 as f64); - let claude_crossfade = runtime.claude_crossfade_opacity.clamp(0.0, 1.0); - context.insert("claude.crossfade_opacity", claude_crossfade); - let (claude_normal_opacity, claude_hover_opacity) = - asymmetric_crossfade_opacities(claude_crossfade); - context.insert("claude.normal.opacity", claude_normal_opacity); - context.insert("claude.hover.opacity", claude_hover_opacity); + context.insert_hover_crossfade("claude", runtime.claude_crossfade_opacity); context.insert( "claude.main.enabled", runtime.claude_main_enabled as u8 as f64, @@ -1713,12 +1698,7 @@ impl DataContext { (runtime.claude_main_enabled && runtime.claude_x_enabled) as u8 as f64, ); context.insert("codex.hovered", runtime.codex_hovered as u8 as f64); - let codex_crossfade = runtime.codex_crossfade_opacity.clamp(0.0, 1.0); - context.insert("codex.crossfade_opacity", codex_crossfade); - let (codex_normal_opacity, codex_hover_opacity) = - asymmetric_crossfade_opacities(codex_crossfade); - context.insert("codex.normal.opacity", codex_normal_opacity); - context.insert("codex.hover.opacity", codex_hover_opacity); + context.insert_hover_crossfade("codex", runtime.codex_crossfade_opacity); context.insert( "codex.main.enabled", runtime.codex_main_enabled as u8 as f64, @@ -1863,6 +1843,15 @@ impl DataContext { context } + pub fn insert_hover_crossfade(&mut self, prefix: &str, progress_to_hover: f64) -> (f64, f64) { + let p = progress_to_hover.clamp(0.0, 1.0); + self.insert(&format!("{prefix}.crossfade_opacity"), p); + let (normal, hover) = asymmetric_crossfade_opacities(p); + self.insert(&format!("{prefix}.normal.opacity"), normal); + self.insert(&format!("{prefix}.hover.opacity"), hover); + (normal, hover) + } + fn insert_provider( &mut self, name: &str, diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index b37ff2df..e18dcea7 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -4065,6 +4065,11 @@ fn antigravity_normal_and_hover_opacity_context_variables() { assert_eq!(ctx.get("antigravity.normal.opacity"), Some(0.0)); let hover_opacity = ctx.get("antigravity.hover.opacity").expect("hover opacity"); assert!((hover_opacity - 0.6).abs() < 1e-6); + assert_eq!(ctx.get("antigravity.gemini.opacity"), Some(0.0)); + let external_opacity = ctx + .get("antigravity.external.opacity") + .expect("external opacity"); + assert!((external_opacity - 0.6).abs() < 1e-6); } #[test] diff --git a/src/window.rs b/src/window.rs index 4a22858b..d1a81238 100644 --- a/src/window.rs +++ b/src/window.rs @@ -163,6 +163,22 @@ struct AppState { suppress_next_left_up: bool, } +impl AppState { + pub(super) fn is_any_crossfade_animating(&self) -> bool { + self.crossfade_animation.is_some() + || self.claude_crossfade_animation.is_some() + || self.codex_crossfade_animation.is_some() + } + + pub(super) fn stop_crossfade_timer_if_idle(&self, hwnd: HWND) { + if !self.is_any_crossfade_animating() { + unsafe { + let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); + } + } + } +} + #[derive(Clone, Debug)] struct PendingMouseClick { surface_index: usize, @@ -3534,47 +3550,64 @@ impl CrossfadeAnimation { (opacity, true) } } -} -pub(super) fn tick_antigravity_crossfade(hwnd: HWND, throttle: bool) { - let (should_render, still_animating) = { - let mut state = lock_state(); - let Some(state) = state.as_mut() else { - return; - }; - let Some(mut anim) = state.crossfade_animation else { - return; + pub(super) fn tick_slot( + slot: &mut Option, + opacity_slot: &mut f64, + throttle: bool, + ) -> (bool, bool) { + let Some(mut anim) = *slot else { + return (false, false); }; let now = std::time::Instant::now(); if throttle && now.duration_since(anim.last_rendered) < std::time::Duration::from_millis(15) { - return; + return (false, true); } let (opacity, more) = anim.current_opacity(); - state.crossfade_opacity = opacity; + *opacity_slot = opacity; anim.last_rendered = now; if more { - state.crossfade_animation = Some(anim); + *slot = Some(anim); } else { - state.crossfade_opacity = anim.target_opacity; - state.crossfade_animation = None; + *opacity_slot = anim.target_opacity; + *slot = None; } (true, more) + } +} + +pub(super) fn stop_crossfade_timer_if_idle(hwnd: HWND) { + if let Some(state) = lock_state().as_ref() { + state.stop_crossfade_timer_if_idle(hwnd); + } +} + +pub(super) fn ensure_crossfade_timer(hwnd: HWND) { + unsafe { + let _ = set_controller_timer( + Some(hwnd), + TIMER_CROSSFADE_SCHEDULE, + ANTIGRAVITY_CROSSFADE_TICK_MS, + None, + ); + } +} + +pub(super) fn tick_antigravity_crossfade(hwnd: HWND, throttle: bool) { + let (should_render, still_animating) = { + let mut state = lock_state(); + let Some(state) = state.as_mut() else { + return; + }; + CrossfadeAnimation::tick_slot( + &mut state.crossfade_animation, + &mut state.crossfade_opacity, + throttle, + ) }; if !still_animating { - let any_animating = { - let state = lock_state(); - state.as_ref().is_some_and(|s| { - s.crossfade_animation.is_some() - || s.codex_crossfade_animation.is_some() - || s.claude_crossfade_animation.is_some() - }) - }; - if !any_animating { - unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); - } - } + stop_crossfade_timer_if_idle(hwnd); } if should_render { render_layered(); @@ -3587,39 +3620,14 @@ pub(super) fn tick_claude_crossfade(hwnd: HWND, throttle: bool) { let Some(state) = state.as_mut() else { return; }; - let Some(mut anim) = state.claude_crossfade_animation else { - return; - }; - let now = std::time::Instant::now(); - if throttle && now.duration_since(anim.last_rendered) < std::time::Duration::from_millis(15) - { - return; - } - let (opacity, more) = anim.current_opacity(); - state.claude_crossfade_opacity = opacity; - anim.last_rendered = now; - if more { - state.claude_crossfade_animation = Some(anim); - } else { - state.claude_crossfade_opacity = anim.target_opacity; - state.claude_crossfade_animation = None; - } - (true, more) + CrossfadeAnimation::tick_slot( + &mut state.claude_crossfade_animation, + &mut state.claude_crossfade_opacity, + throttle, + ) }; if !still_animating { - let any_animating = { - let state = lock_state(); - state.as_ref().is_some_and(|s| { - s.crossfade_animation.is_some() - || s.codex_crossfade_animation.is_some() - || s.claude_crossfade_animation.is_some() - }) - }; - if !any_animating { - unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); - } - } + stop_crossfade_timer_if_idle(hwnd); } if should_render { render_layered(); @@ -3665,14 +3673,7 @@ pub(super) fn set_claude_hover(hwnd: HWND, hovered: bool) { } } if should_animate { - unsafe { - let _ = set_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ANTIGRAVITY_CROSSFADE_TICK_MS, - None, - ); - } + ensure_crossfade_timer(hwnd); render_layered(); } } @@ -3683,39 +3684,14 @@ pub(super) fn tick_codex_crossfade(hwnd: HWND, throttle: bool) { let Some(state) = state.as_mut() else { return; }; - let Some(mut anim) = state.codex_crossfade_animation else { - return; - }; - let now = std::time::Instant::now(); - if throttle && now.duration_since(anim.last_rendered) < std::time::Duration::from_millis(15) - { - return; - } - let (opacity, more) = anim.current_opacity(); - state.codex_crossfade_opacity = opacity; - anim.last_rendered = now; - if more { - state.codex_crossfade_animation = Some(anim); - } else { - state.codex_crossfade_opacity = anim.target_opacity; - state.codex_crossfade_animation = None; - } - (true, more) + CrossfadeAnimation::tick_slot( + &mut state.codex_crossfade_animation, + &mut state.codex_crossfade_opacity, + throttle, + ) }; if !still_animating { - let any_animating = { - let state = lock_state(); - state.as_ref().is_some_and(|s| { - s.crossfade_animation.is_some() - || s.codex_crossfade_animation.is_some() - || s.claude_crossfade_animation.is_some() - }) - }; - if !any_animating { - unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); - } - } + stop_crossfade_timer_if_idle(hwnd); } if should_render { render_layered(); @@ -3762,14 +3738,7 @@ pub(super) fn set_codex_hover(hwnd: HWND, hovered: bool) { } } if should_animate { - unsafe { - let _ = set_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ANTIGRAVITY_CROSSFADE_TICK_MS, - None, - ); - } + ensure_crossfade_timer(hwnd); render_layered(); } } @@ -3799,14 +3768,7 @@ pub(super) fn set_antigravity_hover(hwnd: HWND, hovered: bool) { } } if should_animate { - unsafe { - let _ = set_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ANTIGRAVITY_CROSSFADE_TICK_MS, - None, - ); - } + ensure_crossfade_timer(hwnd); render_layered(); } } @@ -3856,14 +3818,7 @@ fn reload_external_settings(hwnd: HWND) { state.claude_crossfade_opacity = 0.0; state.claude_crossfade_animation = None; } - let any_animating = state.crossfade_animation.is_some() - || state.codex_crossfade_animation.is_some() - || state.claude_crossfade_animation.is_some(); - if !any_animating { - unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); - } - } + state.stop_crossfade_timer_if_idle(hwnd); state.antigravity_tile_rect = None; state.claude_tile_rect = None; state.codex_tile_rect = None; diff --git a/src/window/message_loop.rs b/src/window/message_loop.rs index eac823db..cde91975 100644 --- a/src/window/message_loop.rs +++ b/src/window/message_loop.rs @@ -134,16 +134,7 @@ pub(super) unsafe extern "system" fn wnd_proc( tick_antigravity_crossfade(hwnd, false); tick_claude_crossfade(hwnd, false); tick_codex_crossfade(hwnd, false); - let any_animating = lock_state().as_ref().is_some_and(|s| { - s.crossfade_animation.is_some() - || s.claude_crossfade_animation.is_some() - || s.codex_crossfade_animation.is_some() - }); - if !any_animating { - unsafe { - let _ = kill_controller_timer(Some(hwnd), TIMER_CROSSFADE_SCHEDULE); - } - } + super::stop_crossfade_timer_if_idle(hwnd); } _ => {} } @@ -876,16 +867,7 @@ pub(super) unsafe extern "system" fn wnd_proc( s.antigravity_hovered = false; s.crossfade_opacity = 1.0; s.crossfade_animation = None; - if s.claude_crossfade_animation.is_none() - && s.codex_crossfade_animation.is_none() - { - unsafe { - let _ = kill_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ); - } - } + s.stop_crossfade_timer_if_idle(hwnd); } } } @@ -985,16 +967,7 @@ pub(super) unsafe extern "system" fn wnd_proc( s.claude_hovered = false; s.claude_crossfade_opacity = 0.0; s.claude_crossfade_animation = None; - if s.crossfade_animation.is_none() - && s.codex_crossfade_animation.is_none() - { - unsafe { - let _ = kill_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ); - } - } + s.stop_crossfade_timer_if_idle(hwnd); should_reposition = true; } else if !s.providers.contains(ProviderId::Claude) { s.providers.set(ProviderId::Claude, true); @@ -1094,16 +1067,7 @@ pub(super) unsafe extern "system" fn wnd_proc( s.codex_hovered = false; s.codex_crossfade_opacity = 0.0; s.codex_crossfade_animation = None; - if s.crossfade_animation.is_none() - && s.claude_crossfade_animation.is_none() - { - unsafe { - let _ = kill_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ); - } - } + s.stop_crossfade_timer_if_idle(hwnd); should_reposition = true; } else if !s.providers.contains(ProviderId::Codex) { s.providers.set(ProviderId::Codex, true); @@ -1136,31 +1100,13 @@ pub(super) unsafe extern "system" fn wnd_proc( s.claude_hovered = false; s.claude_crossfade_opacity = 0.0; s.claude_crossfade_animation = None; - if s.crossfade_animation.is_none() - && s.codex_crossfade_animation.is_none() - { - unsafe { - let _ = kill_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ); - } - } + s.stop_crossfade_timer_if_idle(hwnd); } if !s.providers.contains(ProviderId::Codex) { s.codex_hovered = false; s.codex_crossfade_opacity = 0.0; s.codex_crossfade_animation = None; - if s.crossfade_animation.is_none() - && s.claude_crossfade_animation.is_none() - { - unsafe { - let _ = kill_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ); - } - } + s.stop_crossfade_timer_if_idle(hwnd); } if provider == ProviderId::Antigravity && changed { let enabled = s.providers.contains(ProviderId::Antigravity); @@ -1171,14 +1117,7 @@ pub(super) unsafe extern "system" fn wnd_proc( s.crossfade_opacity = 1.0; s.crossfade_animation = None; s.antigravity_tile_rect = None; - if s.claude_crossfade_animation.is_none() - && s.codex_crossfade_animation.is_none() - { - let _ = kill_controller_timer( - Some(hwnd), - TIMER_CROSSFADE_SCHEDULE, - ); - } + s.stop_crossfade_timer_if_idle(hwnd); } } } From bd903f4d430ff516ff73903d097664457372caaf Mon Sep 17 00:00:00 2001 From: fix2it Date: Thu, 29 Oct 2026 15:43:31 +0200 Subject: [PATCH 11/17] refactor(hit-test): consolidate tile hover, profile checks, and bounds resolution --- src/accounts.rs | 8 ++ src/window.rs | 180 +++++++++++++++----------------------------- src/window/mouse.rs | 73 ++++++------------ 3 files changed, 92 insertions(+), 169 deletions(-) diff --git a/src/accounts.rs b/src/accounts.rs index 8c64833e..ca735f52 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -89,6 +89,14 @@ impl Default for ProviderAccounts { } impl ProviderAccounts { + pub fn is_profile_enabled(&self, id: &str, default_if_missing: bool) -> bool { + self.profiles + .iter() + .find(|profile| profile.id == id) + .map(|profile| profile.enabled) + .unwrap_or(default_if_missing) + } + pub fn selected(&self) -> Option<&AccountProfile> { self.profiles .iter() diff --git a/src/window.rs b/src/window.rs index d1a81238..6e69b501 100644 --- a/src/window.rs +++ b/src/window.rs @@ -177,6 +177,36 @@ impl AppState { } } } + + pub(super) fn is_account_profile_enabled( + &self, + provider: ProviderId, + id: &str, + default_if_missing: bool, + ) -> bool { + self.providers.contains(provider) + && match provider { + ProviderId::Claude => self + .accounts + .claude + .is_profile_enabled(id, default_if_missing), + ProviderId::Codex => self + .accounts + .codex + .is_profile_enabled(id, default_if_missing), + _ => false, + } + } + + pub(super) fn is_dual_mode_active(&self, provider: ProviderId) -> bool { + let (main_id, x_id) = match provider { + ProviderId::Claude => ("default", "claude_x"), + ProviderId::Codex => ("default", "codex_x"), + _ => return false, + }; + self.is_account_profile_enabled(provider, main_id, true) + && self.is_account_profile_enabled(provider, x_id, false) + } } #[derive(Clone, Debug)] @@ -838,46 +868,14 @@ fn theme_runtime_from_state(state: &AppState) -> ThemeRuntime { ) .with_claude_state( state.claude_hovered, - state.providers.contains(ProviderId::Claude) - && state - .accounts - .claude - .profiles - .iter() - .find(|p| p.id == "default") - .map(|p| p.enabled) - .unwrap_or(true), - state.providers.contains(ProviderId::Claude) - && state - .accounts - .claude - .profiles - .iter() - .find(|p| p.id == "claude_x") - .map(|p| p.enabled) - .unwrap_or(false), + state.is_account_profile_enabled(ProviderId::Claude, "default", true), + state.is_account_profile_enabled(ProviderId::Claude, "claude_x", false), state.claude_crossfade_opacity, ) .with_codex_state( state.codex_hovered, - state.providers.contains(ProviderId::Codex) - && state - .accounts - .codex - .profiles - .iter() - .find(|p| p.id == "default") - .map(|p| p.enabled) - .unwrap_or(true), - state.providers.contains(ProviderId::Codex) - && state - .accounts - .codex - .profiles - .iter() - .find(|p| p.id == "codex_x") - .map(|p| p.enabled) - .unwrap_or(false), + state.is_account_profile_enabled(ProviderId::Codex, "default", true), + state.is_account_profile_enabled(ProviderId::Codex, "codex_x", false), state.codex_crossfade_opacity, ) } @@ -2846,52 +2844,34 @@ fn render_layered() { let surface = &theme.surfaces[surface_index]; let surface_runtime = theme_runtime_for_surface(&theme, surface_index, runtime); if surface_index == 0 { - let needs_rect = lock_state() - .as_ref() - .map(|s| s.antigravity_tile_rect.is_none()) - .unwrap_or(false); - if needs_rect { - let tile_rect = theme_engine::object_bounds( - &theme, - 0, - usage_data.as_ref(), - surface_runtime, - mouse::ANTIGRAVITY_HOVER_OBJECT, - ); - if let Some(state) = lock_state().as_mut() { - state.antigravity_tile_rect = tile_rect; + let mut state = lock_state(); + if let Some(state) = state.as_mut() { + if state.antigravity_tile_rect.is_none() { + state.antigravity_tile_rect = theme_engine::object_bounds( + &theme, + 0, + usage_data.as_ref(), + surface_runtime, + mouse::ANTIGRAVITY_HOVER_OBJECT, + ); } - } - let needs_claude_rect = lock_state() - .as_ref() - .map(|s| s.claude_tile_rect.is_none()) - .unwrap_or(false); - if needs_claude_rect { - let tile_rect = theme_engine::object_bounds( - &theme, - 0, - usage_data.as_ref(), - surface_runtime, - mouse::CLAUDE_HOVER_OBJECT, - ); - if let Some(state) = lock_state().as_mut() { - state.claude_tile_rect = tile_rect; + if state.claude_tile_rect.is_none() { + state.claude_tile_rect = theme_engine::object_bounds( + &theme, + 0, + usage_data.as_ref(), + surface_runtime, + mouse::CLAUDE_HOVER_OBJECT, + ); } - } - let needs_codex_rect = lock_state() - .as_ref() - .map(|s| s.codex_tile_rect.is_none()) - .unwrap_or(false); - if needs_codex_rect { - let tile_rect = theme_engine::object_bounds( - &theme, - 0, - usage_data.as_ref(), - surface_runtime, - mouse::CODEX_HOVER_OBJECT, - ); - if let Some(state) = lock_state().as_mut() { - state.codex_tile_rect = tile_rect; + if state.codex_tile_rect.is_none() { + state.codex_tile_rect = theme_engine::object_bounds( + &theme, + 0, + usage_data.as_ref(), + surface_runtime, + mouse::CODEX_HOVER_OBJECT, + ); } } } @@ -3641,27 +3621,9 @@ pub(super) fn set_claude_hover(hwnd: HWND, hovered: bool) { let Some(state) = state.as_mut() else { return; }; - let dual_claude = state.providers.contains(ProviderId::Claude) - && state - .accounts - .claude - .profiles - .iter() - .find(|p| p.id == "default") - .map(|p| p.enabled) - .unwrap_or(true) - && state - .accounts - .claude - .profiles - .iter() - .find(|p| p.id == "claude_x") - .map(|p| p.enabled) - .unwrap_or(false); - if state.claude_hovered != hovered { state.claude_hovered = hovered; - if dual_claude { + if state.is_dual_mode_active(ProviderId::Claude) { let target = if hovered { 1.0 } else { 0.0 }; state.claude_crossfade_animation = Some(CrossfadeAnimation::new( state.claude_crossfade_opacity, @@ -3706,27 +3668,9 @@ pub(super) fn set_codex_hover(hwnd: HWND, hovered: bool) { let Some(state) = state.as_mut() else { return; }; - let dual_codex = state.providers.contains(ProviderId::Codex) - && state - .accounts - .codex - .profiles - .iter() - .find(|p| p.id == "default") - .map(|p| p.enabled) - .unwrap_or(true) - && state - .accounts - .codex - .profiles - .iter() - .find(|p| p.id == "codex_x") - .map(|p| p.enabled) - .unwrap_or(false); - if state.codex_hovered != hovered { state.codex_hovered = hovered; - if dual_codex { + if state.is_dual_mode_active(ProviderId::Codex) { let target = if hovered { 1.0 } else { 0.0 }; state.codex_crossfade_animation = Some(CrossfadeAnimation::new( state.codex_crossfade_opacity, diff --git a/src/window/mouse.rs b/src/window/mouse.rs index 86fae3b3..124a327f 100644 --- a/src/window/mouse.rs +++ b/src/window/mouse.rs @@ -200,57 +200,16 @@ pub(super) const ANTIGRAVITY_HOVER_OBJECT: &str = "antigravity-provider"; pub(super) const CLAUDE_HOVER_OBJECT: &str = "claude-provider"; pub(super) const CODEX_HOVER_OBJECT: &str = "codex-provider"; -fn claude_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { - let state = lock_state(); - let Some(state) = state.as_ref() else { - return false; - }; - if surface_index_for_window(state, hwnd) != Some(0) { - return false; - } - let Some(rect) = state.claude_tile_rect else { - return false; - }; - let Some(theme) = state.active_theme.as_ref() else { - return false; - }; - let scale = theme_surface_scale(theme, 0).max(0.01); - let (x, y) = mouse_client_point(lparam); - let lx = x / scale; - let ly = y / scale; - lx >= rect.0 && lx < rect.0 + rect.2 && ly >= rect.1 && ly < rect.1 + rect.3 -} - -fn codex_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { - let state = lock_state(); - let Some(state) = state.as_ref() else { - return false; - }; - if surface_index_for_window(state, hwnd) != Some(0) { - return false; - } - let Some(rect) = state.codex_tile_rect else { - return false; - }; - let Some(theme) = state.active_theme.as_ref() else { - return false; - }; - let scale = theme_surface_scale(theme, 0).max(0.01); - let (x, y) = mouse_client_point(lparam); - let lx = x / scale; - let ly = y / scale; - lx >= rect.0 && lx < rect.0 + rect.2 && ly >= rect.1 && ly < rect.1 + rect.3 -} - -fn antigravity_hover_at(hwnd: HWND, lparam: LPARAM) -> bool { - let state = lock_state(); - let Some(state) = state.as_ref() else { - return false; - }; +fn tile_hover_at( + state: &super::AppState, + hwnd: HWND, + lparam: LPARAM, + rect: Option<(f64, f64, f64, f64)>, +) -> bool { if surface_index_for_window(state, hwnd) != Some(0) { return false; } - let Some(rect) = state.antigravity_tile_rect else { + let Some(rect) = rect else { return false; }; let Some(theme) = state.active_theme.as_ref() else { @@ -267,9 +226,21 @@ pub(super) fn update_mouse_hover(hwnd: HWND, lparam: LPARAM) { track_mouse_hover_with(hwnd, widget_tooltip::visible_for(hwnd), |tracking| unsafe { TrackMouseEvent(tracking) }); - set_antigravity_hover(hwnd, antigravity_hover_at(hwnd, lparam)); - super::set_claude_hover(hwnd, claude_hover_at(hwnd, lparam)); - super::set_codex_hover(hwnd, codex_hover_at(hwnd, lparam)); + let (antigravity_hovered, claude_hovered, codex_hovered) = { + let state = lock_state(); + if let Some(state) = state.as_ref() { + ( + tile_hover_at(state, hwnd, lparam, state.antigravity_tile_rect), + tile_hover_at(state, hwnd, lparam, state.claude_tile_rect), + tile_hover_at(state, hwnd, lparam, state.codex_tile_rect), + ) + } else { + (false, false, false) + } + }; + set_antigravity_hover(hwnd, antigravity_hovered); + super::set_claude_hover(hwnd, claude_hovered); + super::set_codex_hover(hwnd, codex_hovered); super::tick_antigravity_crossfade(hwnd, true); super::tick_claude_crossfade(hwnd, true); super::tick_codex_crossfade(hwnd, true); From b1eca8f22214a5d510f1d8d7277dd135f6208767 Mon Sep 17 00:00:00 2001 From: fix2it Date: Sat, 7 Nov 2026 18:13:57 +0200 Subject: [PATCH 12/17] fix(opencode): add Authorization: Bearer header to Go status requests The OpenCode Console API requires an Authorization: Bearer header and returns HTTP 401 when only the Cookie header is present. - Extract the bare token from auth_cookie (stripping the 'auth=' prefix if present) and send it as Authorization: Bearer alongside the existing Cookie header. - Verified live against the OpenCode Console API: Bearer returns 200 OK with full quota data; Cookie-only returns 401 Unauthorized. --- src/poller/opencode.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/poller/opencode.rs b/src/poller/opencode.rs index 7848b53e..d15c3715 100644 --- a/src/poller/opencode.rs +++ b/src/poller/opencode.rs @@ -202,11 +202,28 @@ fn fetch_go_status( format!("auth={}", credentials.auth_cookie) }; + // Extract the bare token for the Bearer header. If the cookie string + // already carries an `auth=` prefix (full cookie header), strip it so + // the Bearer value is just the opaque token; otherwise the raw value is + // already the token. + let bearer_token = credentials + .auth_cookie + .split(';') + .find_map(|part| { + let part = part.trim_start(); + part.strip_prefix("auth=") + }) + .unwrap_or(&credentials.auth_cookie); + let mut response = match build_agent()? .get(url) .header("Accept", "application/json") .header("x-org-id", &credentials.workspace_id) + // Send both the legacy Cookie header and the modern Bearer token. + // The OpenCode Console API currently requires Authorization: Bearer + // and returns HTTP 401 when only the Cookie header is present. .header("Cookie", &cookie) + .header("Authorization", &format!("Bearer {bearer_token}")) .header("User-Agent", DASHBOARD_USER_AGENT) .call() .and_then(super::check_http_status) From c589f456254708da84af9aa5caca63c0bd50b024 Mon Sep 17 00:00:00 2001 From: fix2it Date: Sat, 7 Nov 2026 18:18:20 +0200 Subject: [PATCH 13/17] test(opencode): assert Authorization: Bearer value in mock-request test Extend console_request_sends_workspace_context_and_normalizes_auth_cookie to verify that the Authorization: Bearer header carries the correct bare token for all seven cookie-format inputs. Covers the gap identified in code review round 1 (F4-1). --- src/poller/opencode.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/poller/opencode.rs b/src/poller/opencode.rs index d15c3715..9bf1f2b4 100644 --- a/src/poller/opencode.rs +++ b/src/poller/opencode.rs @@ -568,6 +568,29 @@ mod tests { }) .unwrap(); assert_eq!(sent_cookie, expected); + // Verify that Authorization: Bearer carries the correct bare token. + // The bearer value is the auth= segment stripped of its prefix, or the + // full cookie string when no auth= segment is present (bare / session tokens). + let sent_bearer = request + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + name.eq_ignore_ascii_case("authorization") + .then_some(value.trim_start()) + }) + .unwrap_or(""); + let expected_bearer = cookie + .split(';') + .find_map(|part| { + let part = part.trim_start(); + part.strip_prefix("auth=") + }) + .unwrap_or(cookie); + assert_eq!( + sent_bearer, + format!("Bearer {expected_bearer}"), + "Authorization header mismatch for cookie={cookie:?}" + ); let request = request.to_ascii_lowercase(); assert!(request.starts_with("get /console/api/go/status http/1.1\r\n")); assert!(request.contains("\r\nx-org-id: wrk_example\r\n")); From 010996f4197566566afe4d3878d03c8d96dc2768 Mon Sep 17 00:00:00 2001 From: fix2it Date: Sat, 7 Nov 2026 18:22:03 +0200 Subject: [PATCH 14/17] fix(opencode): skip empty auth= value in Bearer token extraction When auth_cookie contains 'auth=; ...' (auth key present but blank value), strip_prefix now returns '' which is RFC 6750-invalid as a Bearer token. Add .filter(|s| !s.is_empty()) so an empty auth= segment is skipped and unwrap_or falls back to the full cookie string instead. Also align the test oracle with the same filter so the assertion for the 'auth=; __Host-console_session=SessionToken' input correctly expects the full raw string as the Bearer value rather than an empty token. --- src/poller/opencode.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/poller/opencode.rs b/src/poller/opencode.rs index 9bf1f2b4..07f6d69d 100644 --- a/src/poller/opencode.rs +++ b/src/poller/opencode.rs @@ -211,7 +211,9 @@ fn fetch_go_status( .split(';') .find_map(|part| { let part = part.trim_start(); - part.strip_prefix("auth=") + // Skip segments where auth= is present but empty; a blank Bearer + // token is RFC 6750-invalid and would be rejected by the API. + part.strip_prefix("auth=").filter(|s| !s.is_empty()) }) .unwrap_or(&credentials.auth_cookie); @@ -583,7 +585,7 @@ mod tests { .split(';') .find_map(|part| { let part = part.trim_start(); - part.strip_prefix("auth=") + part.strip_prefix("auth=").filter(|s| !s.is_empty()) }) .unwrap_or(cookie); assert_eq!( From 72127ce1c515b6775ca35f8c3a2012f89d7aef3c Mon Sep 17 00:00:00 2001 From: fix2it Date: Sat, 7 Nov 2026 18:30:04 +0200 Subject: [PATCH 15/17] fix(opencode): only send Authorization: Bearer when clean token is available - Extract bearer_token as Option<&str>: use stripped auth= value if present and non-empty, or fall back to the raw string only if it is a single-segment token without ';'. - Omit the Authorization header entirely when the credential is a multi-segment cookie string lacking an auth= segment, preventing malformed RFC 6750 tokens. - Update mock request test to assert that Authorization header is absent for multi-segment cookies without auth=. --- src/poller/opencode.rs | 73 ++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/poller/opencode.rs b/src/poller/opencode.rs index 07f6d69d..eecc76f1 100644 --- a/src/poller/opencode.rs +++ b/src/poller/opencode.rs @@ -202,31 +202,49 @@ fn fetch_go_status( format!("auth={}", credentials.auth_cookie) }; - // Extract the bare token for the Bearer header. If the cookie string - // already carries an `auth=` prefix (full cookie header), strip it so - // the Bearer value is just the opaque token; otherwise the raw value is - // already the token. - let bearer_token = credentials + // Extract a clean Bearer token from the credential. The token must be a + // single RFC 6750-compatible value (no ';' separators). + // + // - If auth_cookie contains an `auth=` segment, strip the prefix and + // use the value (empty values are skipped — they are RFC-invalid as Bearer). + // - Otherwise, if auth_cookie is a bare token with no ';' separator, use it + // directly (covers legacy opaque tokens stored without a name= prefix). + // - Multi-segment cookie strings without a valid auth= segment are not usable + // as Bearer tokens; in that case no Authorization header is sent. + let bearer_token: Option<&str> = credentials .auth_cookie .split(';') .find_map(|part| { let part = part.trim_start(); - // Skip segments where auth= is present but empty; a blank Bearer - // token is RFC 6750-invalid and would be rejected by the API. part.strip_prefix("auth=").filter(|s| !s.is_empty()) }) - .unwrap_or(&credentials.auth_cookie); + .or_else(|| { + // Single-segment raw value: treat as a bare token. Multi-segment + // strings (containing ';') cannot be a valid RFC 6750 Bearer token. + (!credentials.auth_cookie.contains(';')).then_some(credentials.auth_cookie.as_str()) + }); - let mut response = match build_agent()? + let request = build_agent()? .get(url) .header("Accept", "application/json") .header("x-org-id", &credentials.workspace_id) - // Send both the legacy Cookie header and the modern Bearer token. + // Send the legacy Cookie header on every request. // The OpenCode Console API currently requires Authorization: Bearer // and returns HTTP 401 when only the Cookie header is present. .header("Cookie", &cookie) - .header("Authorization", &format!("Bearer {bearer_token}")) - .header("User-Agent", DASHBOARD_USER_AGENT) + .header("User-Agent", DASHBOARD_USER_AGENT); + // Add Authorization: Bearer only when a clean, RFC 6750-compatible token + // is available. Omitting it for unrecognised credential shapes avoids + // sending a malformed header that could cause non-401 API errors. + let bearer_header; + let request = if let Some(token) = bearer_token { + bearer_header = format!("Bearer {token}"); + request.header("Authorization", &bearer_header) + } else { + request + }; + + let mut response = match request .call() .and_then(super::check_http_status) { @@ -571,28 +589,35 @@ mod tests { .unwrap(); assert_eq!(sent_cookie, expected); // Verify that Authorization: Bearer carries the correct bare token. - // The bearer value is the auth= segment stripped of its prefix, or the - // full cookie string when no auth= segment is present (bare / session tokens). - let sent_bearer = request + // The header is present only when a clean RFC 6750-compatible token is + // available: either an extracted auth= value or a single-segment bare string. + // Multi-segment cookie strings without auth= must NOT send a Bearer header. + let sent_authorization = request .lines() .find_map(|line| { let (name, value) = line.split_once(':')?; name.eq_ignore_ascii_case("authorization") .then_some(value.trim_start()) - }) - .unwrap_or(""); - let expected_bearer = cookie + }); + // Mirror the production extraction logic to derive the expected value. + let expected_bearer: Option<&str> = cookie .split(';') .find_map(|part| { let part = part.trim_start(); part.strip_prefix("auth=").filter(|s| !s.is_empty()) }) - .unwrap_or(cookie); - assert_eq!( - sent_bearer, - format!("Bearer {expected_bearer}"), - "Authorization header mismatch for cookie={cookie:?}" - ); + .or_else(|| (!cookie.contains(';')).then_some(cookie)); + match expected_bearer { + Some(token) => assert_eq!( + sent_authorization, + Some(format!("Bearer {token}").as_str()), + "Authorization header mismatch for cookie={cookie:?}" + ), + None => assert_eq!( + sent_authorization, None, + "Authorization header must be absent for multi-segment cookie without auth= (cookie={cookie:?})" + ), + } let request = request.to_ascii_lowercase(); assert!(request.starts_with("get /console/api/go/status http/1.1\r\n")); assert!(request.contains("\r\nx-org-id: wrk_example\r\n")); From 656fe084a8f9b79ce06e835ee169ff6d4160567c Mon Sep 17 00:00:00 2001 From: fix2it Date: Sat, 7 Nov 2026 18:42:30 +0200 Subject: [PATCH 16/17] fix(opencode): robust Bearer token extraction and independent test assertions - Move extract_bearer_token to dedicated module function. - Strictly distinguish OAuth / auth tokens from session cookie assignments: reject prefixes like '__Host-console_session=' and empty 'auth=' from being sent as invalid RFC 6750 Bearer headers. - Strip trailing/leading spaces inside cookie segments (trim). - Update console mock test table with explicit independent expected values (not mirrored logic). - Add dedicated unit tests for extract_bearer_token covering bare tokens, base64 padding, session cookies, and multi-segment variants. --- src/poller/opencode.rs | 140 ++++++++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 49 deletions(-) diff --git a/src/poller/opencode.rs b/src/poller/opencode.rs index eecc76f1..ae3d7c10 100644 --- a/src/poller/opencode.rs +++ b/src/poller/opencode.rs @@ -187,6 +187,32 @@ fn fetch_dashboard_usage(credentials: &DashboardCredentials) -> Result Option<&str> { + // Look for a non-empty `auth=` segment in a cookie string. + if let Some(token) = auth_cookie.split(';').find_map(|part| { + let part = part.trim(); + let value = part.strip_prefix("auth=")?.trim(); + (!value.is_empty()).then_some(value) + }) { + return Some(token); + } + + // If there is no `auth=` segment, check if this is a single bare token + // (e.g. legacy opaque tokens or OAuth access tokens stored without a cookie name prefix). + // It must not contain semicolons, must not start with known cookie assignment prefixes + // like "__Host-console_session=" or "auth=", and must not be empty. + let trimmed = auth_cookie.trim(); + if !trimmed.is_empty() + && !trimmed.contains(';') + && !trimmed.starts_with("__Host-console_session=") + && !trimmed.starts_with("auth=") + { + Some(trimmed) + } else { + None + } +} + fn fetch_go_status( credentials: &DashboardCredentials, url: &str, @@ -202,27 +228,8 @@ fn fetch_go_status( format!("auth={}", credentials.auth_cookie) }; - // Extract a clean Bearer token from the credential. The token must be a - // single RFC 6750-compatible value (no ';' separators). - // - // - If auth_cookie contains an `auth=` segment, strip the prefix and - // use the value (empty values are skipped — they are RFC-invalid as Bearer). - // - Otherwise, if auth_cookie is a bare token with no ';' separator, use it - // directly (covers legacy opaque tokens stored without a name= prefix). - // - Multi-segment cookie strings without a valid auth= segment are not usable - // as Bearer tokens; in that case no Authorization header is sent. - let bearer_token: Option<&str> = credentials - .auth_cookie - .split(';') - .find_map(|part| { - let part = part.trim_start(); - part.strip_prefix("auth=").filter(|s| !s.is_empty()) - }) - .or_else(|| { - // Single-segment raw value: treat as a bare token. Multi-segment - // strings (containing ';') cannot be a valid RFC 6750 Bearer token. - (!credentials.auth_cookie.contains(';')).then_some(credentials.auth_cookie.as_str()) - }); + + let bearer_token = extract_bearer_token(&credentials.auth_cookie); let request = build_agent()? .get(url) @@ -556,25 +563,41 @@ mod tests { #[test] fn console_request_sends_workspace_context_and_normalizes_auth_cookie() { - for (cookie, expected) in [ - ("test-token", "auth=test-token"), - ("test-token==", "auth=test-token=="), - ("auth=test-token; theme=dark", "auth=test-token; theme=dark"), + for (cookie, expected_cookie, expected_authorization) in [ + ( + "test-token", + "auth=test-token", + Some("Bearer test-token"), + ), + ( + "test-token==", + "auth=test-token==", + Some("Bearer test-token=="), + ), + ( + "auth=test-token; theme=dark", + "auth=test-token; theme=dark", + Some("Bearer test-token"), + ), ( "__Host-console_session=SessionToken==", "__Host-console_session=SessionToken==", + None, ), ( "__Host-console_session=SessionToken; __stripe_mid=StripeValue", "__Host-console_session=SessionToken; __stripe_mid=StripeValue", + None, ), ( "theme=dark; __Host-console_session=SessionToken", "theme=dark; __Host-console_session=SessionToken", + None, ), ( "auth=; __Host-console_session=SessionToken", "auth=; __Host-console_session=SessionToken", + None, ), ] { let (result, request) = mock_status_request(200, GO_STATUS_JSON, cookie); @@ -587,11 +610,9 @@ mod tests { .then_some(value.trim_start()) }) .unwrap(); - assert_eq!(sent_cookie, expected); - // Verify that Authorization: Bearer carries the correct bare token. - // The header is present only when a clean RFC 6750-compatible token is - // available: either an extracted auth= value or a single-segment bare string. - // Multi-segment cookie strings without auth= must NOT send a Bearer header. + assert_eq!(sent_cookie, expected_cookie); + // Verify that Authorization: Bearer carries the correct bare token + // and is strictly omitted when credentials provide only session cookies. let sent_authorization = request .lines() .find_map(|line| { @@ -599,25 +620,10 @@ mod tests { name.eq_ignore_ascii_case("authorization") .then_some(value.trim_start()) }); - // Mirror the production extraction logic to derive the expected value. - let expected_bearer: Option<&str> = cookie - .split(';') - .find_map(|part| { - let part = part.trim_start(); - part.strip_prefix("auth=").filter(|s| !s.is_empty()) - }) - .or_else(|| (!cookie.contains(';')).then_some(cookie)); - match expected_bearer { - Some(token) => assert_eq!( - sent_authorization, - Some(format!("Bearer {token}").as_str()), - "Authorization header mismatch for cookie={cookie:?}" - ), - None => assert_eq!( - sent_authorization, None, - "Authorization header must be absent for multi-segment cookie without auth= (cookie={cookie:?})" - ), - } + assert_eq!( + sent_authorization, expected_authorization, + "Authorization header mismatch for cookie={cookie:?}" + ); let request = request.to_ascii_lowercase(); assert!(request.starts_with("get /console/api/go/status http/1.1\r\n")); assert!(request.contains("\r\nx-org-id: wrk_example\r\n")); @@ -625,6 +631,42 @@ mod tests { } } + #[test] + fn bearer_token_extraction_distinguishes_tokens_from_session_cookies() { + assert_eq!(extract_bearer_token("test-token"), Some("test-token")); + assert_eq!(extract_bearer_token("test-token=="), Some("test-token==")); + assert_eq!( + extract_bearer_token("auth=test-token; theme=dark"), + Some("test-token") + ); + assert_eq!( + extract_bearer_token(" auth=token ; other=val"), + Some("token") + ); + assert_eq!( + extract_bearer_token("st_61dd5f7c-097c-41be-9e06-ba1356bb203f"), + Some("st_61dd5f7c-097c-41be-9e06-ba1356bb203f") + ); + assert_eq!( + extract_bearer_token("__Host-console_session=SessionToken=="), + None + ); + assert_eq!( + extract_bearer_token("__Host-console_session=SessionToken; __stripe_mid=Val"), + None + ); + assert_eq!( + extract_bearer_token("theme=dark; __Host-console_session=SessionToken"), + None + ); + assert_eq!( + extract_bearer_token("auth=; __Host-console_session=SessionToken"), + None + ); + assert_eq!(extract_bearer_token("auth="), None); + assert_eq!(extract_bearer_token(""), None); + } + #[test] fn console_request_distinguishes_auth_errors_from_bad_responses() { for status in [401, 403] { From 6edc4f6d09c8c5599d57ad147c172f67ef8e3ea8 Mon Sep 17 00:00:00 2001 From: fix2it Date: Sun, 8 Nov 2026 14:45:00 +0200 Subject: [PATCH 17/17] fix(i18n): translate missing UI strings and refresh advisory review date Translate "Lock in taskbar" in 12 locales and the Cursor token-expiry prompts in 10 locales; es.toml, pl.toml, th.toml and tr.toml already had translations. Fix German orthography in de.toml (Fuhren/Offnen/anschliessend). Refresh the RUSTSEC-2026-0192 exception review date to 2026-12-08 in deny.toml and docs/dependency-security.md. Checks: cargo test --locked (629 passed), cargo clippy --locked, ./tests/Test-Dependencies.ps1 (cargo audit + cargo deny), git diff --check. Independent loop-code-review round 1: 10/10, no in-scope findings at levels 1-4. --- deny.toml | 4 ++-- docs/dependency-security.md | 2 +- src/localization/locales/de.toml | 12 ++++++------ src/localization/locales/es.toml | 4 ++-- src/localization/locales/fr.toml | 6 +++--- src/localization/locales/ja.toml | 6 +++--- src/localization/locales/ko.toml | 6 +++--- src/localization/locales/nl.toml | 6 +++--- src/localization/locales/pl.toml | 2 +- src/localization/locales/pt-BR.toml | 6 +++--- src/localization/locales/ru.toml | 6 +++--- src/localization/locales/th.toml | 2 +- src/localization/locales/tr.toml | 2 +- src/localization/locales/zh-CN.toml | 6 +++--- src/localization/locales/zh-TW.toml | 6 +++--- 15 files changed, 38 insertions(+), 38 deletions(-) diff --git a/deny.toml b/deny.toml index 20a4dfcc..2d457eb1 100644 --- a/deny.toml +++ b/deny.toml @@ -8,12 +8,12 @@ yanked = "deny" unmaintained = "all" unsound = "all" unused-ignored-advisory = "deny" -# Owner: repository maintainers. Review by 2026-10-23, or on an oxifont update. +# Owner: repository maintainers. Review by 2026-12-08, or on an oxifont update. # Build-time only: oxifont processes font bytes bundled by locked dependencies. # No patched release exists; migrate/update the subsetter to remove this crate. # Keep cargo-audit's warning visible. This does not ignore future vulnerabilities. ignore = [ - { id = "RUSTSEC-2026-0192", reason = "ttf-parser 0.25.1 is unmaintained; required by oxifont-subset/oxifont-parser 0.2.2 for build-time bundled font subsetting. Owner: repository maintainers; review by 2026-10-23. See docs/dependency-security.md." }, + { id = "RUSTSEC-2026-0192", reason = "ttf-parser 0.25.1 is unmaintained; required by oxifont-subset/oxifont-parser 0.2.2 for build-time bundled font subsetting. Owner: repository maintainers; review by 2026-12-08. See docs/dependency-security.md." }, ] [licenses] diff --git a/docs/dependency-security.md b/docs/dependency-security.md index 834706cb..e32bcda0 100644 --- a/docs/dependency-security.md +++ b/docs/dependency-security.md @@ -35,7 +35,7 @@ it does not parse user-provided fonts at runtime through this dependency. `deny.toml` temporarily excepts only this advisory. `cargo audit` still displays its warning, and future vulnerability advisories remain blocking. The repository -maintainers own reviewing this exception by **2026-10-23**, or sooner when +maintainers own reviewing this exception by **2026-12-08**, or sooner when oxifont updates. Update or replace the subsetter to remove `ttf-parser`, then remove the exception; cargo-deny rejects unused advisory exceptions. The review date is a maintenance reminder, not an automatically enforced expiration. diff --git a/src/localization/locales/de.toml b/src/localization/locales/de.toml index bde4f66a..7e6738ef 100644 --- a/src/localization/locales/de.toml +++ b/src/localization/locales/de.toml @@ -48,13 +48,13 @@ second_suffix = "s" token_expired_title = "Authentifizierungsfehler" token_expired_body = "Führen Sie 'claude' in einem Terminal aus, verwenden Sie dann '/login' und folgen Sie den Anweisungen. Aktualisieren oder starten Sie diese App anschließend neu." codex_token_expired_title = "Codex-Authentifizierungsfehler" -codex_token_expired_body = "Fuhren Sie 'codex' in einem Terminal aus und folgen Sie den Anmeldeanweisungen. Aktualisieren oder starten Sie diese App anschliessend neu." +codex_token_expired_body = "Führen Sie 'codex' in einem Terminal aus und folgen Sie den Anmeldeanweisungen. Aktualisieren oder starten Sie diese App anschließend neu." antigravity_token_expired_title = "Antigravity-Authentifizierungsfehler" -antigravity_token_expired_body = "Offnen Sie Antigravity und melden Sie sich erneut an. Aktualisieren oder starten Sie diese App anschliessend neu." +antigravity_token_expired_body = "Öffnen Sie Antigravity und melden Sie sich erneut an. Aktualisieren oder starten Sie diese App anschließend neu." opencode_token_expired_title = "OpenCode nicht gefunden" -opencode_token_expired_body = "Legen Sie OPENCODE_GO_WORKSPACE_ID und OPENCODE_GO_AUTH_COOKIE für die OpenCode Go-Nutzungsüberwachung fest. Siehe README. Aktualisieren oder starten Sie diese App anschliessend neu." -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +opencode_token_expired_body = "Legen Sie OPENCODE_GO_WORKSPACE_ID und OPENCODE_GO_AUTH_COOKIE für die OpenCode Go-Nutzungsüberwachung fest. Siehe README. Aktualisieren oder starten Sie diese App anschließend neu." +cursor_token_expired_title = "Cursor-Authentifizierungsfehler" +cursor_token_expired_body = "Melden Sie sich bei Cursor an (oder setzen Sie CURSOR_SESSION_TOKEN). Aktualisieren oder starten Sie diese App anschließend neu." 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" @@ -461,6 +461,6 @@ copilot_window_title = "Copilot-Nutzungsmonitor" "X" = "X" "Y offset" = "Y-Versatz" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "In Taskleiste fixieren" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/es.toml b/src/localization/locales/es.toml index f89531c8..b0cb9345 100644 --- a/src/localization/locales/es.toml +++ b/src/localization/locales/es.toml @@ -53,8 +53,8 @@ antigravity_token_expired_title = "Error de autenticacion de Antigravity" antigravity_token_expired_body = "Abre Antigravity e inicia sesion otra vez. Despues, actualiza o reinicia esta aplicacion." opencode_token_expired_title = "OpenCode no encontrado" opencode_token_expired_body = "Define OPENCODE_GO_WORKSPACE_ID y OPENCODE_GO_AUTH_COOKIE para supervisar el uso de OpenCode Go. Consulta el README. Despues, actualiza o reinicia esta aplicacion." -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +cursor_token_expired_title = "Error de autenticación de Cursor" +cursor_token_expired_body = "Inicia sesión en Cursor (o define CURSOR_SESSION_TOKEN). Después, actualiza o reinicia esta aplicación." 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" diff --git a/src/localization/locales/fr.toml b/src/localization/locales/fr.toml index be7e6008..3b5446cc 100644 --- a/src/localization/locales/fr.toml +++ b/src/localization/locales/fr.toml @@ -53,8 +53,8 @@ antigravity_token_expired_title = "Erreur d'authentification Antigravity" antigravity_token_expired_body = "Ouvrez Antigravity et reconnectez-vous. Ensuite, actualisez ou redemarrez cette application." opencode_token_expired_title = "OpenCode introuvable" opencode_token_expired_body = "Definissez OPENCODE_GO_WORKSPACE_ID et OPENCODE_GO_AUTH_COOKIE pour suivre l'utilisation d'OpenCode Go. Consultez le README. Ensuite, actualisez ou redemarrez cette application." -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +cursor_token_expired_title = "Erreur d'authentification Cursor" +cursor_token_expired_body = "Connectez-vous à Cursor (ou définissez CURSOR_SESSION_TOKEN). Ensuite, actualisez ou redémarrez cette application." 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" @@ -461,6 +461,6 @@ copilot_window_title = "Moniteur d'utilisation Copilot" "X" = "X" "Y offset" = "Décalage Y" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "Verrouiller dans la barre des tâches" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/ja.toml b/src/localization/locales/ja.toml index f85c2fee..36ccdd64 100644 --- a/src/localization/locales/ja.toml +++ b/src/localization/locales/ja.toml @@ -55,8 +55,8 @@ antigravity_token_expired_title = "Antigravity 認証エラー" antigravity_token_expired_body = "Antigravity を開いて再度サインインしてください。その後、このアプリを更新するか再起動してください。" opencode_token_expired_title = "OpenCode が見つかりません" opencode_token_expired_body = "OpenCode Go の使用量を追跡するには、OPENCODE_GO_WORKSPACE_ID と OPENCODE_GO_AUTH_COOKIE を設定してください。README を参照し、その後このアプリを更新または再起動してください。" -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +cursor_token_expired_title = "Cursor 認証エラー" +cursor_token_expired_body = "Cursor にサインインするか CURSOR_SESSION_TOKEN を設定してください。その後、このアプリを更新または再起動してください。" grok_token_expired_title = "Grok 認証エラー" grok_token_expired_body = "ターミナルで 'grok login' を実行し、サインインの案内に従ってください。その後、このアプリを更新または再起動してください。" copilot_token_expired_title = "Copilot 認証エラー" @@ -463,6 +463,6 @@ copilot_window_title = "Copilot 使用量モニター" "X" = "X" "Y offset" = "Y オフセット" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "タスクバーに固定" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/ko.toml b/src/localization/locales/ko.toml index ab414b2f..db89830f 100644 --- a/src/localization/locales/ko.toml +++ b/src/localization/locales/ko.toml @@ -55,8 +55,8 @@ antigravity_token_expired_title = "Antigravity 인증 오류" antigravity_token_expired_body = "Antigravity를 열고 다시 로그인하세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요." opencode_token_expired_title = "OpenCode를 찾을 수 없음" opencode_token_expired_body = "OpenCode Go 사용량을 추적하려면 OPENCODE_GO_WORKSPACE_ID와 OPENCODE_GO_AUTH_COOKIE를 설정하세요. README를 참조한 다음 이 앱을 새로 고치거나 다시 시작하세요." -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +cursor_token_expired_title = "Cursor 인증 오류" +cursor_token_expired_body = "Cursor에 로그인하거나 CURSOR_SESSION_TOKEN을 설정하세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요." grok_token_expired_title = "Grok 인증 오류" grok_token_expired_body = "터미널에서 'grok login'을 실행하고 로그인 안내를 따르세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요." copilot_token_expired_title = "Copilot 인증 오류" @@ -463,6 +463,6 @@ copilot_window_title = "Copilot 사용량 모니터" "X" = "X" "Y offset" = "Y 오프셋" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "작업 표시줄에 고정" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/nl.toml b/src/localization/locales/nl.toml index 79dceb84..44f271e4 100644 --- a/src/localization/locales/nl.toml +++ b/src/localization/locales/nl.toml @@ -53,8 +53,8 @@ antigravity_token_expired_title = "Antigravity-authenticatiefout" antigravity_token_expired_body = "Open Antigravity en meld je opnieuw aan. Ververs of herstart de app daarna." opencode_token_expired_title = "OpenCode niet gevonden" opencode_token_expired_body = "Stel OPENCODE_GO_WORKSPACE_ID en OPENCODE_GO_AUTH_COOKIE in voor OpenCode Go-gebruiksregistratie. Zie de README. Ververs of herstart de app daarna." -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +cursor_token_expired_title = "Cursor-authenticatiefout" +cursor_token_expired_body = "Meld je aan bij Cursor (of stel CURSOR_SESSION_TOKEN in). Ververs of herstart de app daarna." 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" @@ -461,6 +461,6 @@ copilot_window_title = "Copilot-gebruiksmonitor" "X" = "X" "Y offset" = "Y-verschuiving" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "Vastzetten op taakbalk" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/pl.toml b/src/localization/locales/pl.toml index a7d24a3f..bb076460 100644 --- a/src/localization/locales/pl.toml +++ b/src/localization/locales/pl.toml @@ -461,6 +461,6 @@ copilot_window_title = "Monitor zużycia Copilot" "X" = "X" "Y offset" = "Przesunięcie Y" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "Przypnij do paska zadań" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/pt-BR.toml b/src/localization/locales/pt-BR.toml index cd2a2685..af630fc2 100644 --- a/src/localization/locales/pt-BR.toml +++ b/src/localization/locales/pt-BR.toml @@ -53,8 +53,8 @@ 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." opencode_token_expired_title = "OpenCode não encontrado" opencode_token_expired_body = "Defina OPENCODE_GO_WORKSPACE_ID e OPENCODE_GO_AUTH_COOKIE para acompanhar o uso do OpenCode Go. Veja o README. Depois disso, atualize ou reinicie este aplicativo." -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +cursor_token_expired_title = "Erro de Autenticação do Cursor" +cursor_token_expired_body = "Entre no Cursor (ou defina CURSOR_SESSION_TOKEN). Depois disso, atualize ou reinicie este aplicativo." 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" @@ -461,6 +461,6 @@ copilot_window_title = "Monitor de uso do Copilot" "X" = "X" "Y offset" = "Deslocamento Y" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "Fixar na barra de tarefas" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/ru.toml b/src/localization/locales/ru.toml index 554f6f9b..99da83a0 100644 --- a/src/localization/locales/ru.toml +++ b/src/localization/locales/ru.toml @@ -53,8 +53,8 @@ antigravity_token_expired_title = "Ошибка авторизации Antigravi antigravity_token_expired_body = "Откройте Antigravity и войдите снова. После этого обновите или перезапустите приложение." opencode_token_expired_title = "OpenCode не найден" opencode_token_expired_body = "Задайте OPENCODE_GO_WORKSPACE_ID и OPENCODE_GO_AUTH_COOKIE для отслеживания использования OpenCode Go. См. README. После этого обновите или перезапустите приложение." -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +cursor_token_expired_title = "Ошибка авторизации Cursor" +cursor_token_expired_body = "Войдите в Cursor (или задайте CURSOR_SESSION_TOKEN). После этого обновите или перезапустите приложение." grok_token_expired_title = "Ошибка авторизации Grok" grok_token_expired_body = "Запустите 'grok login' в терминале и следуйте инструкциям для входа. После этого обновите или перезапустите приложение." copilot_token_expired_title = "Ошибка авторизации Copilot" @@ -461,6 +461,6 @@ copilot_window_title = "Монитор использования Copilot" "X" = "X" "Y offset" = "Смещение Y" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "Закрепить на панели задач" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/th.toml b/src/localization/locales/th.toml index fdc7cf08..d12902f4 100644 --- a/src/localization/locales/th.toml +++ b/src/localization/locales/th.toml @@ -463,6 +463,6 @@ copilot_window_title = "ตัวติดตามการใช้งาน C "X" = "X" "Y offset" = "ระยะเยื้อง Y" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "ล็อกในแถบงาน" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/tr.toml b/src/localization/locales/tr.toml index 713d200a..98e7dfa6 100644 --- a/src/localization/locales/tr.toml +++ b/src/localization/locales/tr.toml @@ -461,6 +461,6 @@ copilot_window_title = "Copilot Kullanım İzleyicisi" "X" = "X" "Y offset" = "Y uzaklığı" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "Görev çubuğuna sabitle" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/zh-CN.toml b/src/localization/locales/zh-CN.toml index b23d2332..74031f3e 100644 --- a/src/localization/locales/zh-CN.toml +++ b/src/localization/locales/zh-CN.toml @@ -55,8 +55,8 @@ antigravity_token_expired_title = "Antigravity 验证错误" antigravity_token_expired_body = "请打开 Antigravity 并重新登录。完成后,请刷新或重新启动此应用程序。" opencode_token_expired_title = "OpenCode not found" opencode_token_expired_body = "Configure OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE for OpenCode Go usage tracking. See the README, then refresh or restart the app." -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +cursor_token_expired_title = "Cursor 验证错误" +cursor_token_expired_body = "请登录 Cursor(或设置 CURSOR_SESSION_TOKEN)。完成后,请刷新或重新启动此应用程序。" grok_token_expired_title = "Grok 验证错误" grok_token_expired_body = "请在终端中运行 'grok login',并按照登录提示操作。完成后,请刷新或重新启动此应用程序。" copilot_token_expired_title = "Copilot 验证错误" @@ -463,6 +463,6 @@ copilot_window_title = "Copilot 使用量监控" "X" = "X" "Y offset" = "Y 偏移" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "固定在任务栏" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked" diff --git a/src/localization/locales/zh-TW.toml b/src/localization/locales/zh-TW.toml index 17e92689..2c3b4e18 100644 --- a/src/localization/locales/zh-TW.toml +++ b/src/localization/locales/zh-TW.toml @@ -55,8 +55,8 @@ antigravity_token_expired_title = "Antigravity 驗證錯誤" antigravity_token_expired_body = "請開啟 Antigravity 並重新登入。完成後,請重新整理或重新啟動此應用程式。" opencode_token_expired_title = "找不到 OpenCode" opencode_token_expired_body = "請設定 OPENCODE_GO_WORKSPACE_ID 和 OPENCODE_GO_AUTH_COOKIE 以追蹤 OpenCode Go 使用量。請參閱 README,然後重新整理或重新啟動此應用程式。" -cursor_token_expired_title = "Cursor Auth Error" -cursor_token_expired_body = "Sign in to Cursor (or set CURSOR_SESSION_TOKEN), then refresh or restart this app." +cursor_token_expired_title = "Cursor 驗證錯誤" +cursor_token_expired_body = "請登入 Cursor(或設定 CURSOR_SESSION_TOKEN)。完成後,請重新整理或重新啟動此應用程式。" grok_token_expired_title = "Grok 驗證錯誤" grok_token_expired_body = "請在終端機中執行 'grok login',並依照登入提示操作。完成後,請重新整理或重新啟動此應用程式。" copilot_token_expired_title = "Copilot 驗證錯誤" @@ -463,6 +463,6 @@ copilot_window_title = "Copilot 使用量監控" "X" = "X" "Y offset" = "Y 位移" "Y" = "Y" -"Lock in taskbar" = "Lock in taskbar" +"Lock in taskbar" = "釘選到工作列" "Keep the widget docked; never float automatically" = "Keep the widget docked; never float automatically" "Locked" = "Locked"