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
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ pub struct SettingsSnapshot {
codex_custom_sessions_dirs: Vec<String>,
agent_sessions_enabled: bool,
agent_session_ssh_hosts: Vec<String>,
hooks_enabled: bool,
ui_language: &'static str,
theme: &'static str,
window_scale_percent: u16,
Expand Down Expand Up @@ -531,6 +532,7 @@ impl From<Settings> for SettingsSnapshot {
codex_custom_sessions_dirs: settings.codex_custom_sessions_dirs,
agent_sessions_enabled: settings.agent_sessions_enabled,
agent_session_ssh_hosts: settings.agent_session_ssh_hosts,
hooks_enabled: settings.hooks_enabled,
ui_language: language_label(settings.ui_language),
theme: theme_label(settings.theme),
window_scale_percent: settings.window_scale_percent,
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mod chart;
mod diagnostics;
mod tokens;
mod updater;
mod usage_spend;

mod agent_sessions;
mod bridge;
Expand Down Expand Up @@ -63,6 +64,7 @@ pub use chart::*;
pub use diagnostics::*;
pub use tokens::*;
pub use updater::*;
pub use usage_spend::*;

const PROVIDER_CACHE_STALE_AFTER: std::time::Duration = std::time::Duration::from_secs(30);
const MAX_API_KEY_LEN: usize = 16 * 1024;
Expand Down
43 changes: 43 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,13 @@ fn notify_usage_thresholds(
snapshot.primary.used_percent,
settings,
);
dispatch_quota_hooks(
settings,
provider,
&account,
"session",
snapshot.primary.used_percent,
);
}
if let Some(weekly) = &snapshot.secondary
&& !weekly.is_informational
Expand All @@ -628,6 +635,13 @@ fn notify_usage_thresholds(
weekly.used_percent,
settings,
);
dispatch_quota_hooks(
settings,
provider,
&account,
"weekly",
weekly.used_percent,
);
}
notify_predictive_pace(
&mut guard.notification_manager,
Expand All @@ -641,6 +655,35 @@ fn notify_usage_thresholds(
}
}

fn dispatch_quota_hooks(
settings: &Settings,
provider: ProviderId,
account: &str,
window: &str,
used_percent: f64,
) {
if !settings.hooks_enabled {
return;
}
let thresholds = settings.usage_thresholds(provider, window);
let account = if settings.hide_personal_info {
None
} else if account.is_empty() {
None
} else {
Some(account)
};
codexbar::core::emit_quota_threshold_hooks(
true,
provider.cli_name(),
window,
used_percent,
thresholds.high,
thresholds.critical,
account,
);
}

