Skip to content

Commit 874b182

Browse files
joohwcursoragent
andcommitted
fix(core,desktop): clear subscription usage when logged out
Hide and purge cached quota when subscription OAuth is missing or logged out; reject usage queries without credentials on the core side. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ff1308b commit 874b182

10 files changed

Lines changed: 82 additions & 10 deletions

File tree

core/internal/buildinfo/buildinfo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import "strings"
44

55
// Set at link time via -ldflags (see .goreleaser.yaml).
66
var (
7-
Version = "dev0.1.103"
7+
Version = "dev0.1.104"
88
Commit = "none"
99
Date = "unknown"
1010
)

core/internal/desktop/usage.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ func resolveVendorCredentials(vendor profile.Profile) (baseURL, apiKey, template
8080
func querySubscriptionVendorUsage(vendor profile.Profile) usage.Result {
8181
flat := vendor
8282
profile.HydrateSubscriptionCredentials(&flat)
83+
if strings.TrimSpace(flat.APIKey) == "" {
84+
return usage.Result{Success: false, Kind: "subscription", Error: "subscription not logged in"}
85+
}
8386
return usage.QuerySubscriptionUsage(flat.SubscriptionProviderID, flat.APIKey, flat.AccountID)
8487
}
8588

core/internal/desktop/usage_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,19 @@ func TestResolveVendorCredentialsFallsBackToModelConnection(t *testing.T) {
2525
t.Fatalf("template = %q want %q", template, usage.TemplateAuto)
2626
}
2727
}
28+
29+
func TestQuerySubscriptionVendorUsageRequiresLogin(t *testing.T) {
30+
profile.SetClaudeCredentialsPathOverride(t.TempDir() + "/missing-claude.json")
31+
t.Cleanup(func() { profile.SetClaudeCredentialsPathOverride("") })
32+
33+
result := querySubscriptionVendorUsage(profile.Profile{
34+
Kind: "subscription",
35+
SubscriptionProviderID: "claude-code",
36+
})
37+
if result.Success {
38+
t.Fatal("expected usage query to fail without subscription credentials")
39+
}
40+
if result.Error != "subscription not logged in" {
41+
t.Fatalf("error = %q want %q", result.Error, "subscription not logged in")
42+
}
43+
}

electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "clovapi-switcher",
33
"private": true,
4-
"version": "0.2.12",
4+
"version": "0.2.13",
55
"description": "ClovAPI Switcher desktop app",
66
"main": "main.js",
77
"type": "commonjs",

