Skip to content

Commit 64f3ce4

Browse files
committed
feat(extension): render UBB credits and pool exhaustion in status bar
- Tooltip title switches between "Copilot Premium Requests" (legacy) and "Copilot Credits" (UBB) via `tooltipTitle()`. Body retains the familiar `Used: X / Y (Z%)` line; overage label adapts per mode ("Overage: N requests" vs "Additional credits: N"). - Pooled exhaustion: enterprise unlimited with `hasQuota=false` and no overage now displays `100%` red instead of misleading `∞`. Tooltip shows "Quota: Unlimited · pool exhausted" with the reset date. - noData branch unified into `buildTooltip` as a fourth state, so title and footer logic live in one place. - `appendResetLine` no-ops when resetDate is undefined, mirroring the upstream "hide the row" behavior. - Refresh's session-fetch error path simplified: catch sets session to null and falls through to a single `!session` handler.
1 parent d31c9e9 commit 64f3ce4

2 files changed

Lines changed: 167 additions & 31 deletions

File tree

src/extension.js

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,7 @@ async function refresh(promptSignIn = false, isManual = false) {
9191
});
9292
} catch {
9393
// User cancelled the sign-in prompt (createIfNone: true throws on cancel)
94-
isOffline = false;
95-
offlineSince = null;
96-
showNoAuth();
97-
return;
94+
session = null;
9895
}
9996

