Skip to content

Commit ab9a6c5

Browse files
committed
Merge branch 'ethan/tbb-plan-usage' into ethan/tbb-notification
2 parents a3bc574 + 3116cfd commit ab9a6c5

13 files changed

Lines changed: 591 additions & 429 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: 24 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -3,120 +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 IntervalQuota immediateUsageInterval;
18-
private IntervalQuota extendedUsageInterval;
19-
private String resetDate;
20-
private CopilotPlan copilotPlan;
21-
22-
public Quota getChatQuota() {
23-
return chat;
24-
}
25-
26-
public void setChatQuota(Quota chat) {
27-
this.chat = chat;
28-
}
29-
30-
public Quota getCompletionsQuota() {
31-
return completions;
32-
}
33-
34-
public void setCompletionsQuota(Quota completions) {
35-
this.completions = completions;
36-
}
37-
38-
public Quota getPremiumInteractionsQuota() {
39-
return premiumInteractions;
40-
}
41-
42-
public void setPremiumInteractionsQuota(Quota premiumInteractions) {
43-
this.premiumInteractions = premiumInteractions;
44-
}
45-
46-
/**
47-
* Gets the immediate usage interval quota (for individual plans).
48-
*/
49-
public IntervalQuota getImmediateUsageInterval() {
50-
return immediateUsageInterval;
51-
}
52-
53-
public void setImmediateUsageInterval(IntervalQuota immediateUsageInterval) {
54-
this.immediateUsageInterval = immediateUsageInterval;
55-
}
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);
5628

5729
/**
58-
* Gets the extended usage interval quota (for individual plans).
30+
* Returns an empty {@link CheckQuotaResult} used as a placeholder before the language server
31+
* supplies real quota data.
5932
*/
60-
public IntervalQuota getExtendedUsageInterval() {
61-
return extendedUsageInterval;
62-
}
63-
64-
public void setExtendedUsageInterval(IntervalQuota extendedUsageInterval) {
65-
this.extendedUsageInterval = extendedUsageInterval;
66-
}
67-
68-
public String getResetDate() {
69-
return resetDate;
70-
}
71-
72-
public void setResetDate(String resetDate) {
73-
this.resetDate = resetDate;
74-
}
75-
76-
public CopilotPlan getCopilotPlan() {
77-
return copilotPlan;
78-
}
79-
80-
public void setCopilotPlan(CopilotPlan copilotPlan) {
81-
this.copilotPlan = copilotPlan;
82-
}
83-
84-
@Override
85-
public int hashCode() {
86-
return Objects.hash(chat, completions, copilotPlan, extendedUsageInterval,
87-
immediateUsageInterval, premiumInteractions, resetDate);
88-
}
89-
90-
@Override
91-
public boolean equals(Object obj) {
92-
if (this == obj) {
93-
return true;
94-
}
95-
if (obj == null) {
96-
return false;
97-
}
98-
if (getClass() != obj.getClass()) {
99-
return false;
100-
}
101-
CheckQuotaResult other = (CheckQuotaResult) obj;
102-
return Objects.equals(chat, other.chat) && Objects.equals(completions, other.completions)
103-
&& copilotPlan == other.copilotPlan
104-
&& Objects.equals(extendedUsageInterval, other.extendedUsageInterval)
105-
&& Objects.equals(immediateUsageInterval, other.immediateUsageInterval)
106-
&& Objects.equals(premiumInteractions, other.premiumInteractions)
107-
&& Objects.equals(resetDate, other.resetDate);
108-
}
109-
110-
@Override
111-
public String toString() {
112-
ToStringBuilder builder = new ToStringBuilder(this);
113-
builder.append("chat", chat);
114-
builder.append("completions", completions);
115-
builder.append("premiumInteractions", premiumInteractions);
116-
builder.append("immediateUsageInterval", immediateUsageInterval);
117-
builder.append("extendedUsageInterval", extendedUsageInterval);
118-
builder.append("resetDate", resetDate);
119-
builder.append("copilotPlan", copilotPlan);
120-
return builder.toString();
33+
public static CheckQuotaResult empty() {
34+
return EMPTY;
12135
}
12236
}

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

