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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ pub struct CostSnapshotBridge {
pub remaining: Option<f64>,
#[serde(default = "default_currency")]
pub currency_code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub currency_symbol: Option<String>,
#[serde(default = "default_cost_period")]
pub period: String,
#[serde(default)]
Expand All @@ -101,6 +103,17 @@ fn default_cost_period() -> String {
"month".to_string()
}

/// Format a cost amount using the snapshot's currency symbol when available,
/// otherwise falling back to the currency-code prefix. Used by tray surfaces
/// that render a spend amount without a rate-window percent (MonthlyPlan).
pub(crate) fn format_cost_amount(cost: &CostSnapshotBridge) -> String {
if let Some(ref symbol) = cost.currency_symbol {
format!("{}{:.2}", symbol, cost.used)
} else {
format!("{:.2} {}", cost.used, cost.currency_code)
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NamedRateWindowSnapshot {
Expand Down Expand Up @@ -337,6 +350,7 @@ impl ProviderUsageSnapshot {
limit: c.limit,
remaining: c.remaining(),
currency_code: c.currency_code.clone(),
currency_symbol: c.currency_symbol.clone(),
period: c.period.clone(),
resets_at: c.resets_at.map(|dt| dt.to_rfc3339()),
formatted_used: c.format_used(),
Expand Down Expand Up @@ -686,6 +700,8 @@ pub struct SettingsSnapshot {
claude_daily_routines_usage_visible: bool,
alibaba_token_plan_region: String,
weekly_progress_work_days: Option<u8>,
cost_summary_display_style: &'static str,
provider_accent_colors: std::collections::HashMap<String, String>,
}

#[tauri::command]
Expand Down Expand Up @@ -791,6 +807,19 @@ impl From<Settings> for SettingsSnapshot {
claude_daily_routines_usage_visible: settings.claude_daily_routines_usage_visible,
alibaba_token_plan_region: settings.alibaba_token_plan_region,
weekly_progress_work_days: settings.weekly_progress_work_days,
cost_summary_display_style: cost_summary_display_style_label(
settings.cost_summary_display_style,
),
provider_accent_colors: settings
.provider_configs
.iter()
.filter_map(|(id, config)| {
config
.accent_color
.as_ref()
.map(|color| (id.cli_name().to_string(), color.clone()))
})
.collect(),
}
}
}
Expand Down Expand Up @@ -837,6 +866,28 @@ fn theme_label(theme: ThemePreference) -> &'static str {
}
}

fn cost_summary_display_style_label(
style: codexbar::settings::CostSummaryDisplayStyle,
) -> &'static str {
match style {
codexbar::settings::CostSummaryDisplayStyle::Compact => "compact",
codexbar::settings::CostSummaryDisplayStyle::Detailed => "detailed",
codexbar::settings::CostSummaryDisplayStyle::Hidden => "hidden",
}
}

pub(crate) fn parse_cost_summary_display_style(
s: &str,
) -> Option<codexbar::settings::CostSummaryDisplayStyle> {
use codexbar::settings::CostSummaryDisplayStyle;
match s {
"compact" => Some(CostSummaryDisplayStyle::Compact),
"detailed" => Some(CostSummaryDisplayStyle::Detailed),
"hidden" => Some(CostSummaryDisplayStyle::Hidden),
_ => None,
}
}

