@@ -18,6 +18,10 @@ let refreshInFlight = false;
1818let pendingSignIn = false ;
1919/** @type {boolean } */
2020let deactivated = false ;
21+ /** @type {boolean } */
22+ let isOffline = false ;
23+ /** @type {Date | null } */
24+ let offlineSince = null ;
2125
2226/**
2327 * @param {vscode.ExtensionContext } context
@@ -50,6 +54,8 @@ async function activate(context) {
5054
5155function deactivate ( ) {
5256 deactivated = true ;
57+ isOffline = false ;
58+ offlineSince = null ;
5359 statusBarItem ?. hide ( ) ;
5460 clearTimer ( ) ;
5561}
@@ -80,21 +86,34 @@ async function refresh(promptSignIn = false, isManual = false) {
8086 } ) ;
8187 } catch {
8288 // User cancelled the sign-in prompt (createIfNone: true throws on cancel)
89+ isOffline = false ;
90+ offlineSince = null ;
8391 showNoAuth ( ) ;
8492 return ;
8593 }
8694
8795 if ( ! session ) {
96+ isOffline = false ;
97+ offlineSince = null ;
8898 showNoAuth ( ) ;
8999 return ;
90100 }
91101
92102 const data = await fetchUsage ( session . accessToken ) ;
93103 lastData = data ;
94104 lastUpdatedAt = new Date ( ) ;
105+ isOffline = false ;
106+ offlineSince = null ;
95107 updateStatusBar ( data ) ;
96108 } catch ( err ) {
97109 const code = err ?. code ;
110+
111+ // Any error that reached the server means we are online — clear offline state.
112+ if ( code !== 'NETWORK_ERROR' && code !== 'TIMEOUT' ) {
113+ isOffline = false ;
114+ offlineSince = null ;
115+ }
116+
98117 if ( code === 'AUTH' ) {
99118 showNoAuth ( ) ;
100119 } else if ( code === 'FORBIDDEN' ) {
@@ -105,15 +124,26 @@ async function refresh(promptSignIn = false, isManual = false) {
105124 try {
106125 updateStatusBar ( lastData , true ) ;
107126 } catch {
108- showError ( ' Rate limited') ;
127+ renderStatus ( { text : '$(alert)' , tooltip : 'Copilot Usage: Rate limited' } ) ;
109128 }
110129 } else {
111- showError ( ' Rate limited') ;
130+ renderStatus ( { text : '$(alert)' , tooltip : 'Copilot Usage: Rate limited' } ) ;
112131 }
113132 } else if ( code === 'SERVER_ERROR' ) {
114133 showError ( 'API error (5xx)' ) ;
115134 } else if ( code === 'NETWORK_ERROR' || code === 'TIMEOUT' ) {
116- showError ( 'Network error' ) ;
135+ if ( ! isOffline ) {
136+ isOffline = true ;
137+ offlineSince = new Date ( ) ;
138+ }
139+ if ( lastData ) {
140+ updateStatusBar ( lastData ) ;
141+ } else {
142+ renderStatus ( {
143+ text : '$(alert)' ,
144+ tooltip : 'Copilot Usage: Offline' ,
145+ } ) ;
146+ }
117147 } else {
118148 showError ( 'Network / API error' ) ;
119149 }
@@ -138,21 +168,25 @@ const BILLING_URL = 'https://github.com/settings/billing/premium_requests_usage'
138168 */
139169function updateStatusBar ( data , isRateLimited = false ) {
140170 const cfg = getConfig ( ) ;
171+ const isStale = computeIsStale ( isOffline , offlineSince ) ;
172+ const staleIcon = isStale ? ' $(warning)' : '' ;
141173
142174 if ( data . noData ) {
143175 const md = new vscode . MarkdownString ( '' , true ) ;
144176 md . isTrusted = { enabledCommands : [ 'githubCopilotUsage.refresh' ] } ;
145- md . appendText ( `No premium quota · Plan: ${ data . plan } ` ) ;
146- if ( isRateLimited ) md . appendMarkdown ( '\n\n_(Rate limited — showing last known data)_' ) ;
147- md . appendMarkdown ( ' \n\n' ) ;
177+ md . appendMarkdown ( '**GitHub Copilot Usage**\n\nPlan: ' ) ;
178+ md . appendText ( data . plan ) ;
179+ md . appendMarkdown ( ` \n\nNo premium quota [$(graph)]( ${ BILLING_URL } )\n\n` ) ;
148180 if ( lastUpdatedAt ) md . appendMarkdown ( `Updated at ${ formatTimestamp ( lastUpdatedAt ) } ` ) ;
149- md . appendMarkdown ( `[$(refresh)](command:githubCopilotUsage.refresh) [$(graph)](${ BILLING_URL } )` ) ;
150- renderStatus ( { text : '—' , tooltip : md } ) ;
181+ md . appendMarkdown ( `[$(refresh)](command:githubCopilotUsage.refresh)` ) ;
182+ if ( isRateLimited ) md . appendMarkdown ( '\n\nRate limit \u00b7 data may be outdated' ) ;
183+ if ( isStale || isOffline ) md . appendMarkdown ( '\n\nOffline \u00b7 data may be outdated' ) ;
184+ renderStatus ( { text : `\u2014${ staleIcon } ` , tooltip : md } ) ;
151185 return ;
152186 }
153187
154188 if ( data . unlimited ) {
155- renderStatus ( { text : '∞' , tooltip : buildTooltip ( data , isRateLimited ) } ) ;
189+ renderStatus ( { text : `\u221e ${ staleIcon } ` , tooltip : buildTooltip ( data , isRateLimited , isOffline , isStale ) } ) ;
156190 return ;
157191 }
158192
@@ -166,23 +200,29 @@ function updateStatusBar(data, isRateLimited = false) {
166200 }
167201 }
168202
169- renderStatus ( { text : `${ pct } %` , tooltip : buildTooltip ( data , isRateLimited ) , color } ) ;
203+ renderStatus ( {
204+ text : `${ pct } %${ staleIcon } ` ,
205+ tooltip : buildTooltip ( data , isRateLimited , isOffline , isStale ) ,
206+ color,
207+ } ) ;
170208}
171209
172210/**
173211 * @param {import('./api').UsageData } data
174212 * @param {boolean } isRateLimited
213+ * @param {boolean } [isOfflineState]
214+ * @param {boolean } [isStale]
175215 * @returns {vscode.MarkdownString }
176216 */
177- function buildTooltip ( data , isRateLimited ) {
217+ function buildTooltip ( data , isRateLimited , isOfflineState = false , isStale = false ) {
178218 const md = new vscode . MarkdownString ( '' , true ) ;
179219 md . isTrusted = { enabledCommands : [ 'githubCopilotUsage.refresh' ] } ;
180220 md . appendMarkdown ( '**GitHub Copilot Usage**\n\nPlan: ' ) ;
181221 md . appendText ( data . plan ) ;
182222 md . appendMarkdown ( '\n\n' ) ;
183223
184224 if ( data . unlimited ) {
185- md . appendMarkdown ( ' Quota: Unlimited\n\n' ) ;
225+ md . appendMarkdown ( ` Quota: Unlimited [$(graph)]( ${ BILLING_URL } ) \n\n` ) ;
186226 } else {
187227 md . appendMarkdown ( `Used: ${ data . used } / ${ data . quota } (${ data . usedPct } %) [$(graph)](${ BILLING_URL } )\n\n` ) ;
188228 if ( data . overageEnabled && data . overageUsed > 0 ) {
@@ -200,12 +240,14 @@ function buildTooltip(data, isRateLimited) {
200240 md . appendMarkdown ( '\n\n' ) ;
201241 }
202242
203- if ( isRateLimited ) {
204- md . appendMarkdown ( '_(Rate limited — showing last known data)_\n\n' ) ;
205- }
206-
207243 if ( lastUpdatedAt ) md . appendMarkdown ( `Updated at ${ formatTimestamp ( lastUpdatedAt ) } ` ) ;
208244 md . appendMarkdown ( '[$(refresh)](command:githubCopilotUsage.refresh)' ) ;
245+ if ( isRateLimited ) {
246+ md . appendMarkdown ( '\n\nRate limit \u00b7 data may be outdated' ) ;
247+ }
248+ if ( isStale || isOfflineState ) {
249+ md . appendMarkdown ( '\n\nOffline \u00b7 data may be outdated' ) ;
250+ }
209251 return md ;
210252}
211253
@@ -292,4 +334,29 @@ function getConfig() {
292334 } ;
293335}
294336
295- module . exports = { activate, deactivate, formatTimestamp, getConfig, buildTooltip } ;
337+ /**
338+ * Pure helper: returns true only when offline for > 1 h.
339+ * @param {boolean } offline
340+ * @param {Date | null } since
341+ * @returns {boolean }
342+ */
343+ function computeIsStale ( offline , since ) {
344+ return offline && since !== null && Date . now ( ) - since . getTime ( ) > 60 * 60 * 1000 ;
345+ }
346+
347+ module . exports = {
348+ activate,
349+ deactivate,
350+ formatTimestamp,
351+ getConfig,
352+ buildTooltip,
353+ computeIsStale,
354+ // Test-only: inspect and mutate module-level state.
355+ _setState : ( s ) => {
356+ if ( 'isOffline' in s ) isOffline = s . isOffline ;
357+ if ( 'offlineSince' in s ) offlineSince = s . offlineSince ;
358+ if ( 'lastData' in s ) lastData = s . lastData ;
359+ if ( 'lastUpdatedAt' in s ) lastUpdatedAt = s . lastUpdatedAt ;
360+ } ,
361+ _getState : ( ) => ( { isOffline, offlineSince } ) ,
362+ } ;
0 commit comments