Lines changed: 36 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -5,125 +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-
private Integer entitlement;
18-
private Integer quotaRemaining;
19-
private String timeStamp;
20-
21-
/**
22-
* Creates a new CompletionsQuota quota information with default values.
23-
*/
24-
public Quota() {
25-
this.percentRemaining = 0.0;
26-
this.unlimited = false;
27-
this.overagePermitted = false;
28-
}
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) {
2933

3034
/**
31-
* 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.
3236
*/
33-
public double getPercentRemaining() {
37+
public Quota {
3438
if (percentRemaining < 0.0) {
35-
return 0.0;
39+
percentRemaining = 0.0;
3640
} else if (percentRemaining > 100.0) {
37-
return 100.0;
41+
percentRemaining = 100.0;
3842
}
39-
return percentRemaining;
40-
}
41-
42-
public void setPercentRemaining(double percentRemaining) {
43-
this.percentRemaining = percentRemaining;
44-
}
45-
46-
public boolean isUnlimited() {
47-
return unlimited;
48-
}
49-
50-
public void setUnlimited(boolean unlimited) {
51-
this.unlimited = unlimited;
52-
}
53-
54-
public boolean isOveragePermitted() {
55-
return overagePermitted;
56-
}
57-
58-
public void setOveragePermitted(boolean overagePermitted) {
59-
this.overagePermitted = overagePermitted;
60-
}
61-
62-
/**
63-
* Gets the total entitlement (quota limit).
64-
*/
65-
public Integer getEntitlement() {
66-
return entitlement;
67-
}
68-
69-
public void setEntitlement(Integer entitlement) {
70-
this.entitlement = entitlement;
71-
}
72-
73-
/**
74-
* Gets the remaining quota count.
75-
*/
76-
public Integer getQuotaRemaining() {
77-
return quotaRemaining;
78-
}
79-
80-
public void setQuotaRemaining(Integer quotaRemaining) {
81-
this.quotaRemaining = quotaRemaining;
82-
}
83-
84-
/**
85-
* Gets the timestamp of the quota snapshot.
86-
*/
87-
public String getTimeStamp() {
88-
return timeStamp;
89-
}
90-
91-
public void setTimeStamp(String timeStamp) {
92-
this.timeStamp = timeStamp;
93-
}
94-
95-
@Override
96-
public int hashCode() {
97-
return Objects.hash(entitlement, overagePermitted, percentRemaining, quotaRemaining, timeStamp, unlimited);
9843
}
9944

10045
@Override
10146
public boolean equals(Object obj) {
10247
if (this == obj) {
10348
return true;
10449
}
105-
if (obj == null) {
50+
if (!(obj instanceof Quota other)) {
10651
return false;
10752
}
108-
if (getClass() != obj.getClass()) {
109-
return false;
110-
}
111-
Quota other = (Quota) obj;
112-
return Objects.equals(entitlement, other.entitlement) && overagePermitted == other.overagePermitted
113-
&& Double.doubleToLongBits(percentRemaining) == Double.doubleToLongBits(other.percentRemaining)
114-
&& Objects.equals(quotaRemaining, other.quotaRemaining) && Objects.equals(timeStamp, other.timeStamp)
115-
&& 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;
11659
}
11760

11861
@Override
119-
public String toString() {
120-
ToStringBuilder builder = new ToStringBuilder(this);
121-
builder.append("percentRemaining", percentRemaining);
122-
builder.append("unlimited", unlimited);
123-
builder.append("overagePermitted", overagePermitted);
124-
builder.append("entitlement", entitlement);
125-
builder.append("quotaRemaining", quotaRemaining);
126-
builder.append("timeStamp", timeStamp);
127-
return builder.toString();
62+
public int hashCode() {
63+
return Objects.hash(percentRemaining, unlimited, overagePermitted, overageCount, entitlement, quotaRemaining);
12864
}
12965
}
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 & 5 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,14 +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-
|| userPlan == CopilotPlan.individual_max) {
228-
// Paid individual plan message
227+
if (MenuUtils.isCfiPlan(userPlan)) {
228+
// Pro, Pro+ and Max message
229229
errMsg = String.format(Messages.chat_noQuotaView_proProplusWarnMsg, fallbackModelName);
230230
} else if (userPlan == CopilotPlan.business || userPlan == CopilotPlan.enterprise) {
231231
// CE and CB message
@@ -236,7 +236,7 @@ public void processTurnEvent(ChatProgressValue value) {
236236
renderWarnMessageWithUpgradePlanButton(errMsg, value.getCode());
237237

238238
if (value.getCode() == 402
239-
&& this.serviceManager.getAuthStatusManager().getQuotaStatus().getCopilotPlan() != CopilotPlan.free) {
239+
&& this.serviceManager.getAuthStatusManager().getQuotaStatus().copilotPlan() != CopilotPlan.free) {
240240
this.serviceManager.getModelService().setFallBackModelAsActiveModel();
241241
this.serviceManager.getAuthStatusManager().checkQuota();
242242

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)