/// Stable account discriminator for threshold/session toast dedupe.
/// Prefer token-account id, then email, org, plan; empty for single-account lanes.
fn quota_notification_account_identity(
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub struct SettingsUpdate {
pub codex_custom_sessions_dirs: Option<Vec<String>>,
pub agent_sessions_enabled: Option<bool>,
pub agent_session_ssh_hosts: Option<Vec<String>>,
pub hooks_enabled: Option<bool>,
pub ui_language: Option<String>,
pub theme: Option<String>,
pub window_scale_percent: Option<u16>,
Expand Down Expand Up @@ -250,6 +251,9 @@ impl SettingsUpdate {
settings.agent_session_ssh_hosts =
codexbar::agent_sessions::RemoteSessionFetcher::sanitized_hosts(&v);
}
if let Some(v) = self.hooks_enabled {
settings.hooks_enabled = v;
}
if let Some(v) = self.install_updates_on_quit {
settings.install_updates_on_quit = v;
}
Expand Down
92 changes: 92 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! Usage & Spend settings tab: 7-day / 30-day local cost aggregates.

use codexbar::cost_scanner::CostScanner;
use serde::Serialize;
use tauri::State;

use super::ProviderUsageSnapshot;
use crate::state::AppState;
use std::sync::Mutex;

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UsageSpendRow {
pub provider_id: String,
pub display_name: String,
pub seven_day: Option<f64>,
pub thirty_day: Option<f64>,
pub currency: String,
pub source: String,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UsageSpendSummary {
pub rows: Vec<UsageSpendRow>,
}

#[tauri::command]
pub async fn get_usage_spend_summary(
state: State<'_, Mutex<AppState>>,
) -> Result<UsageSpendSummary, String> {
let cached = {
let guard = state.lock().map_err(|e| e.to_string())?;
guard.provider_cache.clone()
};

tauri::async_runtime::spawn_blocking(move || build_usage_spend_summary(&cached))
.await
.map_err(|e| format!("usage spend worker failed: {e}"))
}

fn build_usage_spend_summary(cached: &[ProviderUsageSnapshot]) -> UsageSpendSummary {
let mut rows = Vec::new();

// Local JSONL scanners for Codex / Claude (primary spend sources).
let codex_7 = CostScanner::new(7).scan_codex().total_cost_usd;
let codex_30 = CostScanner::new(30).scan_codex().total_cost_usd;
rows.push(UsageSpendRow {
provider_id: "codex".into(),
display_name: "Codex".into(),
seven_day: Some(codex_7),
thirty_day: Some(codex_30),
currency: "USD".into(),
source: "local logs".into(),
});

let claude_7 = CostScanner::new(7).scan_claude().total_cost_usd;
let claude_30 = CostScanner::new(30).scan_claude().total_cost_usd;
rows.push(UsageSpendRow {
provider_id: "claude".into(),
display_name: "Claude".into(),
seven_day: Some(claude_7),
thirty_day: Some(claude_30),
currency: "USD".into(),
source: "local logs".into(),
});

// Surface any other provider cost snapshots from the last refresh (period
// costs, not calendar 7d/30d — shown under thirty_day only).
for snapshot in cached {
if snapshot.provider_id == "codex" || snapshot.provider_id == "claude" {
continue;
}
let Some(cost) = &snapshot.cost else {
continue;
};
rows.push(UsageSpendRow {
provider_id: snapshot.provider_id.clone(),
display_name: if snapshot.display_name.is_empty() {
snapshot.provider_id.clone()
} else {
snapshot.display_name.clone()
},
seven_day: None,
thirty_day: Some(cost.used),
currency: cost.currency_code.clone(),
source: format!("period ({})", cost.period),
});
}

UsageSpendSummary { rows }
}
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ fn main() {
commands::get_app_info,
commands::get_provider_chart_data,
commands::get_provider_local_usage_summary,
commands::get_usage_spend_summary,
commands::reorder_providers,
commands::set_provider_cookie_source,
commands::get_provider_cookie_source,
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop-tauri/src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const ALL_LOCALE_KEYS = [
"TabMenu",
"TabApiKeys",
"TabCookies",
"TabUsageSpend",
"TabAdvanced",
"TabAbout",
"TabShortcuts",
Expand Down Expand Up @@ -281,6 +282,21 @@ export const ALL_LOCALE_KEYS = [
"SectionLocalIntegrations",
"PowerToysPipeLabel",
"PowerToysPipeHelper",
"HooksTitle",
"HooksCaption",
"HooksEnableLabel",
"HooksEnableHelper",
"HooksConfigPathHint",
"UsageSpendTitle",
"UsageSpendCaption",
"UsageSpendRefresh",
"UsageSpendLoading",
"UsageSpendEmpty",
"UsageSpendColProvider",
"UsageSpendCol7d",
"UsageSpendCol30d",
"UsageSpendColCurrency",
"UsageSpendColSource",
"AgentSessionsTitle",
"AgentSessionsEnableLabel",
"AgentSessionsEnableHelper",
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop-tauri/src/lib/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
AgentSessionDiscoveryResult,
SessionFocusResult,
TrayVisibilityStatusDto,
UsageSpendSummary,
} from "../types/bridge";

export function getBootstrapState(): Promise<BootstrapState> {
Expand Down Expand Up @@ -249,6 +250,10 @@ export function getProviderLocalUsageSummary(
return invoke<ProviderLocalUsageSummary | null>("get_provider_local_usage_summary", { providerId });
}

export function getUsageSpendSummary(): Promise<UsageSpendSummary> {
return invoke<UsageSpendSummary>("get_usage_spend_summary");
}

// ── Token account bridge ─────────────────────────────────────────────

export function getTokenAccountProviders(): Promise<TokenAccountSupportBridge[]> {
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop-tauri/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5236,6 +5236,28 @@ html:has(.menu-surface--tray) {
line-height: 1.4;
}

/* Usage & Spend table */
.usage-spend-table {
width: 100%;
border-collapse: collapse;
font-size: var(--font-body);
}
.usage-spend-table th,
.usage-spend-table td {
text-align: left;
padding: 6px 8px;
border-bottom: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.08));
}
.usage-spend-table th {
font-weight: 600;
color: var(--text-secondary, #a1a1a6);
font-size: var(--font-caption);
}
.usage-spend-table__source {
color: var(--text-muted);
font-size: var(--font-caption);
}

/* About update controls block */
.about-update-controls {
display: flex;
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop-tauri/src/surfaces/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import DisplayTab from "./settings/tabs/DisplayTab";
import AdvancedTab from "./settings/tabs/AdvancedTab";
import AboutTab from "./settings/tabs/AboutTab";
import ProvidersTab from "./settings/tabs/ProvidersTab";
import UsageSpendTab from "./settings/tabs/UsageSpendTab";

// ── tab types ────────────────────────────────────────────────────────

Expand Down Expand Up @@ -80,6 +81,12 @@ const TabIcons: Record<SettingsTab, ReactElement> = {
<rect x="9" y="9" width="5" height="5" rx="1" />
</Svg>
),
usageSpend: (
<Svg>
<path d="M2 12.5V4.5h12v8" />
<path d="M4.5 10V8M7.5 10V6.5M10.5 10V7.2M13 10V5.5" />
</Svg>
),
advanced: (
<Svg>
<path d="M2 4h8M2 8h5M2 12h10" />
Expand All @@ -103,6 +110,7 @@ export const TAB_META: { id: SettingsTab; labelKey: LocaleKey }[] = [
{ id: "notifications", labelKey: "TabNotifications" },
{ id: "menuBar", labelKey: "TabMenuBar" },
{ id: "menu", labelKey: "TabMenu" },
{ id: "usageSpend", labelKey: "TabUsageSpend" },
{ id: "advanced", labelKey: "TabAdvanced" },
{ id: "about", labelKey: "TabAbout" },
];
Expand Down Expand Up @@ -267,6 +275,9 @@ export default function Settings({ state, initialTab: propTab }: { state: Bootst
{activeTab === "menu" && (
<DisplayTab mode="menu" settings={settings} set={set} saving={saving} />
)}
{activeTab === "usageSpend" && (
<UsageSpendTab settings={settings} set={set} saving={saving} />
)}
{activeTab === "advanced" && (
<AdvancedTab settings={settings} set={set} saving={saving} />
)}
Expand Down
20 changes: 20 additions & 0 deletions apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,26 @@ export default function AdvancedTab({ settings, set, saving }: TabProps) {
</div>
</section>

{/* -- External hooks --------------------------------------- */}
<section className="settings-section">
<h3 className="settings-section__title">{t("HooksTitle")}</h3>
<p className="settings-section__caption">{t("HooksCaption")}</p>
<div className="settings-section__group">
<Field
label={t("HooksEnableLabel")}
description={t("HooksEnableHelper")}
leading
>
<Toggle
checked={settings.hooksEnabled ?? false}
disabled={saving}
onChange={(v) => set({ hooksEnabled: v })}
/>
</Field>
</div>
<p className="settings-section__hint">{t("HooksConfigPathHint")}</p>
</section>

{/* ── Keychain access ──────────────────────────────────────── */}
<section className="settings-section">
<h3 className="settings-section__title settings-section__title--bold">
Expand Down
Loading
Loading