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
18 changes: 17 additions & 1 deletion apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,16 @@ impl ProviderUsageSnapshot {
) -> Self {
let usage = &result.usage;

let primary_pace = codexbar::core::UsagePace::weekly(&usage.primary, None, 10080);
// A missing session is represented by an informational primary so the
// weekly lane keeps its canonical role. Use that weekly lane for the
// provider-level pace summary instead of returning no pace at all.
let primary_pace_window = if usage.primary.is_informational {
usage.secondary.as_ref()
} else {
Some(&usage.primary)
};
let primary_pace = primary_pace_window
.and_then(|window| codexbar::core::UsagePace::weekly(window, None, 10080));

let pace = primary_pace.as_ref().map(|p| PaceSnapshot {
stage: pace_stage_str(p.stage),
Expand Down Expand Up @@ -311,6 +320,13 @@ pub(crate) fn compact_tray_status_label(
window: &RateWindowSnapshot,
lang: codexbar::settings::Language,
) -> String {
if window.is_informational {
return window
.reset_description
.clone()
.unwrap_or_else(|| "Unavailable".to_string());
}

let pct = format!("{:.0}%", window.used_percent);
if let Some(reset) = compact_reset_description(window, lang) {
format!("{pct} • {reset}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,23 @@ describe("UsageSection", () => {
expect(await screen.findByText("Additional Budget")).toBeInTheDocument();
expect(screen.getByText("42%")).toBeInTheDocument();
});

it("marks an unavailable session without rendering a quota bar", async () => {
const detail = provider();
detail.session = {
...rateWindow(0),
isInformational: true,
resetDescription: "No active 5h session",
};

render(
<LocaleProvider>
<UsageSection provider={detail} resetTimeRelative={true} t={(key) => key} />
</LocaleProvider>,
);

const label = await screen.findByText("ProviderSessionLabel");
expect(label.parentElement).toHaveTextContent("No active 5h session");
expect(label.parentElement?.querySelector(".provider-usage-bar__track")).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function UsageBar({
}) {
const usedPct = Number.isFinite(rate.usedPercent) ? Math.max(0, rate.usedPercent) : 0;
const pct = Math.min(100, usedPct);
const isInformational = rate.isInformational === true;
const formattedReset = useFormattedResetTime(
rate.resetsAt,
rate.resetDescription,
Expand All @@ -112,21 +113,25 @@ function UsageBar({
className="provider-usage-bar__pct"
data-exhausted={rate.isExhausted || undefined}
>
{rate.isExhausted
{isInformational
? rate.resetDescription?.trim() || formattedReset || "—"
: rate.isExhausted
? usedPct > 100
? `${usedPct.toFixed(0)}%`
: t("DetailWindowExhausted")
: `${usedPct.toFixed(0)}%`}
</span>
</div>
<div className="provider-usage-bar__track">
<div
className="provider-usage-bar__fill"
style={{ width: `${pct}%` }}
data-exhausted={rate.isExhausted || undefined}
/>
</div>
{resetHint && (
{!isInformational && (
<div className="provider-usage-bar__track">
<div
className="provider-usage-bar__fill"
style={{ width: `${pct}%` }}
data-exhausted={rate.isExhausted || undefined}
/>
</div>
)}
{!isInformational && resetHint && (
<span className="provider-usage-bar__reset">{resetHint}</span>
)}
</div>
Expand Down
29 changes: 20 additions & 9 deletions rust/src/cli/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,12 @@ fn append_usage_window_lines(
}

fn append_window_line(lines: &mut Vec<String>, label: &str, window: &RateWindow, use_color: bool) {
if window.is_informational {
let description = window.reset_description.as_deref().unwrap_or("unavailable");
lines.push(format!(" {:<8} {}", format!("{}:", label), description));
return;
}

let bar = render_progress_bar(window.used_percent, 20, use_color);
let reset = window
.format_countdown()
Expand Down Expand Up @@ -547,15 +553,20 @@ fn append_model_specific_line(
pub fn render_brief_text(provider: ProviderId, result: &ProviderFetchResult) -> String {
let metadata = instantiate_provider(provider).metadata().clone();
let usage = &result.usage;
let reset = usage
.primary
.format_countdown()
.unwrap_or_else(|| "n/a".to_string());
let mut parts = vec![format!(
"{} {}",
metadata.session_label,
format_percent(usage.primary.used_percent)
)];
let mut parts = Vec::new();
let reset = if usage.primary.is_informational {
parts.push(format!("{} unavailable", metadata.session_label));
usage.secondary.as_ref().unwrap_or(&usage.primary)
} else {
parts.push(format!(
"{} {}",
metadata.session_label,
format_percent(usage.primary.used_percent)
));
&usage.primary
}
.format_countdown()
.unwrap_or_else(|| "n/a".to_string());
if let Some(secondary) = &usage.secondary {
parts.push(format!(
"{} {}",
Expand Down
28 changes: 28 additions & 0 deletions rust/src/core/rate_window.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Rate window model - represents a usage limit window (e.g., 5-hour session, 7-day weekly)

use super::session_equivalent_forecast::SESSION_WINDOW_MINUTES;
use chrono::{DateTime, Datelike, Utc};
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -47,6 +48,19 @@ impl RateWindow {
}
}

/// Informational placeholder for an absent 5-hour session lane.
///
/// Weekly-only plans (Codex, Claude web) occasionally report no active
/// session window. Informational primaries are omitted from quota math
/// and rendered as unavailable, so one canonical shape keeps providers
/// and downstream surfaces from diverging.
pub fn no_active_session() -> Self {
Self {
window_minutes: Some(SESSION_WINDOW_MINUTES),
..Self::informational("No active 5h session")
}
}

/// Create a rate window with full details
pub fn with_details(
used_percent: f64,
Expand Down Expand Up @@ -172,6 +186,20 @@ mod tests {
use super::*;
use chrono::TimeZone;

#[test]
fn no_active_session_is_informational_five_hour_placeholder() {
let window = RateWindow::no_active_session();

assert!(window.is_informational);
assert_eq!(window.window_minutes, Some(SESSION_WINDOW_MINUTES));
assert_eq!(
window.reset_description.as_deref(),
Some("No active 5h session")
);
assert_eq!(window.used_percent, 0.0);
assert_eq!(window.resets_at, None);
}

#[test]
fn test_remaining_percent() {
let window = RateWindow::new(75.0);
Expand Down
18 changes: 2 additions & 16 deletions rust/src/providers/claude/web_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ impl ClaudeWebApiFetcher {
.five_hour
.as_ref()
.map(|w| self.to_rate_window(w, Some(300))) // 5 hours = 300 minutes
.unwrap_or_else(synthetic_no_session_primary);
.unwrap_or_else(RateWindow::no_active_session);

// Prefer limits[] weekly_all over legacy seven_day (same as OAuth path).
let secondary = super::scoped_weekly::weekly_all_window(&usage.limits).or_else(|| {
Expand Down Expand Up @@ -646,20 +646,6 @@ impl ClaudeWebApiFetcher {
}
}

/// Synthetic 5h session placeholder for Claude web when `five_hour` is null.
///
/// A genuine idle session (object present, 0% used) is NOT marked informational.
fn synthetic_no_session_primary() -> RateWindow {
let mut window = RateWindow::with_details(
0.0,
Some(300),
None,
Some("No active 5h session".to_string()),
);
window.is_informational = true;
window
}

impl Default for ClaudeWebApiFetcher {
fn default() -> Self {
Self::new()
Expand Down Expand Up @@ -783,7 +769,7 @@ mod tests {

#[test]
fn null_five_hour_session_is_informational_placeholder() {
let placeholder = super::synthetic_no_session_primary();
let placeholder = crate::core::RateWindow::no_active_session();
assert!(placeholder.is_informational);
assert_eq!(placeholder.window_minutes, Some(300));
assert!((placeholder.used_percent - 0.0).abs() < f64::EPSILON);
Expand Down
Loading