From b90578d8dabbeaadaaf57e5032b523aaceb34261 Mon Sep 17 00:00:00 2001 From: David Longman Date: Sun, 22 Mar 2026 14:35:59 -0600 Subject: [PATCH] feat(copilot): show raw request counts for premium interactions Add a 'Requests Used' text line (e.g. '60 / 300') to the Copilot plugin detail view, giving users visibility into exact premium request consumption alongside the existing percentage progress bar. - Compute used = entitlement - remaining from premium_interactions snapshot - Guard against missing/invalid fields and clamp negative values to 0 - Scope to detail view in plugin.json to keep overview uncluttered - Paid tier only; free tier is unaffected - Add 4 tests covering presence, absence, clamping, and free tier exclusion - Update docs/providers/copilot.md with new line entry --- docs/providers/copilot.md | 11 ++--- plugins/copilot/plugin.js | 10 +++++ plugins/copilot/plugin.json | 1 + plugins/copilot/plugin.test.js | 76 ++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/docs/providers/copilot.md b/docs/providers/copilot.md index 5cf0354f..2711587e 100644 --- a/docs/providers/copilot.md +++ b/docs/providers/copilot.md @@ -83,11 +83,12 @@ X-Github-Api-Version: 2025-04-01 ## Displayed Lines -| Line | Tier | Description | -|--------------|------|------------------------------------------| -| Premium | Paid | Premium interactions remaining (percent) | -| Chat | Both | Chat messages remaining | -| Completions | Free | Code completions remaining | +| Line | Tier | Description | +|----------------|------|------------------------------------------------------| +| Premium | Paid | Premium interactions remaining (percent) | +| Requests Used | Paid | Premium requests used vs. entitlement (e.g. 60 / 300)| +| Chat | Both | Chat messages remaining | +| Completions | Free | Code completions remaining | All progress lines include: - `resetsAt` — ISO timestamp of next quota reset diff --git a/plugins/copilot/plugin.js b/plugins/copilot/plugin.js index 3d67f489..3ae46860 100644 --- a/plugins/copilot/plugin.js +++ b/plugins/copilot/plugin.js @@ -225,6 +225,16 @@ ); if (premiumLine) lines.push(premiumLine); + // Show raw request counts for premium interactions + const pi = snapshots.premium_interactions; + if (pi && typeof pi.entitlement === "number" && typeof pi.remaining === "number" && pi.entitlement > 0) { + var used = pi.entitlement - pi.remaining; + lines.push(ctx.line.text({ + label: "Requests Used", + value: String(Math.max(0, used)) + " / " + String(pi.entitlement), + })); + } + const chatLine = makeProgressLine( ctx, "Chat", diff --git a/plugins/copilot/plugin.json b/plugins/copilot/plugin.json index 2c322139..0ac3abc6 100644 --- a/plugins/copilot/plugin.json +++ b/plugins/copilot/plugin.json @@ -8,6 +8,7 @@ "brandColor": "#A855F7", "lines": [ { "type": "progress", "label": "Premium", "scope": "overview", "primaryOrder": 1 }, + { "type": "text", "label": "Requests Used", "scope": "detail" }, { "type": "progress", "label": "Chat", "scope": "overview", "primaryOrder": 2 }, { "type": "progress", "label": "Completions", "scope": "overview" } ] diff --git a/plugins/copilot/plugin.test.js b/plugins/copilot/plugin.test.js index a7f94e80..12f32a38 100644 --- a/plugins/copilot/plugin.test.js +++ b/plugins/copilot/plugin.test.js @@ -179,6 +179,82 @@ describe("copilot plugin", () => { expect(chat.used).toBe(5); // 100 - 95 }); + it("shows Requests Used text line for paid tier", async () => { + const ctx = makePluginTestContext(); + setKeychainToken(ctx, "tok"); + mockUsageOk(ctx); + const plugin = await loadPlugin(); + const result = plugin.probe(ctx); + const reqLine = result.lines.find((l) => l.label === "Requests Used"); + expect(reqLine).toBeTruthy(); + expect(reqLine.type).toBe("text"); + expect(reqLine.value).toBe("60 / 300"); // 300 - 240 = 60 + }); + + it("omits Requests Used when entitlement/remaining are missing", async () => { + const ctx = makePluginTestContext(); + setKeychainToken(ctx, "tok"); + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify( + makeUsageResponse({ + quota_snapshots: { + premium_interactions: { + percent_remaining: 80, + quota_id: "premium", + }, + }, + }), + ), + }); + const plugin = await loadPlugin(); + const result = plugin.probe(ctx); + expect(result.lines.find((l) => l.label === "Requests Used")).toBeFalsy(); + }); + + it("clamps Requests Used to 0 when remaining exceeds entitlement", async () => { + const ctx = makePluginTestContext(); + setKeychainToken(ctx, "tok"); + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify( + makeUsageResponse({ + quota_snapshots: { + premium_interactions: { + percent_remaining: 120, + entitlement: 300, + remaining: 360, + quota_id: "premium", + }, + }, + }), + ), + }); + const plugin = await loadPlugin(); + const result = plugin.probe(ctx); + const reqLine = result.lines.find((l) => l.label === "Requests Used"); + expect(reqLine).toBeTruthy(); + expect(reqLine.value).toBe("0 / 300"); + }); + + it("omits Requests Used for free tier", async () => { + const ctx = makePluginTestContext(); + setKeychainToken(ctx, "tok"); + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify({ + copilot_plan: "individual", + access_type_sku: "free_limited_copilot", + limited_user_quotas: { chat: 410, completions: 4000 }, + monthly_quotas: { chat: 500, completions: 4000 }, + limited_user_reset_date: "2026-02-11", + }), + }); + const plugin = await loadPlugin(); + const result = plugin.probe(ctx); + expect(result.lines.find((l) => l.label === "Requests Used")).toBeFalsy(); + }); + it("renders only Premium when Chat is missing", async () => { const ctx = makePluginTestContext(); setKeychainToken(ctx, "tok");