Skip to content

Commit d2602e8

Browse files
committed
Enhance usage and spending tracking logic
Adds support for fetching and displaying aggregate usage summary and hard limit data from new API endpoints. Improves logic for resolving hard limit values, updates models to include new fields, and refines status bar display to show both requests and spending information. This provides more accurate and comprehensive usage reporting for both individual and team flows.
1 parent bf6818c commit d2602e8

4 files changed

Lines changed: 184 additions & 13 deletions

File tree

src/api.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import * as https from "https";
2-
import { TeamsResponse, TeamDetails, SpendData, UserMeResponse, UserUsageResponse } from "./models";
2+
import {
3+
TeamsResponse,
4+
TeamDetails,
5+
SpendData,
6+
UserMeResponse,
7+
UserUsageResponse,
8+
UsageSummaryResponse,
9+
HardLimitResponse,
10+
} from "./models";
311

412
const BASE_URL = "https://cursor.com/api";
513

@@ -120,3 +128,13 @@ export async function fetchUserMe(cookie: string): Promise<UserMeResponse> {
120128
export async function fetchUserUsage(userId: string, cookie: string): Promise<UserUsageResponse> {
121129
return get<UserUsageResponse>(`usage?user=${userId}`, cookie);
122130
}
131+
132+
/** Fetches the aggregate usage summary, including on-demand USD usage. */
133+
export async function fetchUsageSummary(cookie: string): Promise<UsageSummaryResponse> {
134+
return get<UsageSummaryResponse>("usage-summary", cookie);
135+
}
136+
137+
/** Fetches the current hard limit for on-demand spending. */
138+
export async function fetchHardLimit(cookie: string): Promise<HardLimitResponse> {
139+
return get<HardLimitResponse>("dashboard/get-hard-limit", cookie);
140+
}

src/extension.ts

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import * as vscode from "vscode";
22
import * as api from "./api";
33
import * as statusBar from "./statusBar";
44
import * as config from "./configuration";
5-
import { TeamMemberSpend } from "./models";
5+
import { SpendData, TeamMemberSpend } from "./models";
66

77
let refreshTimer: NodeJS.Timeout | undefined;
8+
const CENTS_PER_DOLLAR = 100;
89

910
/**
1011
* Calculates the next reset date based on the start of month date.
@@ -33,6 +34,63 @@ function calculateResetInfo(startOfMonth: string): {
3334
return { resetDate, daysRemaining, resetDateStr };
3435
}
3536

37+
function resolveHardLimitDollars(
38+
spendData?: SpendData,
39+
memberSpend?: TeamMemberSpend
40+
): number | undefined {
41+
const memberCandidates = [
42+
memberSpend?.hardLimitOverrideDollars,
43+
memberSpend?.hardLimitDollars,
44+
];
45+
46+
for (const candidate of memberCandidates) {
47+
if (isValidNumber(candidate)) {
48+
return candidate;
49+
}
50+
}
51+
52+
if (!spendData) {
53+
return undefined;
54+
}
55+
56+
const teamDollarCandidates = [
57+
spendData.teamHardLimitDollars,
58+
spendData.hardLimitDollars,
59+
spendData.defaultHardLimitDollars,
60+
];
61+
62+
for (const candidate of teamDollarCandidates) {
63+
if (isValidNumber(candidate)) {
64+
return candidate;
65+
}
66+
}
67+
68+
const teamCentCandidates = [
69+
spendData.teamHardLimitCents,
70+
spendData.hardLimitCents,
71+
spendData.defaultHardLimitCents,
72+
];
73+
74+
for (const candidate of teamCentCandidates) {
75+
if (isValidNumber(candidate)) {
76+
return candidate / 100;
77+
}
78+
}
79+
80+
return undefined;
81+
}
82+
83+
function isValidNumber(value?: number | null): value is number {
84+
return typeof value === "number" && Number.isFinite(value);
85+
}
86+
87+
function dollarsToCents(amount?: number | null): number | undefined {
88+
if (!isValidNumber(amount)) {
89+
return undefined;
90+
}
91+
return Math.round(amount );
92+
}
93+
3694
/**
3795
* This is the main activation function for the extension.
3896
* It's called by VS Code when the extension is activated.
@@ -191,12 +249,14 @@ async function refreshUsage(context: vscode.ExtensionContext): Promise<void> {
191249

192250
// TEAM FLOW: Try to get team-based usage data if user has related team from api/sets a team ID manually
193251
// If we couldn't get team spend data, we'll show a simplified view with just the individual user data
194-
if (teamId) {
252+
let teamSpendData: SpendData | undefined;
253+
254+
if (teamId) {
195255
try {
196256
const userDetails = await api.fetchTeamDetails(teamId, cookie);
197-
const spendData = await api.fetchTeamSpend(teamId, cookie);
257+
teamSpendData = await api.fetchTeamSpend(teamId, cookie);
198258

199-
mySpend = spendData.teamMemberSpend.find(
259+
mySpend = teamSpendData.teamMemberSpend.find(
200260
(member) => member.userId === userDetails.userId
201261
);
202262
} catch (teamError: any) {
@@ -221,7 +281,47 @@ async function refreshUsage(context: vscode.ExtensionContext): Promise<void> {
221281
// TEAM FLOW: Use team-based data when available
222282
usedRequests = mySpend.fastPremiumRequests;
223283
spendCents = mySpend.spendCents;
224-
hardLimitDollars = mySpend.hardLimitOverrideDollars;
284+
hardLimitDollars = resolveHardLimitDollars(teamSpendData, mySpend);
285+
}
286+
287+
if (spendCents === undefined || hardLimitDollars === undefined) {
288+
try {
289+
const usageSummary = await api.fetchUsageSummary(cookie);
290+
const onDemandUsage = usageSummary.individualUsage?.onDemand;
291+
292+
if (onDemandUsage?.enabled) {
293+
if (spendCents === undefined) {
294+
const usageCents = dollarsToCents(onDemandUsage.used);
295+
if (usageCents !== undefined) {
296+
spendCents = usageCents;
297+
}
298+
}
299+
300+
if (hardLimitDollars === undefined && isValidNumber(onDemandUsage.limit)) {
301+
hardLimitDollars = dollarsToCents(onDemandUsage.limit);
302+
if (hardLimitDollars !== undefined) {
303+
hardLimitDollars = hardLimitDollars / 100;
304+
}
305+
}
306+
}
307+
} catch (summaryError: any) {
308+
console.warn(
309+
`[Cursor Usage] Failed to fetch usage summary: ${summaryError.message}`
310+
);
311+
}
312+
}
313+
314+
if (hardLimitDollars === undefined) {
315+
try {
316+
const hardLimitResponse = await api.fetchHardLimit(cookie);
317+
if (isValidNumber(hardLimitResponse.hardLimit)) {
318+
hardLimitDollars = hardLimitResponse.hardLimit;
319+
}
320+
} catch (hardLimitError: any) {
321+
console.warn(
322+
`[Cursor Usage] Failed to fetch hard limit: ${hardLimitError.message}`
323+
);
324+
}
225325
}
226326

227327
// Calculate final values and update status bar
@@ -231,6 +331,7 @@ async function refreshUsage(context: vscode.ExtensionContext): Promise<void> {
231331
statusBar.updateStatusBar(
232332
remainingRequests,
233333
maxRequests,
334+
usedRequests,
234335
spendCents,
235336
hardLimitDollars,
236337
resetInfo

src/models.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface TeamMemberSpend {
3030
userId?: number;
3131
spendCents?: number;
3232
hardLimitOverrideDollars?: number;
33+
hardLimitDollars?: number;
3334
name?: string;
3435
role?: string;
3536
}
@@ -40,6 +41,43 @@ export interface TeamMemberSpend {
4041
*/
4142
export interface SpendData {
4243
teamMemberSpend: TeamMemberSpend[];
44+
hardLimitDollars?: number;
45+
hardLimitCents?: number;
46+
defaultHardLimitDollars?: number;
47+
defaultHardLimitCents?: number;
48+
teamHardLimitDollars?: number;
49+
teamHardLimitCents?: number;
50+
}
51+
52+
interface UsageSummaryBreakdown {
53+
included?: number;
54+
bonus?: number;
55+
total?: number;
56+
}
57+
58+
export interface UsageSummaryMetric {
59+
enabled: boolean;
60+
used: number;
61+
limit: number;
62+
remaining: number;
63+
breakdown?: UsageSummaryBreakdown;
64+
}
65+
66+
export interface UsageSummaryResponse {
67+
billingCycleStart: string;
68+
billingCycleEnd: string;
69+
membershipType?: string;
70+
limitType?: string;
71+
isUnlimited?: boolean;
72+
individualUsage?: {
73+
plan?: UsageSummaryMetric;
74+
onDemand?: UsageSummaryMetric;
75+
};
76+
teamUsage?: Record<string, unknown>;
77+
}
78+
79+
export interface HardLimitResponse {
80+
hardLimit: number;
4381
}
4482

4583
/**

src/statusBar.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ export function createStatusBarItem() {
2929
* Updates the status bar with the remaining requests, spending info, reset info, and appropriate color/icon.
3030
* @param remainingRequests The number of requests left.
3131
* @param totalRequests The total number of requests allowed in the cycle.
32+
* @param usedRequests The number of requests already used in the cycle.
3233
* @param spendCents The amount spent in cents (optional).
3334
* @param hardLimitDollars The hard limit in dollars (optional).
3435
* @param resetInfo Information about when the usage resets (optional).
3536
*/
3637
export function updateStatusBar(
3738
remainingRequests: number,
3839
totalRequests: number,
40+
usedRequests: number,
3941
spendCents?: number,
4042
hardLimitDollars?: number,
4143
resetInfo?: ResetInfo
@@ -92,15 +94,22 @@ export function updateStatusBar(
9294
// Only show spending when there are 0 requests left
9395
let statusText: string;
9496

97+
const normalizedUsedRequests = Math.max(0, usedRequests);
98+
const usageSummary = `${normalizedUsedRequests}/${totalRequests}`;
99+
const spendSummary =
100+
spendCents !== undefined && hardLimitDollars !== undefined
101+
? `$${(spendCents / 100).toFixed(2)}/$${hardLimitDollars.toFixed(2)}`
102+
: undefined;
103+
95104
if (remainingRequests > 0) {
96105
// Show remaining requests
97-
statusText = `${icon} ${remainingRequests}`;
106+
statusText = spendSummary
107+
? `${icon} ${remainingRequests} · ${spendSummary}`
108+
: `${icon} ${remainingRequests} · ${usageSummary}`;
98109
} else {
99110
// No requests left - show spending instead (if available)
100-
if (spendCents !== undefined && hardLimitDollars !== undefined) {
101-
const spendDollars = (spendCents / 100).toFixed(2);
102-
const limitDollars = hardLimitDollars.toFixed(2);
103-
statusText = `${icon} $${spendDollars}/$${limitDollars}`;
111+
if (spendSummary) {
112+
statusText = `${icon} ${spendSummary}`;
104113
} else {
105114
// No spending data available, just show 0
106115
statusText = `${icon} 0`;
@@ -112,6 +121,7 @@ export function updateStatusBar(
112121
// Update tooltip with detailed information
113122
updateTooltip(
114123
remainingRequests,
124+
normalizedUsedRequests,
115125
totalRequests,
116126
spendCents,
117127
hardLimitDollars,
@@ -122,13 +132,15 @@ export function updateStatusBar(
122132
/**
123133
* Updates the tooltip with comprehensive usage, spending, and reset information.
124134
* @param remainingRequests The number of requests left.
135+
* @param usedRequests The number of requests already used in the cycle.
125136
* @param totalRequests The total number of requests allowed in the cycle.
126137
* @param spendCents The amount spent in cents (optional).
127138
* @param hardLimitDollars The hard limit in dollars (optional).
128139
* @param resetInfo Information about when the usage resets (optional).
129140
*/
130141
function updateTooltip(
131142
remainingRequests: number,
143+
usedRequests: number,
132144
totalRequests: number,
133145
spendCents?: number,
134146
hardLimitDollars?: number,
@@ -138,8 +150,10 @@ function updateTooltip(
138150
return;
139151
}
140152

141-
const usedRequests = totalRequests - remainingRequests;
142-
const requestPercentage = ((usedRequests / totalRequests) * 100).toFixed(1);
153+
const usageForDisplay = Math.max(0, usedRequests);
154+
const requestPercentage = totalRequests
155+
? ((usageForDisplay / totalRequests) * 100).toFixed(1)
156+
: "0.0";
143157

144158
// Calculate cycle information once if resetInfo is available
145159
let daysElapsed = 0;

0 commit comments

Comments
 (0)