pub(super) fn parse_theme(s: &str) -> Option<ThemePreference> {
match s {
"auto" => Some(ThemePreference::Auto),
Expand All @@ -855,6 +906,7 @@ fn metric_preference_label(pref: MetricPreference) -> &'static str {
MetricPreference::Tertiary => "tertiary",
MetricPreference::Credits => "credits",
MetricPreference::ExtraUsage => "extraUsage",
MetricPreference::MonthlyPlan => "monthlyPlan",
MetricPreference::Average => "average",
}
}
Expand All @@ -868,6 +920,7 @@ pub(super) fn parse_metric_preference(s: &str) -> Option<MetricPreference> {
"tertiary" => Some(MetricPreference::Tertiary),
"credits" => Some(MetricPreference::Credits),
"extraUsage" | "extrausage" => Some(MetricPreference::ExtraUsage),
"monthlyPlan" | "monthlyplan" => Some(MetricPreference::MonthlyPlan),
"average" => Some(MetricPreference::Average),
_ => None,
}
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use codexbar::core::{
instantiate_provider,
};
use codexbar::locale;
use codexbar::login::{self, LoginOutcome, LoginPhase};
use codexbar::providers::copilot::{CopilotApi, device_flow::CopilotDeviceFlow};
use codexbar::secure_file::{self, SecureFileStatus};
use codexbar::settings::{
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub struct SettingsUpdate {
pub claude_daily_routines_usage_visible: Option<bool>,
pub alibaba_token_plan_region: Option<String>,
pub weekly_progress_work_days: Option<u8>,
pub cost_summary_display_style: Option<String>,
}

impl SettingsUpdate {
Expand Down Expand Up @@ -326,6 +327,13 @@ impl SettingsUpdate {
if let Some(v) = self.weekly_progress_work_days {
settings.weekly_progress_work_days = if (2..=6).contains(&v) { Some(v) } else { None };
}
if let Some(v) = self
.cost_summary_display_style
.as_deref()
.and_then(crate::commands::bridge::parse_cost_summary_display_style)
{
settings.cost_summary_display_style = v;
}
self
}

Expand Down
43 changes: 39 additions & 4 deletions apps/desktop-tauri/src-tauri/src/commands/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,12 @@ pub async fn trigger_provider_login(
return run_copilot_device_login(&app).await;
}

// TODO(6b): replace fallthrough once LoginPhase events land. The login
// runners live in `codexbar::login` but are async-oriented and tightly
// coupled to the egui UI's phase callbacks. For the Tauri shell we
// currently surface the dashboard URL.
if id == ProviderId::Kiro {
return run_cli_provider_login(&app, &provider_id, "kiro", 120).await;
}

// For other providers, surface the dashboard URL as the login flow
// is not yet wired through the Tauri shell.
if let Some(url) = dashboard_url_for_provider(&provider_id) {
return open_url_in_browser(&url);
}
Expand All @@ -273,6 +275,39 @@ pub async fn trigger_provider_login(
))
}

/// Run a CLI-based provider login (e.g. Kiro) and emit phase events.
async fn run_cli_provider_login(
app: &tauri::AppHandle,
provider_id: &str,
display_name: &str,
timeout_secs: u64,
) -> Result<(), String> {
let app_handle = app.clone();
let provider_id_owned = provider_id.to_string();
let result = login::run_kiro_login(timeout_secs, move |phase| {
let phase_str = match phase {
LoginPhase::Idle => "idle",
LoginPhase::Requesting => "requesting",
LoginPhase::WaitingBrowser => "waiting-browser",
LoginPhase::Complete => "complete",
};
events::emit_login_phase(&app_handle, &provider_id_owned, phase_str, None);
})
.await;

match result.outcome {
LoginOutcome::Success => Ok(()),
LoginOutcome::MissingBinary => Err(format!(
"{display_name} CLI not found. Install it and ensure it is on your PATH."
)),
LoginOutcome::LaunchFailed(e) => Err(format!("Failed to launch {display_name} login: {e}")),
LoginOutcome::TimedOut => Err(format!("{display_name} login timed out")),
LoginOutcome::Failed { status } => Err(format!(
"{display_name} login failed with exit code {status}"
)),
}
}

