Skip to content

Commit 63b3c1b

Browse files
authored
refactor: Update quota usage menu and UI updates (#172)
1 parent b303976 commit 63b3c1b

16 files changed

Lines changed: 817 additions & 304 deletions

File tree

com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/AuthStatusManagerTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ void testSignInConfirm() throws InterruptedException, ExecutionException {
5555
when(mockResult.getUser()).thenReturn(mockedUser);
5656
when(mockResult.getStatus()).thenReturn(CopilotStatusResult.OK);
5757
when(mockConnection.signInConfirm(userCode)).thenReturn(CompletableFuture.completedFuture(mockResult));
58-
when(mockConnection.checkQuota()).thenReturn(CompletableFuture.completedFuture(new CheckQuotaResult()));
58+
when(mockConnection.checkQuota()).thenReturn(CompletableFuture.completedFuture(CheckQuotaResult.empty()));
5959

6060
CopilotStatusResult result = authStatusManager.signInConfirm(userCode);
6161

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/AuthStatusManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public void setQuotaStatus(CheckQuotaResult checkQuotaResult) {
182182
*/
183183
public CheckQuotaResult getQuotaStatus() {
184184
if (this.checkQuotaResult == null) {
185-
this.checkQuotaResult = new CheckQuotaResult();
185+
this.checkQuotaResult = CheckQuotaResult.empty();
186186
}
187187
return this.checkQuotaResult;
188188
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/quota/CheckQuotaResult.java

Lines changed: 27 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,90 +3,34 @@
33

44
package com.microsoft.copilot.eclipse.core.lsp.protocol.quota;
55

6-
import java.util.Objects;
7-
8-
import org.apache.commons.lang3.builder.ToStringBuilder;
9-
106
/**
11-
* Result of the checkQuota request.
7+
* Result of the {@code checkQuota} request.
8+
*
9+
* @param chat chat quota snapshot
10+
* @param completions completions quota snapshot
11+
* @param premiumInteractions premium interactions quota snapshot
12+
* @param resetDate ISO-8601 local date when the monthly allowance resets, or {@code null}
13+
* @param resetDateUtc ISO-8601 instant when the monthly allowance resets in UTC, or {@code null}
14+
* @param copilotPlan the user's Copilot plan
15+
* @param tokenBasedBillingEnabled whether the user's billing is token-based
1216
*/
13-
public class CheckQuotaResult {
14-
private Quota chat;
15-
private Quota completions;
16-
private Quota premiumInteractions;
17-
private String resetDate;
18-
private CopilotPlan copilotPlan;
19-
20-
public Quota getChatQuota() {
21-
return chat;
22-
}
23-
24-
public void setChatQuota(Quota chat) {
25-
this.chat = chat;
26-
}
27-
28-
public Quota getCompletionsQuota() {
29-
return completions;
30-
}
31-
32-
public void setCompletionsQuota(Quota completions) {
33-
this.completions = completions;
34-
}
35-
36-
public Quota getPremiumInteractionsQuota() {
37-
return premiumInteractions;
38-
}
39-
40-
public void setPremiumInteractionsQuota(Quota premiumInteractions) {
41-
this.premiumInteractions = premiumInteractions;
42-
}
43-
44-
public String getResetDate() {
45-
return resetDate;
46-
}
47-
48-
public void setResetDate(String resetDate) {
49-
this.resetDate = resetDate;
50-
}
51-
52-
public CopilotPlan getCopilotPlan() {
53-
return copilotPlan;
54-
}
55-
56-
public void setCopilotPlan(CopilotPlan copilotPlan) {
57-
this.copilotPlan = copilotPlan;
58-
}
59-
60-
@Override
61-
public int hashCode() {
62-
return Objects.hash(chat, completions, copilotPlan, premiumInteractions, resetDate);
63-
}
64-
65-
@Override
66-
public boolean equals(Object obj) {
67-
if (this == obj) {
68-
return true;
69-
}
70-
if (obj == null) {
71-
return false;
72-
}
73-
if (getClass() != obj.getClass()) {
74-
return false;
75-
}
76-
CheckQuotaResult other = (CheckQuotaResult) obj;
77-
return Objects.equals(chat, other.chat) && Objects.equals(completions, other.completions)
78-
&& copilotPlan == other.copilotPlan && Objects.equals(premiumInteractions, other.premiumInteractions)
79-
&& Objects.equals(resetDate, other.resetDate);
80-
}
81-
82-
@Override
83-
public String toString() {
84-
ToStringBuilder builder = new ToStringBuilder(this);
85-
builder.append("chat", chat);
86-
builder.append("completions", completions);
87-
builder.append("premiumInteractions", premiumInteractions);
88-
builder.append("resetDate", resetDate);
89-
builder.append("copilotPlan", copilotPlan);
90-
return builder.toString();
17+
public record CheckQuotaResult(
18+
Quota chat,
19+
Quota completions,
20+
Quota premiumInteractions,
21+
String resetDate,
22+
String resetDateUtc,
23+
CopilotPlan copilotPlan,
24+
boolean tokenBasedBillingEnabled) {
25+
26+
private static final CheckQuotaResult EMPTY =
27+
new CheckQuotaResult(null, null, null, null, null, null, false);
28+
29+
/**
30+
* Returns an empty {@link CheckQuotaResult} used as a placeholder before the language server
31+
* supplies real quota data.
32+
*/
33+
public static CheckQuotaResult empty() {
34+
return EMPTY;
9135
}
9236
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/quota/CopilotPlan.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
* Enum representing the different Copilot plans.
88
*/
99
public enum CopilotPlan {
10-
free, individual, individual_pro, business, enterprise
10+
free, individual, individual_pro, individual_max, business, enterprise
1111
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/quota/Quota.java

Lines changed: 36 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,85 +5,61 @@
55

66
import java.util.Objects;
77

8-
import org.apache.commons.lang3.builder.ToStringBuilder;
9-
108
/**
11-
* Completions quota information.
9+
* Quota information for a single tracked category (chat, completions, or premium interactions).
10+
*
11+
* <p>Equality intentionally excludes {@link #timeStamp} so that two snapshots with the same
12+
* display-meaningful state compare equal even when the language server stamps a different
13+
* production time on each refresh.
14+
*
15+
* @param percentRemaining percentage of the quota remaining; clamped into {@code [0.0, 100.0]} by
16+
* the accessor since the language server may report drift slightly outside that range
17+
* @param unlimited whether this category has no monthly limit
18+
* @param overagePermitted whether the user has enabled additional paid usage beyond the allowance
19+
* @param overageCount additional paid units already consumed, when reported
20+
* @param entitlement total monthly allowance, when reported
21+
* @param quotaRemaining absolute units remaining in the monthly allowance, when reported
22+
* @param timeStamp ISO-8601 timestamp of when the snapshot was produced by the language server;
23+
* not part of {@link #equals(Object)} / {@link #hashCode()}
1224
*/
13-
public class Quota {
14-
private double percentRemaining;
15-
private boolean unlimited;
16-
private boolean overagePermitted;
17-
18-
/**
19-
* Creates a new CompletionsQuota quota information with default values.
20-
*/
21-
public Quota() {
22-
this.percentRemaining = 0.0;
23-
this.unlimited = false;
24-
this.overagePermitted = false;
25-
}
25+
public record Quota(
26+
double percentRemaining,
27+
boolean unlimited,
28+
boolean overagePermitted,
29+
double overageCount,
30+
double entitlement,
31+
double quotaRemaining,
32+
String timeStamp) {
2633

2734
/**
28-
* Gets the percentage of the quota remaining within the range of 0.0 to 100.0.
35+
* Returns the percentage of the quota remaining, clamped into the {@code [0.0, 100.0]} range.
2936
*/
30-
public double getPercentRemaining() {
37+
public Quota {
3138
if (percentRemaining < 0.0) {
32-
return 0.0;
39+
percentRemaining = 0.0;
3340
} else if (percentRemaining > 100.0) {
34-
return 100.0;
41+
percentRemaining = 100.0;
3542
}
36-
return percentRemaining;
37-
}
38-
39-
public void setPercentRemaining(double percentRemaining) {
40-
this.percentRemaining = percentRemaining;
41-
}
42-
43-
public boolean isUnlimited() {
44-
return unlimited;
45-
}
46-
47-
public void setUnlimited(boolean unlimited) {
48-
this.unlimited = unlimited;
49-
}
50-
51-
public boolean isOveragePermitted() {
52-
return overagePermitted;
53-
}
54-
55-
public void setOveragePermitted(boolean overagePermitted) {
56-
this.overagePermitted = overagePermitted;
57-
}
58-
59-
@Override
60-
public int hashCode() {
61-
return Objects.hash(overagePermitted, percentRemaining, unlimited);
6243
}
6344

6445
@Override
6546
public boolean equals(Object obj) {
6647
if (this == obj) {
6748
return true;
6849
}
69-
if (obj == null) {
50+
if (!(obj instanceof Quota other)) {
7051
return false;
7152
}
72-
if (getClass() != obj.getClass()) {
73-
return false;
74-
}
75-
Quota other = (Quota) obj;
76-
return overagePermitted == other.overagePermitted
77-
&& Double.doubleToLongBits(percentRemaining) == Double.doubleToLongBits(other.percentRemaining)
78-
&& unlimited == other.unlimited;
53+
return Double.compare(percentRemaining, other.percentRemaining) == 0
54+
&& unlimited == other.unlimited
55+
&& overagePermitted == other.overagePermitted
56+
&& Double.compare(overageCount, other.overageCount) == 0
57+
&& Double.compare(entitlement, other.entitlement) == 0
58+
&& Double.compare(quotaRemaining, other.quotaRemaining) == 0;
7959
}
8060

8161
@Override
82-
public String toString() {
83-
ToStringBuilder builder = new ToStringBuilder(this);
84-
builder.append("percentRemaining", percentRemaining);
85-
builder.append("unlimited", unlimited);
86-
builder.append("overagePermitted", overagePermitted);
87-
return builder.toString();
62+
public int hashCode() {
63+
return Objects.hash(percentRemaining, unlimited, overagePermitted, overageCount, entitlement, quotaRemaining);
8864
}
8965
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package com.microsoft.copilot.eclipse.core.lsp.protocol.quota;
5+
6+
/**
7+
* Snapshot of a single quota bucket (chat, completions, or premium interactions) shipped with
8+
* {@code copilot/quotaChange} and {@code copilot/quotaWarning} notifications.
9+
*
10+
* @param quota total entitlement
11+
* @param used computed amount used (entitlement * (1 - percentRemaining / 100))
12+
* @param percentRemaining percentage of the quota remaining (0-100)
13+
* @param overageUsed overage amount consumed
14+
* @param overageEnabled whether overages are permitted
15+
* @param resetDate ISO 8601 timestamp when the quota resets, or empty when unknown
16+
* @param unlimited true when the quota is unlimited
17+
*/
18+
public record QuotaSnapshotParams(double quota, double used, double percentRemaining, double overageUsed,
19+
boolean overageEnabled, String resetDate, boolean unlimited) {
20+
}

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.microsoft.copilot.eclipse.ui.chat.services.TodoListService;
3939
import com.microsoft.copilot.eclipse.ui.i18n.Messages;
4040
import com.microsoft.copilot.eclipse.ui.swt.CssConstants;
41+
import com.microsoft.copilot.eclipse.ui.utils.MenuUtils;
4142
import com.microsoft.copilot.eclipse.ui.utils.SwtUtils;
4243

4344
/**
@@ -218,13 +219,13 @@ public void processTurnEvent(ChatProgressValue value) {
218219
if (StringUtils.isNotEmpty(errMsg)) {
219220
// TODO: remove this error message replacement if statement when the CLS side warn message is aligned.
220221
if (value.getCode() == 402) {
221-
CopilotPlan userPlan = this.serviceManager.getAuthStatusManager().getQuotaStatus().getCopilotPlan();
222+
CopilotPlan userPlan = this.serviceManager.getAuthStatusManager().getQuotaStatus().copilotPlan();
222223
CopilotModel fallbackModel = this.serviceManager.getModelService().getFallbackModel();
223224
String fallbackModelName = fallbackModel != null ? fallbackModel.getModelName()
224225
: Messages.chat_noQuotaView_fallbackModel;
225226

226-
if (userPlan == CopilotPlan.individual || userPlan == CopilotPlan.individual_pro) {
227-
// Pro and Pro+ message
227+
if (MenuUtils.isCfiPlan(userPlan)) {
228+
// Pro, Pro+ and Max message
228229
errMsg = String.format(Messages.chat_noQuotaView_proProplusWarnMsg, fallbackModelName);
229230
} else if (userPlan == CopilotPlan.business || userPlan == CopilotPlan.enterprise) {
230231
// CE and CB message
@@ -235,7 +236,7 @@ public void processTurnEvent(ChatProgressValue value) {
235236
renderWarnMessageWithUpgradePlanButton(errMsg, value.getCode());
236237

237238
if (value.getCode() == 402
238-
&& this.serviceManager.getAuthStatusManager().getQuotaStatus().getCopilotPlan() != CopilotPlan.free) {
239+
&& this.serviceManager.getAuthStatusManager().getQuotaStatus().copilotPlan() != CopilotPlan.free) {
239240
this.serviceManager.getModelService().setFallBackModelAsActiveModel();
240241
this.serviceManager.getAuthStatusManager().checkQuota();
241242

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/CopilotTurnWidget.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.eclipse.swt.widgets.Composite;
1313
import org.eclipse.swt.widgets.Label;
1414

15+
import com.microsoft.copilot.eclipse.core.CopilotCore;
1516
import com.microsoft.copilot.eclipse.ui.chat.services.AvatarService;
1617
import com.microsoft.copilot.eclipse.ui.chat.services.ChatServiceManager;
1718
import com.microsoft.copilot.eclipse.ui.i18n.Messages;
@@ -72,8 +73,19 @@ public void renderModelInfo(String modelName, double billingMultiplier) {
7273
}
7374
if (StringUtils.isNotBlank(modelName)) {
7475
Label modelInfoLabel = new Label(footer, SWT.NONE);
75-
String formattedMultiplier = ModelUtils.formatBillingMultiplier(billingMultiplier);
76-
String displayText = String.format("%s - %s", modelName, formattedMultiplier);
76+
// When token-based billing is enabled on the language server, the per-turn billing
77+
// multiplier is no longer a meaningful price signal, so render the model name on its
78+
// own. Fall back to the legacy "{model} - {multiplier}" format otherwise.
79+
boolean tbbEnabled = CopilotCore.getPlugin().getAuthStatusManager()
80+
.getQuotaStatus().tokenBasedBillingEnabled();
81+
String displayText;
82+
if (tbbEnabled) {
83+
displayText = modelName;
84+
} else {
85+
// TODO: Remove this legacy fallback after TBB is officially released.
86+
String formattedMultiplier = ModelUtils.formatBillingMultiplier(billingMultiplier);
87+
displayText = String.format("%s - %s", modelName, formattedMultiplier);
88+
}
7789
modelInfoLabel.setText(displayText);
7890
GridData labelGridData = new GridData(SWT.RIGHT, SWT.CENTER, true, false);
7991
modelInfoLabel.setLayoutData(labelGridData);

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ public void bindModelPicker(final DropdownButton picker) {
424424
}, (Map<String, CopilotModel> modelMap) -> {
425425
if (!picker.isDisposed()) {
426426
boolean showAddPremiumModelOption = this.authStatusManager.getQuotaStatus()
427-
.getCopilotPlan() == CopilotPlan.free;
427+
.copilotPlan() == CopilotPlan.free;
428428
// TODO: need to remove this logic after group policy is available
429429
FeatureFlags flags = CopilotCore.getPlugin().getFeatureFlags();
430430
boolean showByokManageOption = flags == null || flags.isByokEnabled();

0 commit comments

Comments
 (0)