electron/ui/src/components/ProfilesPanel.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
subscriptionStatusForVendor,
1010
vendorSummaryLine,
1111
subscriptionIsUsable,
12+
shouldShowVendorUsage,
1213
} from "../lib/helpers";
1314
import { displayVendorName, i18n, t } from "../lib/i18n";
1415
import {
@@ -17,7 +18,7 @@
1718
openProfilesVendor,
1819
queryVendorUsage,
1920
store,
20-
vendorUsageSummary,
21+
vendorUsageSummaryForVendor,
2122
canFetchVendorModels,
2223
} from "../lib/store.svelte";
2324
import ListRow from "./ListRow.svelte";
@@ -75,7 +76,7 @@
7576
7677
function canQueryUsage(vendor: (typeof vendorList)[number]) {
7778
if (vendor.kind === "subscription") {
78-
return subscriptionStatusForVendor(vendor, store.subscriptions)?.active === true;
79+
return shouldShowVendorUsage(vendor, store.subscriptions);
7980
}
8081
if (vendor.kind === "api") {
8182
return Boolean((vendor.baseUrl && vendor.apiKey) || vendor.models?.some((model) => model.baseUrl && model.apiKey));
@@ -86,7 +87,7 @@
8687
function summaryLine(vendor: (typeof vendorList)[number]) {
8788
const sub = subscriptionStatusForVendor(vendor, store.subscriptions);
8889
const base = vendorSummaryLine(vendor, sub, store.ollamaInstalled);
89-
const usage = vendor.kind === "api" || vendor.kind === "subscription" ? vendorUsageSummary(vendor.name) : "";
90+
const usage = vendorUsageSummaryForVendor(vendor, store.subscriptions);
9091
return usage ? `${base} · ${usage}` : base;
9192
}
9293

electron/ui/src/components/VendorDetailPanel.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
isSubscriptionLogging,
2424
openModelDialog,
2525
openProfileDialog,
26-
vendorUsageSummary,
26+
vendorUsageSummaryForVendor,
2727
removeVendorModel,
2828
runModelTest,
2929
runSubscriptionLogin,
@@ -90,7 +90,7 @@
9090
9191
const cardDescription = $derived.by(() => {
9292
void i18n.locale;
93-
const usage = vendor.kind === "api" || vendor.kind === "subscription" ? vendorUsageSummary(vendor.name) : "";
93+
const usage = vendorUsageSummaryForVendor(vendor, store.subscriptions);
9494
const parts: string[] = [];
9595
if (vendor.kind === "subscription") {
9696
parts.push(formatSubscriptionSummary(subscription?.summary || ""));

electron/ui/src/lib/helpers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,19 @@ export function subscriptionIsUsable(subscription: SubscriptionItem | undefined)
467467
return subscription.active !== false;
468468
}
469469

470+
/** Whether vendor quota/balance should be queried or shown in the UI. */
471+
export function shouldShowVendorUsage(vendor: Vendor, subscriptions: SubscriptionItem[]): boolean {
472+
if (vendor.kind === "local") return false;
473+
if (vendor.kind === "subscription") {
474+
return subscriptionIsUsable(subscriptionStatusForVendor(vendor, subscriptions));
475+
}
476+
if (vendor.kind === "api") {
477+
if (vendor.baseUrl && vendor.apiKey) return true;
478+
return Boolean(vendor.models?.some((model) => model.baseUrl && model.apiKey));
479+
}
480+
return false;
481+
}
482+
470483
export type CliBindingOption = {
471484
value: string;
472485
label: string;

electron/ui/src/lib/store/store.svelte.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ export {
2727
removeVendorModel,
2828
} from "./profiles";
2929
export { canFetchVendorModels, isVendorFetching, fetchVendorModels } from "./vendor-models";
30-
export { queryVendorUsage, vendorUsageSummary, isVendorUsageLoading } from "./vendor-usage";
30+
export {
31+
queryVendorUsage,
32+
vendorUsageSummary,
33+
vendorUsageSummaryForVendor,
34+
clearVendorUsage,
35+
isVendorUsageLoading,
36+
} from "./vendor-usage";
3137
export {
3238
refreshProxyStatus,
3339
refreshProxyLogs,

electron/ui/src/lib/store/subscriptions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { clearModelTest } from "./model-tests";
1313
import { runModelTest } from "./model-runner";
1414
import { loadProfilesFromDisk, persistProfiles } from "./profiles";
1515
import { fetchVendorModels } from "./vendor-models";
16+
import { clearVendorUsage, pruneVendorUsageForSubscriptions } from "./vendor-usage";
1617
import { store } from "./state.svelte";
1718
import { toast } from "../toast";
1819
import type { SubscriptionItem, SubscriptionVendorRow } from "../../global";
@@ -39,6 +40,7 @@ export async function refreshSubscriptions() {
3940
} catch {
4041
store.subscriptions = [];
4142
}
43+
pruneVendorUsageForSubscriptions(store.subscriptions);
4244
}
4345

4446
export async function cancelSubscriptionLogin(providerId: string) {
@@ -98,6 +100,7 @@ export async function runSubscriptionLogout(providerId: string, label: string) {
98100
const previousVendor = getSubscriptionVendors(store.profiles).find(
99101
(item) => item.subscriptionProviderId === providerId,
100102
);
103+
if (previousVendor?.name) clearVendorUsage(previousVendor.name);
101104
const result = await bridge.authLogout(providerId);
102105
if (!result?.ok) {
103106
toast.error(result?.error || t("toast.logoutFailed"));

electron/ui/src/lib/store/vendor-usage.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { Vendor, VendorUsageData, VendorUsageResult } from "../../global";
1+
import type { SubscriptionItem, Vendor, VendorUsageData, VendorUsageResult } from "../../global";
2+
import { getSubscriptionVendors, shouldShowVendorUsage } from "../helpers";
23
import { t } from "../i18n";
34
import { toast } from "../toast";
45
import { store } from "./state.svelte";
@@ -7,6 +8,26 @@ export function vendorUsageSummary(vendorName: string): string {
78
return store.vendorUsage[vendorName]?.summary || "";
89
}
910

11+
export function vendorUsageSummaryForVendor(vendor: Vendor, subscriptions: SubscriptionItem[]): string {
12+
if (!shouldShowVendorUsage(vendor, subscriptions)) return "";
13+
return vendorUsageSummary(vendor.name);
14+
}
15+
16+
export function clearVendorUsage(vendorName: string): void {
17+
const name = String(vendorName || "").trim();
18+
if (!name) return;
19+
delete store.vendorUsage[name];
20+
delete store.vendorUsageLoading[name];
21+
}
22+
23+
export function pruneVendorUsageForSubscriptions(subscriptions: SubscriptionItem[]): void {
24+
for (const vendor of getSubscriptionVendors(store.profiles)) {
25+
if (!shouldShowVendorUsage(vendor, subscriptions)) {
26+
clearVendorUsage(vendor.name);
27+
}
28+
}
29+
}
30+
1031
export function isVendorUsageLoading(vendorName: string): boolean {
1132
return Boolean(store.vendorUsageLoading[vendorName]);
1233
}
@@ -74,6 +95,10 @@ export async function queryVendorUsage(vendor: Vendor, options: { silent?: boole
7495
const name = String(vendor?.name || "").trim();
7596
if (!name) return;
7697
if (vendor.kind === "local") return;
98+
if (vendor.kind === "subscription" && !shouldShowVendorUsage(vendor, store.subscriptions)) {
99+
clearVendorUsage(name);
100+
return;
101+
}
77102
if (vendor.kind === "api" && !vendorHasUsageCredentials(vendor)) return;
78103
const bridge = window.clovapiCli;
79104
if (!bridge?.profilesUsage) {
@@ -87,7 +112,12 @@ export async function queryVendorUsage(vendor: Vendor, options: { silent?: boole
87112
const result = await bridge.profilesUsage(name);
88113
if (!result?.ok || !result.usage?.success) {
89114
const message = result?.error || result?.usage?.error || t("toast.vendorUsageFailed");
90-
if (options.silent) return;
115+
if (vendor.kind === "subscription") {
116+
clearVendorUsage(name);
117+
if (options.silent) return;
118+
} else if (options.silent) {
119+
return;
120+
}
91121
store.vendorUsage[name] = { summary: message, rows: [], error: message };
92122
if (!options.silent) toast.error(message);
93123
return;

0 commit comments

Comments
 (0)