@@ -7,6 +7,8 @@ const PLAN_MAP = {
77 free : "Free" ,
88 individual : "Pro" ,
99 individual_pro : "Pro+" ,
10+ individual_max : "Max" ,
11+ individual_edu : "Student" ,
1012 business : "Business" ,
1113 enterprise : "Enterprise" ,
1214} ;
@@ -17,11 +19,14 @@ const PLAN_MAP = {
1719 * @property {number } quota
1820 * @property {number } usedPct - already-used percentage (0–100+)
1921 * @property {boolean } unlimited
22+ * @property {boolean } hasQuota - false signals pooled-entitlement exhaustion (enterprise unlimited)
23+ * @property {boolean } exhausted - derived: unlimited pool drained with no overage; consumers should signal a hard error state
2024 * @property {boolean } noData - true when the plan has no premium interactions quota
2125 * @property {boolean } overageEnabled
2226 * @property {number } overageUsed
2327 * @property {string } plan
24- * @property {Date } resetDate
28+ * @property {Date | undefined } resetDate - undefined when the server provided no reset source; consumers should hide the row
29+ * @property {boolean } tokenBasedBilling - true = UBB (credits) mode, false = legacy premium-requests mode
2530 */
2631
2732/**
@@ -80,59 +85,130 @@ async function fetchUsage(token) {
8085 const plan = PLAN_MAP [ data . copilot_plan ] ?? data . copilot_plan ?? "Unknown" ;
8186
8287 const pi = data ?. quota_snapshots ?. premium_interactions ;
83- if ( ! pi || pi . percent_remaining == null ) {
84- const unlimited = ! ! pi ?. unlimited ;
85- return {
86- used : 0 ,
87- quota : pi ?. entitlement ?? 0 ,
88- usedPct : 0 ,
89- unlimited,
90- noData : ! unlimited ,
91- overageEnabled : ! ! pi ?. overage_permitted ,
92- overageUsed : pi ?. overage_count ?? 0 ,
93- plan,
94- resetDate : getNextMonthReset ( ) ,
95- } ;
88+ const tokenBasedBilling = ! ! data . token_based_billing ;
89+
90+ const parsedEntitlement = parseNonNegativeNumber ( pi ?. entitlement ) ;
91+ const entitlement = parsedEntitlement ?? 0 ;
92+
93+ // Two cases route to noData (no usable premium counter):
94+ // 1. Explicit zero entitlement (`entitlement: '0'` or `0`) — matches upstream parseQuotas
95+ // `parsedEntitlement === 0` skip rule.
96+ // 2. Free CFI shape: entitlement field absent AND percent_remaining === 0. Upstream
97+ // dashboard keeps this snapshot because chat/completions indicators give context;
98+ // our single-value status bar would otherwise show "100% red" and falsely imply
99+ // exhaustion to a user who never had a premium allowance.
100+ // percent_remaining is coerced via parseNonNegativeNumber so a string "0" payload
101+ // doesn't bypass this carve-out (the downstream Number(...) path is already lenient).
102+ const noEntitlement =
103+ ! pi ?. unlimited &&
104+ ( parsedEntitlement === 0 ||
105+ ( parsedEntitlement === undefined && parseNonNegativeNumber ( pi ?. percent_remaining ) === 0 ) ) ;
106+
107+ const resetDate = parseResetDate ( data , pi , tokenBasedBilling ) ;
108+ const unlimited = ! ! pi ?. unlimited ;
109+ const hasQuota = Boolean ( pi ?. has_quota ?? true ) ;
110+ const overageEnabled = ! ! pi ?. overage_permitted ;
111+ const exhausted = unlimited && ! hasQuota && ! overageEnabled ;
112+ const overageUsed = pi ?. overage_count ?? 0 ;
113+
114+ // Fields shared by both return shapes — keeps the per-branch returns focused
115+ // on the state-dependent fields (used, usedPct, noData).
116+ const shared = {
117+ quota : entitlement ,
118+ unlimited,
119+ hasQuota,
120+ exhausted,
121+ overageEnabled,
122+ overageUsed,
123+ plan,
124+ resetDate,
125+ tokenBasedBilling,
126+ } ;
127+
128+ if ( ! pi || pi . percent_remaining == null || noEntitlement ) {
129+ return { ...shared , used : 0 , usedPct : 0 , noData : ! unlimited } ;
96130 }
97131
98- const entitlement = pi . entitlement ?? 0 ;
99132 const percentRemaining = Number ( pi . percent_remaining ) ;
100133 if ( ! Number . isFinite ( percentRemaining ) ) {
101134 throw makeError ( "API_ERROR" , "Invalid percent_remaining from GitHub API" ) ;
102135 }
103136 const usedPct = Math . max ( 0 , Math . round ( ( 100 - percentRemaining ) * 10 ) / 10 ) ;
104- const used =
105- entitlement > 0 ? Math . max ( 0 , Math . round ( ( entitlement * ( 100 - percentRemaining ) ) / 100 ) ) : 0 ;
106137
107- const rawResetDate = data . quota_reset_date ? new Date ( data . quota_reset_date ) : null ;
108- const resetDate =
109- rawResetDate && ! isNaN ( rawResetDate . getTime ( ) ) ? rawResetDate : getNextMonthReset ( ) ;
138+ const quotaRemaining = parseNonNegativeNumber ( pi . quota_remaining ) ;
139+ const used =
140+ quotaRemaining !== undefined
141+ ? Math . max ( 0 , entitlement - quotaRemaining )
142+ : entitlement > 0
143+ ? Math . max ( 0 , Math . round ( ( entitlement * ( 100 - percentRemaining ) ) / 100 ) )
144+ : 0 ;
110145
111- return {
112- used,
113- quota : entitlement ,
114- usedPct,
115- unlimited : ! ! pi . unlimited ,
116- noData : false ,
117- overageEnabled : ! ! pi . overage_permitted ,
118- overageUsed : pi . overage_count ?? 0 ,
119- plan,
120- resetDate,
121- } ;
146+ return { ...shared , used, usedPct, noData : false } ;
122147 } finally {
123148 clearTimeout ( timeout ) ;
124149 }
125150}
126151
127- function getNextMonthReset ( ) {
128- const now = new Date ( ) ;
129- return new Date ( Date . UTC ( now . getUTCFullYear ( ) , now . getUTCMonth ( ) + 1 , 1 ) ) ;
152+ /**
153+ * Coerce a raw value to a finite non-negative number, or undefined if it
154+ * cannot be. Matches upstream parseQuotas behavior for `entitlement` and
155+ * `quota_remaining` (string or number both acceptable).
156+ *
157+ * @param {unknown } raw
158+ * @returns {number | undefined }
159+ */
160+ function parseNonNegativeNumber ( raw ) {
161+ if ( raw == null ) return undefined ;
162+ const n = Number ( raw ) ;
163+ return Number . isFinite ( n ) && n >= 0 ? n : undefined ;
164+ }
165+
166+ /**
167+ * Resolve resetDate by priority, returning the first source that yields a
168+ * valid Date — or `undefined` if none does. Mirrors upstream parseQuotas:
169+ * 1. UBB only: per-snapshot quota_reset_at (Unix seconds)
170+ * 2. Top-level quota_reset_date_utc
171+ * 3. Top-level quota_reset_date (local)
172+ * 4. Top-level limited_user_reset_date (Free SKU)
173+ *
174+ * Upstream returns undefined when no source is available; the dashboard hides
175+ * the "Resets …" line entirely. We follow the same shape and let consumers
176+ * decide what to render — simpler than synthesizing a misleading next-month date.
177+ *
178+ * Note: upstream `parseQuotas` keeps `resetAt` regardless of billing mode and
179+ * only narrows to UBB at the consumer (chatStatusDashboard). We narrow here
180+ * because this module's only consumer (the status bar) treats UBB as the sole
181+ * legitimate source of per-snapshot reset times. Behavior equivalent to upstream.
182+ *
183+ * @param {any } data - top-level API response body
184+ * @param {any } pi - quota_snapshots.premium_interactions (may be null/undefined)
185+ * @param {boolean } tokenBasedBilling
186+ * @returns {Date | undefined }
187+ */
188+ function parseResetDate ( data , pi , tokenBasedBilling ) {
189+ if ( tokenBasedBilling ) {
190+ const parsedResetAt = Number ( pi ?. quota_reset_at ) ;
191+ if ( Number . isFinite ( parsedResetAt ) && parsedResetAt > 0 ) {
192+ return new Date ( parsedResetAt * 1000 ) ;
193+ }
194+ }
195+ // Walk fallbacks: a malformed entry must not block the next one. limited_user_reset_date
196+ // is the Free-SKU-only field upstream uses last (see parseQuotas).
197+ for ( const raw of [
198+ data ?. quota_reset_date_utc ,
199+ data ?. quota_reset_date ,
200+ data ?. limited_user_reset_date ,
201+ ] ) {
202+ if ( ! raw ) continue ;
203+ const d = new Date ( raw ) ;
204+ if ( ! isNaN ( d . getTime ( ) ) ) return d ;
205+ }
206+ return undefined ;
130207}
131208
132209/** @param {string } code @param {string } message */
133210function makeError ( code , message ) {
134211 const err = new Error ( message ) ;
135- // @ts -ignore
136212 err . code = code ;
137213 return err ;
138214}
0 commit comments