10097
if (!session) {
@@ -193,22 +190,27 @@ function updateStatusBar(data, isRateLimited = false) {
193190
const staleIcon = isStale ? " $(warning)" : "";
194191

195192
if (data.noData) {
196-
const md = new vscode.MarkdownString("", true);
197-
md.isTrusted = { enabledCommands: ["githubCopilotUsage.refresh"] };
198-
md.appendMarkdown("**GitHub Copilot Usage**\n\nPlan: ");
199-
md.appendText(data.plan);
200-
md.appendMarkdown(`\n\nNo premium quota  [$(graph)](${BILLING_URL})\n\n`);
201-
if (lastUpdatedAt) md.appendMarkdown(`Updated at ${formatTimestamp(lastUpdatedAt)} `);
202-
md.appendMarkdown(`[$(refresh)](command:githubCopilotUsage.refresh)`);
203-
if (isRateLimited) md.appendMarkdown("\n\nRate limit \u00b7 data may be outdated");
204-
if (isStale || isOffline) md.appendMarkdown("\n\nOffline \u00b7 data may be outdated");
205-
renderStatus({ text: `\u2014${staleIcon}`, tooltip: md });
193+
renderStatus({
194+
text: `—${staleIcon}`,
195+
tooltip: buildTooltip(data, isRateLimited, isOffline, isStale),
196+
});
197+
return;
198+
}
199+
200+
// Pooled exhaustion: enterprise unlimited plans signal "pool drained" via has_quota=false.
201+
// Without overage, the user effectively can't use Copilot — show 100% red instead of ∞.
202+
if (data.exhausted) {
203+
renderStatus({
204+
text: `100%${staleIcon}`,
205+
tooltip: buildTooltip(data, isRateLimited, isOffline, isStale),
206+
color: new vscode.ThemeColor("editorError.foreground"),
207+
});
206208
return;
207209
}
208210

209211
if (data.unlimited) {
210212
renderStatus({
211-
text: `\u221e${staleIcon}`,
213+
text: `${staleIcon}`,
212214
tooltip: buildTooltip(data, isRateLimited, isOffline, isStale),
213215
});
214216
return;
@@ -231,6 +233,36 @@ function updateStatusBar(data, isRateLimited = false) {
231233
});
232234
}
233235

236+
/**
237+
* Append a "Reset: …" line, or nothing if resetDate is unknown — mirrors
238+
* upstream's "hide the row when no source is available" behavior.
239+
* @param {vscode.MarkdownString} md
240+
* @param {Date | undefined} resetDate
241+
*/
242+
function appendResetLine(md, resetDate) {
243+
if (!resetDate) return;
244+
const resetStr = resetDate.toLocaleString(undefined, {
245+
year: "numeric",
246+
month: "short",
247+
day: "numeric",
248+
hour: "2-digit",
249+
minute: "2-digit",
250+
});
251+
md.appendMarkdown("Reset: ");
252+
md.appendText(resetStr);
253+
md.appendMarkdown("\n\n");
254+
}
255+
256+
/**
257+
* Mode-aware tooltip title — "Copilot Credits" under UBB, otherwise the
258+
* legacy "Copilot Premium Requests".
259+
* @param {boolean} tokenBasedBilling
260+
* @returns {string}
261+
*/
262+
function tooltipTitle(tokenBasedBilling) {
263+
return tokenBasedBilling ? "**Copilot Credits**" : "**Copilot Premium Requests**";
264+
}
265+
234266
/**
235267
* @param {import('./api').UsageData} data
236268
* @param {boolean} isRateLimited
@@ -241,38 +273,46 @@ function updateStatusBar(data, isRateLimited = false) {
241273
function buildTooltip(data, isRateLimited, isOfflineState = false, isStale = false) {
242274
const md = new vscode.MarkdownString("", true);
243275
md.isTrusted = { enabledCommands: ["githubCopilotUsage.refresh"] };
244-
md.appendMarkdown("**GitHub Copilot Usage**\n\nPlan: ");
276+
md.appendMarkdown(`${tooltipTitle(data.tokenBasedBilling)}\n\nPlan: `);
245277
md.appendText(data.plan);
246278
md.appendMarkdown("\n\n");
247279

248-
if (data.unlimited) {
280+
// Four quota-display states:
281+
if (data.noData) {
282+
// 1. No quota assigned: free/CFI plans without a premium counter
283+
// Title already names the unit (Credits vs Premium requests); body just states absence.
284+
md.appendMarkdown(`No quota assigned  [$(graph)](${BILLING_URL})\n\n`);
285+
appendResetLine(md, data.resetDate); // no-op when server provided no reset source
286+
} else if (data.exhausted) {
287+
// 2. Pool drained: hard error state, but show reset so user knows when access returns
288+
md.appendMarkdown(`Quota: Unlimited · pool exhausted  [$(graph)](${BILLING_URL})\n\n`);
289+
appendResetLine(md, data.resetDate);
290+
} else if (data.unlimited) {
291+
// 3. Plain unlimited: no counter, no reset
249292
md.appendMarkdown(`Quota: Unlimited  [$(graph)](${BILLING_URL})\n\n`);
250293
} else {
294+
// 4. Counted plan: show used/quota and (optional) overage.
295+
// Title already names the unit (Credits vs Premium requests), so the
296+
// count line uses a neutral "Used:" label and skips the unit suffix.
251297
md.appendMarkdown(
252298
`Used: ${data.used} / ${data.quota} (${data.usedPct}%)  [$(graph)](${BILLING_URL})\n\n`,
253299
);
254300
if (data.overageEnabled && data.overageUsed > 0) {
255-
md.appendMarkdown(`Overage: ${data.overageUsed} requests\n\n`);
301+
const overageLine = data.tokenBasedBilling
302+
? `Additional credits: ${data.overageUsed}`
303+
: `Overage: ${data.overageUsed} requests`;
304+
md.appendMarkdown(`${overageLine}\n\n`);
256305
}
257-
const resetStr = data.resetDate.toLocaleString(undefined, {
258-
year: "numeric",
259-
month: "short",
260-
day: "numeric",
261-
hour: "2-digit",
262-
minute: "2-digit",
263-
});
264-
md.appendMarkdown("Reset: ");
265-
md.appendText(resetStr);
266-
md.appendMarkdown("\n\n");
306+
appendResetLine(md, data.resetDate);
267307
}
268308

269309
if (lastUpdatedAt) md.appendMarkdown(`Updated at ${formatTimestamp(lastUpdatedAt)} `);
270310
md.appendMarkdown("[$(refresh)](command:githubCopilotUsage.refresh)");
271311
if (isRateLimited) {
272-
md.appendMarkdown("\n\nRate limit \u00b7 data may be outdated");
312+
md.appendMarkdown("\n\nRate limit · data may be outdated");
273313
}
274314
if (isStale || isOfflineState) {
275-
md.appendMarkdown("\n\nOffline \u00b7 data may be outdated");
315+
md.appendMarkdown("\n\nOffline · data may be outdated");
276316
}
277317
return md;
278318
}

tests/extension.test.js

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,26 +106,61 @@ describe("buildTooltip", () => {
106106
const BASE_DATA = {
107107
plan: "Pro",
108108
unlimited: false,
109+
hasQuota: true,
110+
exhausted: false,
109111
noData: false,
110112
used: 90,
111113
quota: 300,
112114
usedPct: 30,
113115
overageEnabled: false,
114116
overageUsed: 0,
115117
resetDate: new Date("2026-04-01T00:00:00Z"),
118+
tokenBasedBilling: false,
116119
};
117120

118121
it("shows plan name and used/quota/pct for normal quota", () => {
119122
const md = buildTooltip(BASE_DATA, false);
120123
expect(md.value).toContain("Pro");
121-
expect(md.value).toContain("90 / 300 (30%)");
124+
expect(md.value).toContain("Used: 90 / 300 (30%)");
125+
});
126+
127+
it("uses 'Copilot Premium Requests' title in legacy mode", () => {
128+
const md = buildTooltip(BASE_DATA, false);
129+
expect(md.value).toContain("**Copilot Premium Requests**");
130+
expect(md.value).not.toContain("**Copilot Credits**");
131+
});
132+
133+
it("uses 'Copilot Credits' title in token-based billing mode", () => {
134+
const md = buildTooltip({ ...BASE_DATA, tokenBasedBilling: true }, false);
135+
expect(md.value).toContain("**Copilot Credits**");
136+
expect(md.value).not.toContain("**Copilot Premium Requests**");
122137
});
123138

124139
it("shows Unlimited for unlimited plan", () => {
125140
const md = buildTooltip({ ...BASE_DATA, unlimited: true }, false);
126141
expect(md.value).toContain("Unlimited");
127142
});
128143

144+
it("shows 'pool exhausted' when data.exhausted is true", () => {
145+
const data = { ...BASE_DATA, unlimited: true, hasQuota: false, exhausted: true };
146+
const md = buildTooltip(data, false);
147+
expect(md.value).toContain("pool exhausted");
148+
expect(md.value).toContain("Reset:"); // pool-exhausted users need to see the reset date
149+
});
150+
151+
it("shows plain Unlimited when hasQuota=false but overage is enabled (exhausted=false)", () => {
152+
const data = {
153+
...BASE_DATA,
154+
unlimited: true,
155+
hasQuota: false,
156+
overageEnabled: true,
157+
exhausted: false,
158+
};
159+
const md = buildTooltip(data, false);
160+
expect(md.value).toContain("Quota: Unlimited");
161+
expect(md.value).not.toContain("pool exhausted");
162+
});
163+
129164
it("includes rate-limit notice when isRateLimited is true", () => {
130165
const md = buildTooltip(BASE_DATA, true);
131166
expect(md.value).toContain("Rate limit");
@@ -159,10 +194,22 @@ describe("buildTooltip", () => {
159194
expect(md.value).toContain("Overage: 5 requests");
160195
});
161196

197+
it("uses 'Additional credits' label for overage in UBB mode", () => {
198+
const data = { ...BASE_DATA, tokenBasedBilling: true, overageEnabled: true, overageUsed: 5 };
199+
const md = buildTooltip(data, false);
200+
expect(md.value).toContain("Additional credits: 5");
201+
expect(md.value).not.toContain("Overage:");
202+
});
203+
162204
it("omits overage section when overageUsed is 0", () => {
163205
const md = buildTooltip(BASE_DATA, false);
164206
expect(md.value).not.toContain("Overage");
165207
});
208+
209+
it("omits Reset row when resetDate is undefined", () => {
210+
const md = buildTooltip({ ...BASE_DATA, resetDate: undefined }, false);
211+
expect(md.value).not.toContain("Reset:");
212+
});
166213
});
167214

168215
// ---------------------------------------------------------------------------
@@ -389,13 +436,16 @@ const API_BODY = {
389436
const BASE_USAGE = {
390437
plan: "Pro",
391438
unlimited: false,
439+
hasQuota: true,
440+
exhausted: false,
392441
noData: false,
393442
used: 90,
394443
quota: 300,
395444
usedPct: 30,
396445
overageEnabled: false,
397446
overageUsed: 0,
398447
resetDate: new Date("2026-04-01T00:00:00Z"),
448+
tokenBasedBilling: false,
399449
};
400450

401451
/** Creates a minimal mock ExtensionContext. */
@@ -569,6 +619,7 @@ describe("refresh", () => {
569619
deactivate();
570620
restoreVscodeMock();
571621
vi.unstubAllGlobals();
622+
vi.useRealTimers();
572623
resetAllState();
573624
_clearRecoveryTimer();
574625
_clearTimer();
@@ -692,12 +743,57 @@ describe("updateStatusBar", () => {
692743
expect(barItem.text).toBe("\u2014");
693744
});
694745

746+
it("noData tooltip shows reset date when server provided one", async () => {
747+
await activate(makeContext());
748+
_updateStatusBar({
749+
...BASE_USAGE,
750+
noData: true,
751+
resetDate: new Date("2026-07-01T00:00:00Z"),
752+
});
753+
expect(barItem.tooltip.value).toContain("Reset:");
754+
});
755+
756+
it("noData tooltip omits reset row when resetDate is undefined", async () => {
757+
await activate(makeContext());
758+
_updateStatusBar({ ...BASE_USAGE, noData: true, resetDate: undefined });
759+
expect(barItem.tooltip.value).not.toContain("Reset:");
760+
});
761+
762+
it("noData tooltip uses 'Copilot Credits' title in UBB mode", async () => {
763+
// Regression guard for the simplification that folded the noData branch into
764+
// buildTooltip — the title must still switch on tokenBasedBilling.
765+
await activate(makeContext());
766+
_updateStatusBar({ ...BASE_USAGE, noData: true, tokenBasedBilling: true });
767+
expect(barItem.tooltip.value).toContain("**Copilot Credits**");
768+
expect(barItem.tooltip.value).not.toContain("**Copilot Premium Requests**");
769+
});
770+
695771
it("renders infinity symbol for unlimited plan", async () => {
696772
await activate(makeContext());
697773
_updateStatusBar({ ...BASE_USAGE, unlimited: true });
698774
expect(barItem.text).toBe("\u221e");
699775
});
700776

777+
it("renders 100% red when data.exhausted is true (pooled entitlement drained)", async () => {
778+
await activate(makeContext());
779+
_updateStatusBar({ ...BASE_USAGE, unlimited: true, hasQuota: false, exhausted: true });
780+
expect(barItem.text).toBe("100%");
781+
expect(barItem.color).toBeDefined();
782+
expect(barItem.color.id).toBe("editorError.foreground");
783+
});
784+
785+
it("still renders \u221e when pooled hasQuota=false but overage is enabled (exhausted=false)", async () => {
786+
await activate(makeContext());
787+
_updateStatusBar({
788+
...BASE_USAGE,
789+
unlimited: true,
790+
hasQuota: false,
791+
overageEnabled: true,
792+
exhausted: false,
793+
});
794+
expect(barItem.text).toBe("\u221e");
795+
});
796+
701797
it("renders percentage for normal quota", async () => {
702798
await activate(makeContext());
703799
_updateStatusBar({ ...BASE_USAGE, usedPct: 42 });

0 commit comments

Comments
 (0)