async fn run_copilot_device_login(app: &tauri::AppHandle) -> Result<(), String> {
let flow = CopilotDeviceFlow::new();
let device = flow
Expand Down
20 changes: 20 additions & 0 deletions apps/desktop-tauri/src-tauri/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub const UPDATE_STATE_CHANGED: &str = "update-state-changed";
pub const LOCALE_CHANGED: &str = "locale-changed";
pub const SETTINGS_CHANGED: &str = "settings-changed";
pub const CODEX_ACCOUNTS_UPDATED: &str = "codex-accounts-updated";
pub const LOGIN_PHASE: &str = "login-phase";

// ── Payloads ─────────────────────────────────────────────────────────

Expand All @@ -40,6 +41,14 @@ pub struct RefreshStartedPayload {
pub provider_ids: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginPhasePayload {
pub provider_id: String,
pub phase: String,
pub auth_link: Option<String>,
}

// ── Emit helpers ─────────────────────────────────────────────────────

pub fn emit_surface_mode_changed(
Expand Down Expand Up @@ -101,3 +110,14 @@ pub fn emit_update_state_changed(app: &AppHandle, payload: &UpdateStatePayload)
pub fn emit_settings_changed(app: &AppHandle) {
let _ = app.emit(SETTINGS_CHANGED, ());
}

pub fn emit_login_phase(app: &AppHandle, provider_id: &str, phase: &str, auth_link: Option<&str>) {
let _ = app.emit(
LOGIN_PHASE,
LoginPhasePayload {
provider_id: provider_id.to_string(),
phase: phase.to_string(),
auth_link: auth_link.map(|s| s.to_string()),
},
);
}
25 changes: 23 additions & 2 deletions apps/desktop-tauri/src-tauri/src/tray_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
use std::sync::Mutex;

use crate::commands::ProviderCatalogEntry;
use codexbar::settings::{Settings, TrayIconMode};
#[cfg(test)]
use codexbar::{core::ProviderId, settings::MetricPreference};
use codexbar::core::ProviderId;
use codexbar::settings::MetricPreference;
use codexbar::settings::{Settings, TrayIconMode};
use tauri::image::Image;
use tauri::menu::{CheckMenuItemBuilder, IsMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
Expand Down Expand Up @@ -563,6 +564,25 @@ fn provider_status_label(
snapshot: &crate::commands::ProviderUsageSnapshot,
lang: codexbar::settings::Language,
) -> (String, String) {
// MonthlyPlan metric (PAYG spend, e.g. Mistral): show formatted cost.
let provider = codexbar::core::ProviderId::from_cli_name(&snapshot.provider_id);
let preference = provider
.map(|id| Settings::load().get_provider_metric(id))
.unwrap_or_default();
if preference == MetricPreference::MonthlyPlan
&& let Some(cost) = snapshot.cost.as_ref()
{
let amount = if !cost.formatted_used.is_empty() {
cost.formatted_used.clone()
} else {
crate::commands::format_cost_amount(cost)
};
return (
snapshot.provider_id.clone(),
format!("{} {}", snapshot.display_name, amount),
);
}

// F5 (upstream 0.48.0): for Codex, prefer the first non-informational lane so
// a monthly-only plan shows the monthly window with its reset countdown
// instead of the informational "No active 5h session" placeholder.
Expand Down Expand Up @@ -1041,6 +1061,7 @@ mod tests {
limit: Some(limit),
remaining: Some((limit - used).max(0.0)),
currency_code: "USD".to_string(),
currency_symbol: None,
period: "monthly".to_string(),
resets_at: None,
formatted_used: format!("${used:.2}"),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/usage_metric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ fn preferred_window(
extra_usage_window(snapshot).or_else(|| cost_window(snapshot))
}
MetricPreference::Average => average_window(snapshot),
MetricPreference::MonthlyPlan => cost_window(snapshot),
}
}

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ function settings(overrides: Partial<SettingsSnapshot> = {}): SettingsSnapshot {
claudeDailyRoutinesUsageVisible: true,
alibabaTokenPlanRegion: "cn",
weeklyProgressWorkDays: null,
costSummaryDisplayStyle: "compact",
providerAccentColors: {},
...overrides,
};
}
Expand Down
18 changes: 15 additions & 3 deletions apps/desktop-tauri/src/components/MenuCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { type CSSProperties, useCallback, useEffect, useState } from "react";
import type {
CostSummaryDisplayStyle,
ProviderChartData,
ProviderUsageSnapshot,
} from "../types/bridge";
Expand Down Expand Up @@ -47,15 +48,19 @@ export interface MenuCardDisplayOptions {
showResetWhenExhausted?: boolean;
showAsUsed?: boolean;
compactMetrics?: boolean;
costSummaryDisplayStyle?: CostSummaryDisplayStyle;
}

interface MenuCardProps {
provider: ProviderUsageSnapshot;
display: MenuCardDisplayOptions;
isRefreshing?: boolean;
/** Per-provider accent color override (hex); applied as CSS --provider-accent. */
accentColor?: string;
onLayoutChange?: () => void;
}


export function maskEmail(email: string): string {
const at = email.indexOf("@");
if (at <= 1) return "••••@••••";
Expand Down Expand Up @@ -109,6 +114,7 @@ export default function MenuCard({
provider,
display,
isRefreshing = false,
accentColor,
onLayoutChange,
}: MenuCardProps) {
const {
Expand All @@ -117,6 +123,7 @@ export default function MenuCard({
showResetWhenExhausted = false,
showAsUsed = false,
compactMetrics = false,
costSummaryDisplayStyle,
} = display;
const { t } = useLocale();
const [chartData, setChartData] = useState<ProviderChartData | null>(null);
Expand Down Expand Up @@ -206,7 +213,7 @@ export default function MenuCard({
}
const visibleMetrics = compactMetrics ? metrics.slice(0, 2) : metrics;

const presence = describeCard(provider, chartData, visibleMetrics);
const presence = describeCard(provider, chartData, visibleMetrics, costSummaryDisplayStyle);
const { hasDetails } = presence;
const cardClassName = [
"menu-card",
Expand All @@ -218,7 +225,11 @@ export default function MenuCard({
.join(" ");

return (
<article className={cardClassName} aria-busy={isRefreshing}>
<article
className={cardClassName}
aria-busy={isRefreshing}
style={accentColor ? ({ "--provider-accent": accentColor } as CSSProperties) : undefined}
>
<header className="menu-card__header">
<div className="menu-card__title-row">
<div className="menu-card__name-group">
Expand Down Expand Up @@ -254,6 +265,7 @@ export default function MenuCard({
resetTimeRelative,
showResetWhenExhausted,
showAsUsed,
costSummaryDisplayStyle,
}}
metrics={visibleMetrics}
chartData={chartData}
Expand Down
Loading
Loading