@@ -226,6 +226,135 @@ local function get_models(self, opts)
226226 return models
227227end
228228
229+ --- Get Copilot usage statistics
230+ --- @return table | nil
231+ local function get_copilot_stats ()
232+ local dummy_adapter = { url = " " }
233+ if not get_and_authorize_token (dummy_adapter ) then
234+ return nil
235+ end
236+
237+ log :debug (" Fetching Copilot usage statistics" )
238+
239+ local ok , response = pcall (function ()
240+ return curl .get (" https://api.github.com/copilot_internal/user" , {
241+ sync = true ,
242+ headers = {
243+ Authorization = " Bearer " .. _oauth_token ,
244+ Accept = " */*" ,
245+ [" User-Agent" ] = " CodeCompanion.nvim" ,
246+ },
247+ insecure = config .adapters .opts .allow_insecure ,
248+ proxy = config .adapters .opts .proxy ,
249+ })
250+ end )
251+ if not ok then
252+ log :error (" Could not get Copilot stats: %s" , response )
253+ return nil
254+ end
255+
256+ local ok , json = pcall (vim .json .decode , response .body )
257+ if not ok then
258+ log :error (" Error parsing Copilot stats response: %s" , response .body )
259+ return nil
260+ end
261+
262+ return json
263+ end
264+
265+ --- Show Copilot usage statistics in a floating window
266+ --- @return nil
267+ local function show_copilot_stats ()
268+ local stats = get_copilot_stats ()
269+ if not stats then
270+ return vim .notify (" Could not retrieve Copilot stats" , vim .log .levels .ERROR )
271+ end
272+
273+ local lines = {}
274+ local ui = require (" codecompanion.utils.ui" )
275+ table.insert (lines , " # GitHub Copilot Usage Statistics " )
276+ table.insert (lines , " " )
277+
278+ if stats .quota_snapshots .premium_interactions then
279+ local premium = stats .quota_snapshots .premium_interactions
280+ table.insert (lines , " ## Premium Interactions" )
281+ local used = premium .entitlement - premium .remaining
282+ local usage_percent = premium .entitlement > 0 and (used / premium .entitlement * 100 ) or 0
283+ table.insert (lines , string.format (" - Used: %d / %d (%.1f%%)" , used , premium .entitlement , usage_percent ))
284+ table.insert (lines , string.format (" - Remaining: %d" , premium .remaining ))
285+ table.insert (lines , string.format (" - Percentage: %.1f%%" , premium .percent_remaining ))
286+ if premium .unlimited then
287+ table.insert (lines , " - Status: Unlimited ✨" )
288+ else
289+ table.insert (lines , " - Status: Limited" )
290+ end
291+ table.insert (lines , " " )
292+ end
293+
294+ if stats .quota_snapshots .chat then
295+ local chat = stats .quota_snapshots .chat
296+ table.insert (lines , " ## Chat" )
297+ if chat .unlimited then
298+ table.insert (lines , " - Status: Unlimited ✨" )
299+ else
300+ local used = chat .entitlement - chat .remaining
301+ local usage_percent = chat .entitlement > 0 and (used / chat .entitlement * 100 ) or 0
302+ table.insert (lines , string.format (" - Used: %d / %d (%.1f%%)" , used , chat .entitlement , usage_percent ))
303+ end
304+ table.insert (lines , " " )
305+ end
306+
307+ if stats .quota_snapshots .completions then
308+ local completions = stats .quota_snapshots .completions
309+ table.insert (lines , " ## Completions" )
310+ if completions .unlimited then
311+ table.insert (lines , " - Status: Unlimited ✨" )
312+ else
313+ local used = completions .entitlement - completions .remaining
314+ local usage_percent = completions .entitlement > 0 and (used / completions .entitlement * 100 ) or 0
315+ table.insert (lines , string.format (" - Used: %d / %d (%.1f%%)" , used , completions .entitlement , usage_percent ))
316+ end
317+ end
318+ if stats .quota_reset_date then
319+ table.insert (lines , " " )
320+ table.insert (lines , string.format (" > Quota resets on: %s" , stats .quota_reset_date ))
321+ table.insert (lines , " " )
322+ end
323+
324+ -- Create floating window
325+ local float_opts = {
326+ title = " Copilot Stats" ,
327+ lock = true ,
328+ relative = " editor" ,
329+ row = " center" ,
330+ col = " center" ,
331+ window = {
332+ width = 43 ,
333+ height = math.min (# lines + 2 , 20 ),
334+ },
335+ ignore_keymaps = false ,
336+ }
337+ local _ , winnr = ui .create_float (lines , float_opts )
338+
339+ local function get_usage_highlight (usage_percent )
340+ if usage_percent >= 80 then
341+ return " Error"
342+ else
343+ return " MoreMsg"
344+ end
345+ end
346+ vim .api .nvim_win_call (winnr , function ()
347+ -- Usage percentages with color coding
348+ local premium = stats .quota_snapshots .premium_interactions
349+ if premium and not premium .unlimited then
350+ local used = premium .entitlement - premium .remaining
351+ local usage_percent = premium .entitlement > 0 and (used / premium .entitlement * 100 ) or 0
352+ local highlight = get_usage_highlight (usage_percent )
353+ vim .fn .matchadd (highlight , string.format (" - Used: %d / %d (%.1f%%)" , used , premium .entitlement , usage_percent ))
354+ end
355+ end )
356+ end
357+
229358--- @class Copilot.Adapter : CodeCompanion.Adapter
230359return {
231360 name = " copilot" ,
@@ -256,6 +385,12 @@ return {
256385 [" Copilot-Integration-Id" ] = " vscode-chat" ,
257386 [" Editor-Version" ] = " Neovim/" .. vim .version ().major .. " ." .. vim .version ().minor .. " ." .. vim .version ().patch ,
258387 },
388+ get_copilot_stats = function ()
389+ return get_copilot_stats ()
390+ end ,
391+ show_copilot_stats = function ()
392+ return show_copilot_stats ()
393+ end ,
259394 handlers = {
260395 --- Check for a token before starting the request
261396 --- @param self CodeCompanion.Adapter
0 commit comments