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
6 changes: 5 additions & 1 deletion apps/desktop-tauri/scripts/proof-scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@
"size": [310, 1032],
"steps": [
{ "expectText": ["Vercel", "Personal budget", "Spend", "Limit", "Remaining", "$58.00"] },
{ "expectNoText": "Available balance" },
{ "expectNoText": ["Available balance", "in reserve", "Lasts until reset"] },
{ "screenshot": "flyout-vercel-budget", "clip": "panel" }
]
},
Expand All @@ -438,7 +438,11 @@
"size": [880, 1400],
"steps": [
{ "expectText": ["Vercel personal budget", "Access token", "Vercel team", "Team ID"] },
{ "expectText": ["Spend", "Limit", "Remaining", "$42.00", "$100.00", "$58.00"] },
{ "expectText": ["Credential Storage", "Access tokens"] },
{ "expectNoText": "Remove" },
{ "expectNoText": ["On-pace budget", "in reserve"] },
{ "expectNoText": "Cookie source" },
{ "screenshot": "settings-vercel-budget" }
]
},
Expand Down
12 changes: 10 additions & 2 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,12 @@ pub struct ProviderUsageSnapshot {
pub pace: Option<PaceSnapshot>,
/// Whether the fetch allows pace (upstream `allowsPace(dataConfidence:)`).
/// The tray scores its pace tint at presentation time, so it needs this
/// beside the windows. Tray-only; not sent to the frontend.
#[serde(default = "default_pace_authoritative", skip_serializing)]
/// beside the windows. Sent only when false, so the frontend can skip
/// the pace advice it derives from window timing (on-pace budgets).
#[serde(
default = "default_pace_authoritative",
skip_serializing_if = "is_pace_authoritative"
)]
pub pace_authoritative: bool,
/// The quota percentages are a local estimate (upstream
/// `dataConfidence == .estimated`); percent surfaces prefix them with `~`.
Expand Down Expand Up @@ -469,6 +473,10 @@ fn default_pace_authoritative() -> bool {
true
}

fn is_pace_authoritative(value: &bool) -> bool {
*value
}

fn default_error_state() -> codexbar::core::ProviderStateKind {
codexbar::core::ProviderStateKind::Unknown
}
Expand Down
32 changes: 32 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge/pace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,38 @@ mod tests {
assert!(!pace.reset_window);
}

/// A spend budget (Vercel's personal budget) opts out of pace in its
/// fetch result, so the card gets neither a pace marker nor a reserve.
#[test]
fn non_authoritative_spend_budget_has_no_pace_or_reserve() {
let metadata = instantiate_provider(ProviderId::Vercel).metadata().clone();
let mut budget = monthly(50.0, Some(40_320));
budget.reset_description = Some("$42.00 / $84.00".into());
budget.description_is_detail = true;
let result = ProviderFetchResult::new(UsageSnapshot::new(budget), "api")
.with_non_authoritative_pace();

let snap = ProviderUsageSnapshot::from_fetch_result_at(
ProviderId::Vercel,
&metadata,
&result,
None,
None,
february_now(),
);

assert_eq!(snap.primary.pace, None);
assert_eq!(snap.primary.reserve_percent, None);
assert!(!snap.primary.reserve_will_last_to_reset);
assert!(snap.pace.is_none());
// The frontend derives on-pace budgets from window timing, so it
// needs the flag as well.
let json = serde_json::to_value(&snap).unwrap();
assert_eq!(json["paceAuthoritative"], serde_json::Value::Bool(false));
let cached: ProviderUsageSnapshot = serde_json::from_value(json).unwrap();
assert!(!cached.pace_authoritative);
}

