From 0d776135e9b49bbbb5419b91f6a2ad3c47a084d8 Mon Sep 17 00:00:00 2001 From: iMelki Date: Tue, 25 Aug 2026 21:50:24 +0300 Subject: [PATCH 1/5] Clarify provider availability states --- apps/desktop-tauri/src/floatbar/FloatBar.css | 8 ++ .../src/floatbar/FloatBar.test.tsx | 50 ++++++++++++ apps/desktop-tauri/src/floatbar/FloatBar.tsx | 23 +++++- apps/desktop-tauri/src/i18n/keys.ts | 6 ++ .../src/lib/providerState.test.ts | 28 +++++++ apps/desktop-tauri/src/lib/providerState.ts | 78 +++++++++++++++++++ .../settings/providers/ProviderDetailPane.tsx | 9 +-- .../sections/ProviderIssueNotice.test.tsx | 27 +++++++ .../sections/ProviderIssueNotice.tsx | 44 ++--------- .../sections/QuickActionsSection.tsx | 11 --- rust/src/locale.rs | 6 ++ rust/src/locale/en-US.ftl | 6 ++ rust/src/locale/es-MX.ftl | 6 ++ rust/src/locale/ja-JP.ftl | 6 ++ rust/src/locale/ko-KR.ftl | 6 ++ rust/src/locale/ru-RU.ftl | 6 ++ rust/src/locale/tr-TR.ftl | 6 ++ rust/src/locale/zh-CN.ftl | 6 ++ rust/src/locale/zh-TW.ftl | 6 ++ 19 files changed, 277 insertions(+), 61 deletions(-) create mode 100644 apps/desktop-tauri/src/lib/providerState.test.ts create mode 100644 apps/desktop-tauri/src/lib/providerState.ts create mode 100644 apps/desktop-tauri/src/surfaces/settings/providers/sections/ProviderIssueNotice.test.tsx diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.css b/apps/desktop-tauri/src/floatbar/FloatBar.css index 188957ac36..c809df443f 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.css +++ b/apps/desktop-tauri/src/floatbar/FloatBar.css @@ -103,6 +103,14 @@ body.floatbar-window #root { .floatbar__empty * { pointer-events: none; } + +.floatbar__cost-estimate { + color: rgba(24, 42, 54, 0.72); + font-size: calc(8px * var(--floatbar-scale, 1)); + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; +} .floatbar__provider-icon { display: inline-flex; align-items: center; diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx index 567ea0bff7..a18b8dcfdf 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx @@ -221,6 +221,12 @@ describe("FloatBar", () => { TrayResetsDueNow: "Resetting", PanelToday: "Today", PanelUsedSuffix: "used", + OverviewSpendEstimate: "Estimate", + ProviderIssueAuthRequired: "Sign-in required", + ProviderIssueSessionExpired: "Session expired", + ProviderIssueLegacyTelemetry: "Legacy telemetry unavailable", + ProviderIssueLocalRuntimeOffline: "Local runtime offline", + ProviderIssueUnknown: "Usage unavailable", FloatBarThirtyDayShort: "30d", FloatBarNoProviders: "No providers", FloatBarRemainingSuffix: "remaining", @@ -393,6 +399,31 @@ describe("FloatBar", () => { expect(tauriMocks.getProviderChartData).not.toHaveBeenCalled(); }); + it("marks displayed local cost as an estimate", async () => { + tauriMocks.getCachedProviders.mockResolvedValue([snapshot("codex", "Codex", 75)]); + tauriMocks.getSettingsSnapshot.mockResolvedValue(settings({ floatBarShowCost: true })); + tauriMocks.getProviderLocalUsageSummary.mockResolvedValue({ + todayCost: 1.25, + thirtyDayCost: 12.5, + thirtyDayTokens: 1000, + latestTokens: 200, + topModel: "gpt-5", + estimateNote: "Estimated from local logs", + tokenCostUpdatedAtMs: 1234, + }); + + const { container } = renderFloatBar(bootstrap({ floatBarShowCost: true })); + + await waitFor(() => { + expect(container.querySelector(".floatbar__cost-estimate")?.textContent).toBe( + "Estimate", + ); + }); + expect(container.querySelector(".floatbar__cost-pill")?.getAttribute("title")).toContain( + "(Estimate)", + ); + }); + it("does not scan local costs by default", async () => { tauriMocks.getCachedProviders.mockResolvedValue([ snapshot("codex", "Codex", 75), @@ -405,6 +436,25 @@ describe("FloatBar", () => { expect(tauriMocks.getCachedProviders).toHaveBeenCalled(); }); expect(tauriMocks.getProviderLocalUsageSummary).not.toHaveBeenCalled(); + expect(document.querySelector(".floatbar__cost-pill")).toBeNull(); + }); + + it("uses a safe state label instead of a raw provider error", async () => { + const raw = "legacy telemetry failed for https://private.example.test; cookie=super-secret"; + tauriMocks.getCachedProviders.mockResolvedValue([ + snapshot("gemini", "Gemini", 12, { error: raw }), + ]); + tauriMocks.getSettingsSnapshot.mockResolvedValue(settings({ enabledProviders: ["gemini"] })); + + const { container } = renderFloatBar(bootstrap({ enabledProviders: ["gemini"] })); + + await waitFor(() => { + const pill = container.querySelector(".floatbar__pill"); + expect(pill?.textContent).toContain("Legacy telemetry unavailable"); + expect(pill?.getAttribute("title")).toBe("Gemini: Legacy telemetry unavailable"); + expect(pill?.textContent).not.toContain("super-secret"); + expect(pill?.getAttribute("title")).not.toContain("private.example.test"); + }); }); it("can show remaining percentages when configured", async () => { diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.tsx index 86df5a307b..881e031f8a 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.tsx @@ -19,6 +19,7 @@ import { } from "../lib/tauri"; import { ProviderIcon } from "../components/providers/ProviderIcon"; import { getProviderIcon } from "../components/providers/providerIcons"; +import { describeProviderState } from "../lib/providerState"; import type { BootstrapState, ProviderLocalUsageSummary, @@ -98,11 +99,13 @@ function CostPill({ scale, todayLabel, thirtyDayLabel, + estimateLabel, }: { summary: FloatBarCostSummary; scale: number; todayLabel: string; thirtyDayLabel: string; + estimateLabel: string; }) { const today = formatUsd(summary.todayCost); const thirtyDay = formatUsd(summary.thirtyDayCost); @@ -118,7 +121,7 @@ function CostPill({ return (
@@ -147,6 +150,9 @@ function CostPill({ )} + + {estimateLabel} +
); } @@ -167,6 +173,7 @@ function ProviderPill({ resetRelative, usedSuffix, remainingSuffix, + stateLabel, }: { provider: ProviderUsageSnapshot; highRemaining: number; @@ -177,19 +184,21 @@ function ProviderPill({ resetRelative: boolean; usedSuffix: string; remainingSuffix: string; + stateLabel: string; }) { const rateWindow = provider.selectedMetric; const remaining = Math.max(0, Math.min(100, rateWindow.remainingPercent)); const used = Math.max(0, Math.min(100, rateWindow.usedPercent)); const displayPercent = showAsUsed ? used : remaining; const displaySuffix = showAsUsed ? usedSuffix : remainingSuffix; - const exhausted = rateWindow.isExhausted || provider.error; + const state = describeProviderState(provider.error); + const exhausted = rateWindow.isExhausted || state.isProblem; let tone: "ok" | "warn" | "crit" = "ok"; if (exhausted || remaining <= critRemaining) tone = "crit"; else if (remaining <= highRemaining) tone = "warn"; const brand = getProviderIcon(provider.providerId).brandColor; - const label = provider.error ? "—" : `${Math.round(displayPercent)}%`; + const label = state.isProblem ? stateLabel : `${Math.round(displayPercent)}%`; const resetText = useFormattedResetTime( rateWindow.resetsAt, rateWindow.resetDescription, @@ -203,7 +212,11 @@ function ProviderPill({ return (
@@ -478,6 +491,7 @@ export default function FloatBar({ state }: { state: BootstrapState }) { resetRelative={settings.resetTimeRelative} usedSuffix={t("PanelUsedSuffix")} remainingSuffix={t("FloatBarRemainingSuffix")} + stateLabel={t(describeProviderState(p.error).labelKey)} /> ))} {visibleCosts.map((summary) => ( @@ -487,6 +501,7 @@ export default function FloatBar({ state }: { state: BootstrapState }) { scale={scale} todayLabel={t("PanelToday")} thirtyDayLabel={t("FloatBarThirtyDayShort")} + estimateLabel={t("OverviewSpendEstimate")} /> ))} diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index fc5dcb8a22..387f4dcf64 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -641,6 +641,12 @@ export const ALL_LOCALE_KEYS = [ "ProviderIssueFetchNeedsAttention", "ProviderIssueCopy", "ProviderIssueUnsupportedSourceModePrefix", + "ProviderIssueAuthRequired", + "ProviderIssueSessionExpired", + "ProviderIssueLegacyTelemetry", + "ProviderIssueLocalRuntimeOffline", + "ProviderIssueUnknown", + "ProviderIssuePrivacySafeDetail", "CredentialStorageTitle", "CredentialRevokeStored", "CredentialApiKeys", diff --git a/apps/desktop-tauri/src/lib/providerState.test.ts b/apps/desktop-tauri/src/lib/providerState.test.ts new file mode 100644 index 0000000000..840ba0f7e7 --- /dev/null +++ b/apps/desktop-tauri/src/lib/providerState.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { describeProviderState } from "./providerState"; + +describe("describeProviderState", () => { + it.each([ + [null, "ready", "ProviderStatusOk"], + ["authentication required", "needs-authentication", "ProviderIssueAuthRequired"], + ["cookie session expired for https://private.example.test", "expired-session", "ProviderIssueSessionExpired"], + ["legacy Gemini OAuth telemetry returned 403", "legacy-telemetry", "ProviderIssueLegacyTelemetry"], + ["Antigravity local runtime is offline", "local-runtime-offline", "ProviderIssueLocalRuntimeOffline"], + ["unexpected provider response", "unknown", "ProviderIssueUnknown"], + ] as const)("maps %s to a safe %s descriptor", (error, kind, labelKey) => { + expect(describeProviderState(error)).toEqual({ + kind, + isProblem: kind !== "ready", + labelKey, + }); + }); + + it("never returns raw error, cookie, or endpoint content", () => { + const raw = + "cookie=super-secret; request failed at https://private.example.test/v1"; + const descriptor = describeProviderState(raw); + expect(JSON.stringify(descriptor)).not.toContain("super-secret"); + expect(JSON.stringify(descriptor)).not.toContain("private.example.test"); + expect(JSON.stringify(descriptor)).not.toContain("cookie="); + }); +}); diff --git a/apps/desktop-tauri/src/lib/providerState.ts b/apps/desktop-tauri/src/lib/providerState.ts new file mode 100644 index 0000000000..270974f37d --- /dev/null +++ b/apps/desktop-tauri/src/lib/providerState.ts @@ -0,0 +1,78 @@ +import type { LocaleKey } from "../i18n/keys"; + +/** + * A presentation-safe summary of a provider refresh result. + * + * Provider errors can contain account, host, path, or credential details. + * Keep those details out of compact and settings-facing surfaces. The raw + * value stays in the backend diagnostic path; this helper exposes only a + * stable category that is safe to render. + */ +export type ProviderStateKind = + | "ready" + | "needs-authentication" + | "expired-session" + | "legacy-telemetry" + | "local-runtime-offline" + | "unknown"; + +export interface ProviderStateDescriptor { + kind: ProviderStateKind; + isProblem: boolean; + labelKey: LocaleKey; +} + +const AUTH_PATTERN = + /\bauth(?:entication|orization)?\b|\bsign[ -]?in\b|\blog[ -]?in\b|\bcredential(?:s)?\b|\bcookie(?:s)?\b|\boauth\b|\bunauthori[sz]ed\b|\bforbidden\b|\bpermission denied\b/i; +const EXPIRED_SESSION_PATTERN = + /\b(?:session|token|cookie|credential|oauth)\b[^\n]{0,48}\b(?:expired|invalid|revoked)\b|\b(?:expired|invalid|revoked)\b[^\n]{0,48}\b(?:session|token|cookie|credential|oauth)\b/i; +const LEGACY_TELEMETRY_PATTERN = + /\blegacy\b|\btelemetry\b|\bdeprecated\b|\bsource mode\b[^\n]{0,48}\bnot supported\b/i; +const LOCAL_RUNTIME_PATTERN = + /\b(?:local|runtime|daemon|service|app|cli|language server)\b[^\n]{0,48}\b(?:offline|not running|unavailable|not found)\b|\bconnection refused\b|\boffline\b/i; + +/** + * Categorize a raw refresh error without returning any part of that error. + * Ordering matters: an explicitly expired credential is more useful than the + * broader authentication category, and a legacy telemetry source is distinct + * from a local runtime that is simply not running. + */ +export function describeProviderState(error: string | null | undefined): ProviderStateDescriptor { + if (!error?.trim()) { + return { kind: "ready", isProblem: false, labelKey: "ProviderStatusOk" }; + } + + if (EXPIRED_SESSION_PATTERN.test(error)) { + return { + kind: "expired-session", + isProblem: true, + labelKey: "ProviderIssueSessionExpired", + }; + } + if (LEGACY_TELEMETRY_PATTERN.test(error)) { + return { + kind: "legacy-telemetry", + isProblem: true, + labelKey: "ProviderIssueLegacyTelemetry", + }; + } + if (AUTH_PATTERN.test(error)) { + return { + kind: "needs-authentication", + isProblem: true, + labelKey: "ProviderIssueAuthRequired", + }; + } + if (LOCAL_RUNTIME_PATTERN.test(error)) { + return { + kind: "local-runtime-offline", + isProblem: true, + labelKey: "ProviderIssueLocalRuntimeOffline", + }; + } + return { + kind: "unknown", + isProblem: true, + labelKey: "ProviderIssueUnknown", + }; +} diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx index 744672f088..8147c1cf17 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx @@ -254,11 +254,6 @@ export function ProviderDetailPane({ const handleOpenStatusPage = () => { void openProviderStatusPage(detail.id).catch(setErr); }; - const handleCopyError = () => { - if (detail.lastError && navigator.clipboard) { - void navigator.clipboard.writeText(detail.lastError); - } - }; const handleBuyCredits = () => { if (detail.buyCreditsUrl) { void openProviderDashboard(detail.id).catch(setErr); @@ -272,8 +267,7 @@ export function ProviderDetailPane({ {detail.lastError && ( )} @@ -368,7 +362,6 @@ export function ProviderDetailPane({ onSwitchAccount={handleSwitchAccount} onOpenDashboard={handleOpenDashboard} onOpenStatusPage={handleOpenStatusPage} - onCopyError={handleCopyError} onBuyCredits={handleBuyCredits} t={t} /> diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/ProviderIssueNotice.test.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/ProviderIssueNotice.test.tsx new file mode 100644 index 0000000000..886ed53708 --- /dev/null +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/ProviderIssueNotice.test.tsx @@ -0,0 +1,27 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { ProviderDetail } from "../../../../types/bridge"; +import { ProviderIssueNotice } from "./ProviderIssueNotice"; + +const detail = { + id: "cursor", + displayName: "Cursor", +} as ProviderDetail; + +describe("ProviderIssueNotice", () => { + it("renders a categorized notice without rendering the raw diagnostic", () => { + const raw = "cookie=super-secret; authentication required at https://private.example.test"; + const t = vi.fn((key: string) => ({ + ProviderIssueAuthRequired: "Sign-in required", + ProviderIssuePrivacySafeDetail: "Details are hidden here to protect account data.", + })[key] ?? key); + + render(); + + expect(screen.getByRole("status")).toHaveTextContent("Cursor: Sign-in required"); + expect(screen.getByRole("status")).toHaveTextContent( + "Details are hidden here to protect account data.", + ); + expect(screen.queryByText(/super-secret|private\.example\.test/i)).toBeNull(); + }); +}); diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/ProviderIssueNotice.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/ProviderIssueNotice.tsx index 97a75bda45..59423a858e 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/ProviderIssueNotice.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/ProviderIssueNotice.tsx @@ -1,55 +1,23 @@ import type { ProviderDetail } from "../../../../types/bridge"; import type { LocaleKey } from "../../../../i18n/keys"; +import { describeProviderState } from "../../../../lib/providerState"; interface Props { detail: ProviderDetail; - message: string; - onCopy: () => void; + rawError: string; t: (key: LocaleKey) => string; } -export function ProviderIssueNotice({ detail, message, onCopy, t }: Props) { - const cleaned = message.replace(/^last fetch failed:\s*/i, "").trim(); - const lower = cleaned.toLowerCase(); - const needsLogin = - lower.includes("auth.json not found") || - lower.includes("not signed in") || - lower.includes("credentials not found") || - lower.includes("oauth credentials not found") || - lower.includes("run `") || - lower.includes("run codex") || - lower.includes("run claude"); - const title = needsLogin - ? `${detail.displayName} ${t("ProviderIssueNeedsSignIn")}` - : t("ProviderIssueFetchNeedsAttention"); - const displayMessage = localizeProviderIssue(cleaned, t); +export function ProviderIssueNotice({ detail, rawError, t }: Props) { + const state = describeProviderState(rawError); + const title = `${detail.displayName}: ${t(state.labelKey)}`; return (
{title} -
-

{displayMessage}

+

{t("ProviderIssuePrivacySafeDetail")}

); } - -function localizeProviderIssue( - message: string, - t: (key: LocaleKey) => string, -): string { - const unsupported = message.match( - /^Source mode `?([^`']+)`? not supported for this provider$/i, - ); - if (unsupported) { - return `${t("ProviderIssueUnsupportedSourceModePrefix")} (${unsupported[1]})`; - } - return message; -} diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/QuickActionsSection.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/QuickActionsSection.tsx index a9731745bb..63964a6ab3 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/QuickActionsSection.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/QuickActionsSection.tsx @@ -8,7 +8,6 @@ interface Props { onSwitchAccount: () => void; onOpenDashboard: () => void; onOpenStatusPage: () => void; - onCopyError: () => void; onBuyCredits: () => void; t: (key: LocaleKey) => string; } @@ -26,7 +25,6 @@ export function QuickActionsSection({ onSwitchAccount, onOpenDashboard, onOpenStatusPage, - onCopyError, onBuyCredits, t, }: Props) { @@ -70,15 +68,6 @@ export function QuickActionsSection({ {t("ActionStatusPage")} )} - {provider.lastError && ( - - )} {provider.buyCreditsUrl && (