#[test]
fn exhausted_reset_window_has_no_pace() {
let usage = UsageSnapshot::new(monthly(100.0, None));
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ pub fn revoke_provider_credentials(
.collect();
forget_provider_accounts(&app, id, &removed_ids);

let mut settings = Settings::load();
if settings.management_api_token(id).is_some() {
settings.set_management_api_token(id, None);
settings.save().map_err(|e| e.to_string())?;
}

Ok(())
}

Expand All @@ -318,6 +324,9 @@ pub struct CredentialStorageStatusBridge {
pub manual_cookies: String,
pub api_keys: String,
pub token_accounts: String,
/// Provider access tokens kept in `settings.json` (Vercel personal budget,
/// OpenRouter management key).
pub access_tokens: String,
}

pub(crate) fn credential_file_status_label(status: SecureFileStatus) -> String {
Expand All @@ -334,6 +343,23 @@ fn optional_credential_status(path: Option<std::path::PathBuf>) -> String {
.unwrap_or_else(|| "unavailable".to_string())
}

/// Access tokens share `settings.json` with ordinary preferences, so the file
/// existing says nothing about them; report its protection only while a token
/// is actually stored.
pub(crate) fn access_token_status(
settings: &Settings,
settings_file_status: impl FnOnce() -> String,
) -> String {
let any_stored = ProviderId::all()
.iter()
.any(|id| settings.management_api_token(*id).is_some());
if any_stored {
settings_file_status()
} else {
"missing".to_string()
}
}

#[tauri::command]
pub fn get_credential_storage_status() -> CredentialStorageStatusBridge {
CredentialStorageStatusBridge {
Expand All @@ -342,5 +368,8 @@ pub fn get_credential_storage_status() -> CredentialStorageStatusBridge {
token_accounts: credential_file_status_label(secure_file::status(
&TokenAccountStore::default_path(),
)),
access_tokens: access_token_status(&Settings::load(), || {
optional_credential_status(Settings::settings_path())
}),
}
}
34 changes: 34 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,40 @@ fn credential_status_labels_do_not_include_error_details() {
);
}

#[test]
fn access_token_status_reports_settings_file_only_while_a_token_is_stored() {
let mut settings = codexbar::settings::Settings::default();
assert_eq!(
super::access_token_status(&settings, || unreachable!("no token stored")),
"missing"
);

settings.set_management_api_token(ProviderId::Vercel, Some("vercel-test-token".into()));
let status =
super::access_token_status(&settings, || "protected:windows-dpapi-user".to_string());
assert_eq!(status, "protected:windows-dpapi-user");
assert!(!status.contains("vercel-test-token"));

settings.set_management_api_token(ProviderId::Vercel, None);
settings.set_management_api_token(ProviderId::OpenRouter, Some("or-test-key".into()));
assert_eq!(
super::access_token_status(&settings, || "plaintext".to_string()),
"plaintext"
);
}

#[test]
fn credential_storage_status_serializes_access_tokens() {
let json = serde_json::to_value(super::CredentialStorageStatusBridge {
manual_cookies: "missing".into(),
api_keys: "missing".into(),
token_accounts: "missing".into(),
access_tokens: "protected:windows-dpapi-user".into(),
})
.expect("serialize");
assert_eq!(json["accessTokens"], "protected:windows-dpapi-user");
}

#[test]
fn command_inputs_reject_invalid_provider_ids_before_storage_writes() {
assert!(super::set_api_key("not-a-provider".into(), "sk-test".into(), None).is_err());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ fn cookie_options_empty_for_providers_without_picker() {
assert!(crate::commands::cookie_source_options_for("unknown", Language::English).is_empty());
}

#[test]
fn cookie_source_picker_is_offered_only_to_providers_with_a_cookie_domain() {
for provider in codexbar::core::ProviderId::all() {
let options =
crate::commands::cookie_source_options_for(provider.cli_name(), Language::English);
if !options.is_empty() {
assert!(
provider.cookie_domain().is_some(),
"{} offers a cookie source without a cookie domain",
provider.cli_name()
);
}
}
assert!(codexbar::core::ProviderId::Vercel.cookie_domain().is_none());
assert!(crate::commands::cookie_source_options_for("vercel", Language::English).is_empty());
assert!(codexbar::core::ProviderId::Claude.cookie_domain().is_some());
assert!(!crate::commands::cookie_source_options_for("claude", Language::English).is_empty());
}

#[test]
fn region_options_for_regional_provider() {
let opts = crate::commands::region_options_for("alibaba", Language::English);
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop-tauri/src/components/MenuCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,9 @@ export default function MenuCard({
const metricDisplay = {
resetTimeRelative,
showResetWhenExhausted,
showPace: showPace && providerAllowsPace(provider.providerId, provider.sourceLabel),
showPace:
showPace &&
providerAllowsPace(provider.providerId, provider.sourceLabel, provider.paceAuthoritative),
showAsUsed,
compactOverview,
monthlyLimitBlockNow,
Expand Down
44 changes: 22 additions & 22 deletions apps/desktop-tauri/src/components/ProviderDisplayRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,20 @@ export function groupProviderDisplayDetails(
}

/**
* One transient provider detail line: "{title}: {value} [secondary]"
* plus an optional clamped progress bar.
* One transient provider detail row for a `<dl>`: the title as `<dt>`, the
* value [secondary] and an optional clamped progress bar as `<dd>`.
*
* Used by the settings provider detail (`UsageSection`); the tray card renders
* the same data as key-value rows in `card/DetailSections`.
* Used by the settings provider detail (`UsageSection`) inside a
* `provider-detail-grid`; the tray card renders the same data as key-value
* rows in `card/DetailSections`.
*/
export function ProviderDisplayRow({
detail,
lineClassName,
secondaryClassName,
trackClassName,
fillClassName,
}: {
detail: ProviderDisplayDetail;
lineClassName: string;
secondaryClassName?: string;
trackClassName: string;
fillClassName: string;
Expand All @@ -64,9 +63,10 @@ export function ProviderDisplayRow({
: null;

return (
<div>
<div className={lineClassName}>
<span>{title}: {localizeProviderText(detail.value, t)}</span>
<div style={{ display: "contents" }}>
<dt>{title}</dt>
<dd>
{localizeProviderText(detail.value, t)}
{detail.secondaryValue && secondaryClassName && (
<>
{" "}
Expand All @@ -75,19 +75,19 @@ export function ProviderDisplayRow({
</span>
</>
)}
</div>
{progressPercent != null && (
<div
className={trackClassName}
role="progressbar"
aria-label={title}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={progressPercent}
>
<div className={fillClassName} style={{ width: `${progressPercent}%` }} />
</div>
)}
{progressPercent != null && (
<div
className={trackClassName}
role="progressbar"
aria-label={title}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={progressPercent}
>
<div className={fillClassName} style={{ width: `${progressPercent}%` }} />
</div>
)}
</dd>
</div>
);
}
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/i18n/keyGroups/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,7 @@ export const CORE_LOCALE_KEYS = [
"CredentialApiKeys",
"CredentialManualCookies",
"CredentialTokenAccounts",
"CredentialAccessTokens",
"CredentialProtectedPrefix",
"CredentialStatusNotCreated",
"CredentialStatusPlaintext",
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop-tauri/src/lib/providerPace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,11 @@ describe("providerAllowsPace", () => {
expect(providerAllowsPace("opencodego", "web")).toBe(true);
expect(providerAllowsPace("claude", "local estimate")).toBe(true);
expect(providerAllowsPace("opencodego", null)).toBe(true);
expect(providerAllowsPace("claude", "oauth", true)).toBe(true);
expect(providerAllowsPace("claude", "oauth", undefined)).toBe(true);
});

it("rejects snapshots the backend marks as not pace-authoritative", () => {
expect(providerAllowsPace("vercel", "api", false)).toBe(false);
});
});
8 changes: 7 additions & 1 deletion apps/desktop-tauri/src/lib/providerPace.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
/** Device-local OpenCode Go quota estimates do not establish account-wide pace. */
/**
* Whether a provider snapshot may show pace advice. The backend marks
* non-authoritative fetches with `paceAuthoritative: false`; device-local
* OpenCode Go quota estimates do not establish account-wide pace either.
*/
export function providerAllowsPace(
providerId: string,
sourceLabel: string | null | undefined,
paceAuthoritative?: boolean,
): boolean {
if (paceAuthoritative === false) return false;
return !(
providerId === "opencodego" &&
sourceLabel?.trim().toLowerCase() === "local estimate"
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src/proof/fixtures/bridgeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export function providerDetail(
namedExtraMetrics:
providerId === "cursor" ? [{ id: "cursor-grok-bot", label: "Grok Bot" }] : [],
extraRateWindows: snapshot?.extraRateWindows ?? [],
inventory: snapshot?.inventory ?? [],
displayDetails: snapshot?.displayDetails ?? [],
usageItems: [],
hiddenUsageItemIds: [],
cost: snapshot?.cost ?? null,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/proof/fixtures/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ function vercelSnapshot(now: Date): ProviderUsageSnapshot {
secondary: null,
accountOrganization: "Example Team",
sourceLabel: "api",
paceAuthoritative: false,
displayDetails: [
{
id: "vercel-budget-spend",
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop-tauri/src/proof/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ describe("proof router", () => {
expect(openrouter.email).toBeNull();
});

it("offers cookie-source options only to providers with a cookie domain", () => {
const router = routerFor("vercel", "settings");
const options = (providerId: string) =>
router.handle("get_provider_cookie_source_options", { providerId }) as unknown[];
expect(options("claude").length).toBeGreaterThan(0);
expect(options("vercel")).toEqual([]);
expect(
(router.handle("get_provider_detail", { providerId: "vercel" }) as ProviderDetail)
.cookieSource,
).toBeNull();
});

it("applies settings patches to later reads", () => {
const router = routerFor("default");
router.handle("update_settings", { patch: { theme: "light" } });
Expand Down
19 changes: 14 additions & 5 deletions apps/desktop-tauri/src/proof/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ export function createProofRouter(
manualCookies: "Windows Credential Manager",
apiKeys: "Windows Credential Manager",
tokenAccounts: "Windows Credential Manager",
accessTokens: "missing",
}),
get_provider_detail: (args) => {
const detail = providerDetail(current(), stringArg(args, "providerId"));
Expand All @@ -302,11 +303,19 @@ export function createProofRouter(
get_usage_spend_summary: () => usageSpendSummary(current(), now),
get_spend_contract: (args) =>
spendContract(stringArg(args, "providerId"), settings.spendDashboardPeriod ?? "rolling:30"),
get_provider_cookie_source_options: () => [
{ value: "auto", label: "Automatic" },
{ value: "manual", label: "Manual" },
{ value: "off", label: "Off" },
],
// The real bridge offers no cookie-source options for providers that
// never read browser cookies, which hides the picker.
get_provider_cookie_source_options: (args) => {
const providerId = stringArg(args, "providerId");
const entry = current().catalog.find((candidate) => candidate.id === providerId);
return entry?.cookieDomain
? [
{ value: "auto", label: "Automatic" },
{ value: "manual", label: "Manual" },
{ value: "off", label: "Off" },
]
: [];
},
get_provider_region_options: () => [],
get_provider_workspace_id: () => null,
get_provider_gateway_url: () => "",
Expand Down
Loading