Skip to content

Commit 47283f3

Browse files
authored
feat: Add canUpgradePlan field to quota-related classes to guard upgrade plan button. (#212)
1 parent 96a4120 commit 47283f3

10 files changed

Lines changed: 85 additions & 35 deletions

File tree

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
* @param resetDateUtc ISO-8601 instant when the monthly allowance resets in UTC, or {@code null}
1414
* @param copilotPlan the user's Copilot plan
1515
* @param tokenBasedBillingEnabled whether the user's billing is token-based
16+
* @param canUpgradePlan whether the user is eligible to upgrade their Copilot plan; {@code null} when the language
17+
* server has not supplied this field, in which case callers should fall back to plan-based defaults
1618
*/
1719
public record CheckQuotaResult(
1820
Quota chat,
@@ -21,10 +23,11 @@ public record CheckQuotaResult(
2123
String resetDate,
2224
String resetDateUtc,
2325
CopilotPlan copilotPlan,
24-
boolean tokenBasedBillingEnabled) {
26+
boolean tokenBasedBillingEnabled,
27+
Boolean canUpgradePlan) {
2528

2629
private static final CheckQuotaResult EMPTY =
27-
new CheckQuotaResult(null, null, null, null, null, null, false);
30+
new CheckQuotaResult(null, null, null, null, null, null, false, null);
2831

2932
/**
3033
* Returns an empty {@link CheckQuotaResult} used as a placeholder before the language server

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
* @param completions current completions quota snapshot, when available
1717
* @param premiumInteractions current premium interactions quota snapshot, when available
1818
* @param copilotPlan the user's Copilot plan
19+
* @param canUpgradePlan whether the user is eligible to upgrade their Copilot plan; {@code null} when the language
20+
* server has not supplied this field, in which case callers should fall back to plan-based defaults
1921
*/
2022
public record QuotaWarningParams(String title, String message, String severity, QuotaSnapshotParams chat,
2123
QuotaSnapshotParams completions,
22-
@SerializedName("premium_interactions") QuotaSnapshotParams premiumInteractions, CopilotPlan copilotPlan) {
24+
@SerializedName("premium_interactions") QuotaSnapshotParams premiumInteractions, CopilotPlan copilotPlan,
25+
Boolean canUpgradePlan) {
2326
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -977,16 +977,20 @@ public void createRateLimitBanner(String message, boolean warning) {
977977

978978
/**
979979
* Show the quota-warning static banner above the input area. Action links are sourced from
980-
* {@link QuotaActions#forPlan(CopilotPlan, boolean)} so they stay in sync with the inline {@link WarnWidget}.
980+
* {@link QuotaActions#forPlan(CopilotPlan, boolean, Boolean)} so they stay in sync with the inline
981+
* {@link WarnWidget}.
981982
*
982983
* @param message the message to display
983984
* @param plan the user's Copilot plan, or {@code null} for no action links
984985
* @param overageEnabled whether additional paid usage is already enabled for the user; switches the
985986
* "Enable Additional Usage" label to "Increase Budget"
987+
* @param canUpgradePlan whether the user can upgrade their Copilot plan, or {@code null} when the language
988+
* server did not supply this field
986989
* @param warning {@code true} for the warning icon; {@code false} for the info icon
987990
*/
988-
public void createQuotaWarningBanner(String message, CopilotPlan plan, boolean overageEnabled, boolean warning) {
989-
List<BannerAction> bannerActions = QuotaActions.forPlan(plan, overageEnabled).stream()
991+
public void createQuotaWarningBanner(String message, CopilotPlan plan, boolean overageEnabled,
992+
Boolean canUpgradePlan, boolean warning) {
993+
List<BannerAction> bannerActions = QuotaActions.forPlan(plan, overageEnabled, canUpgradePlan).stream()
990994
.map(action -> new BannerAction(action.label(), action.url()))
991995
.toList();
992996
showStaticBanner(message, bannerActions, warning);

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.microsoft.copilot.eclipse.core.lsp.protocol.AgentToolCall;
2929
import com.microsoft.copilot.eclipse.core.lsp.protocol.LanguageModelToolConfirmationResult;
3030
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageRequestParams;
31+
import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.CheckQuotaResult;
3132
import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.CopilotPlan;
3233
import com.microsoft.copilot.eclipse.core.persistence.ConversationDataFactory;
3334
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData;
@@ -611,13 +612,15 @@ protected void createWarnDialog(String message, int code, String modelProviderNa
611612
String displayMessage = byokQuotaExceeded ? Messages.chat_warnWidget_byokQuotaUsageMessage : message;
612613
CopilotPlan planForActions = null;
613614
boolean overageEnabled = false;
615+
Boolean canUpgradePlan = null;
614616
if (code == 402 && !byokQuotaExceeded) {
615-
var quotaStatus = this.serviceManager.getAuthStatusManager().getQuotaStatus();
617+
CheckQuotaResult quotaStatus = this.serviceManager.getAuthStatusManager().getQuotaStatus();
616618
planForActions = quotaStatus.copilotPlan();
617619
overageEnabled = quotaStatus.premiumInteractions() != null
618620
&& quotaStatus.premiumInteractions().overagePermitted();
621+
canUpgradePlan = quotaStatus.canUpgradePlan();
619622
}
620-
new WarnWidget(this, SWT.NONE, displayMessage, planForActions, overageEnabled);
623+
new WarnWidget(this, SWT.NONE, displayMessage, planForActions, overageEnabled, canUpgradePlan);
621624
ensureFooterAtBottom();
622625
requestLayout();
623626
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,8 @@ public void done(IJobChangeEvent event) {
386386
boolean warning = "warning".equalsIgnoreCase(params.severity());
387387
boolean overageEnabled = params.premiumInteractions() != null
388388
&& params.premiumInteractions().overageEnabled();
389-
actionBar.createQuotaWarningBanner(params.message(), params.copilotPlan(), overageEnabled, warning);
389+
actionBar.createQuotaWarningBanner(params.message(), params.copilotPlan(), overageEnabled,
390+
params.canUpgradePlan(), warning);
390391
}
391392
}, parent);
392393
}

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

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,31 @@ private QuotaActions() {
3737
/**
3838
* Returns the ordered list of {@link QuotaAction}s appropriate for the supplied plan.
3939
*
40-
* <p>Mapping:
40+
* <p>Non-upgrade actions are derived from the plan:
4141
* <ul>
42-
* <li>{@code free} &rarr; "Upgrade Plan" (primary)</li>
43-
* <li>{@code individual}, {@code individual_pro} &rarr; "Enable Additional Usage" /
44-
* "Increase Budget" (primary) + "Upgrade Plan" (secondary)</li>
45-
* <li>{@code individual_max} &rarr; "Enable Additional Usage" / "Increase Budget" (primary)</li>
46-
* <li>{@code business}, {@code enterprise} &rarr; empty list</li>
47-
* <li>{@code null} &rarr; empty list</li>
42+
* <li>{@code free} &rarr; none</li>
43+
* <li>{@code individual}, {@code individual_pro}, {@code individual_max} &rarr;
44+
* "Enable Additional Usage" / "Increase Budget"</li>
45+
* <li>{@code business}, {@code enterprise} &rarr; none</li>
4846
* </ul>
4947
*
50-
* <p>The "Enable Additional Usage" label is replaced with "Increase Budget" when {@code overageEnabled}
51-
* is {@code true}, matching the IntelliJ quota dialog wording.
48+
* <p>The "Upgrade Plan" action is then appended when the user is eligible. When
49+
* {@code canUpgradePlan} is non-{@code null} it takes precedence over the plan-based default;
50+
* otherwise the plan default is used (eligible for {@code free}, {@code individual},
51+
* {@code individual_pro}). When the upgrade action is the only entry it is rendered as primary;
52+
* when it follows another primary action it is rendered as secondary so the surfaces stay
53+
* visually consistent.
54+
*
55+
* <p>The "Enable Additional Usage" label is replaced with "Increase Budget" when
56+
* {@code overageEnabled} is {@code true}, matching the IntelliJ quota dialog wording.
5257
*
5358
* @param plan the user's Copilot plan, or {@code null} when unknown
5459
* @param overageEnabled {@code true} when additional paid usage is already enabled for the user
60+
* @param canUpgradePlan whether the user can upgrade their Copilot plan, or {@code null} when the
61+
* language server did not supply this field
5562
* @return an immutable, possibly empty list; never {@code null}
5663
*/
57-
public static List<QuotaAction> forPlan(CopilotPlan plan, boolean overageEnabled) {
64+
public static List<QuotaAction> forPlan(CopilotPlan plan, boolean overageEnabled, Boolean canUpgradePlan) {
5865
if (plan == null) {
5966
return List.of();
6067
}
@@ -69,20 +76,34 @@ public static List<QuotaAction> forPlan(CopilotPlan plan, boolean overageEnabled
6976
QuotaAction manageOverage = new QuotaAction(overageLabel,
7077
Messages.chat_noQuotaView_enableAdditionalUsageButton_tooltip,
7178
UiConstants.MANAGE_COPILOT_OVERAGE_URL, true);
72-
return switch (plan) {
73-
case free -> List.of(upgradePrimary);
74-
case individual, individual_pro -> List.of(manageOverage, upgradeSecondary);
75-
case individual_max -> List.of(manageOverage);
76-
case business, enterprise -> List.of();
79+
80+
boolean hasOverage = switch (plan) {
81+
case individual, individual_pro, individual_max -> true;
82+
case free, business, enterprise -> false;
83+
};
84+
boolean showUpgrade = canUpgradePlan != null ? canUpgradePlan : switch (plan) {
85+
case free, individual, individual_pro -> true;
86+
case individual_max, business, enterprise -> false;
7787
};
88+
if (hasOverage && showUpgrade) {
89+
return List.of(manageOverage, upgradeSecondary);
90+
}
91+
if (hasOverage) {
92+
return List.of(manageOverage);
93+
}
94+
if (showUpgrade) {
95+
return List.of(upgradePrimary);
96+
}
97+
return List.of();
7898
}
7999

80100
/**
81101
* Returns {@code true} when an error response represents a Bring-Your-Own-Key (BYOK) quota-exceeded
82102
* condition, i.e. a {@code 402} from the language server that also carries a non-blank model
83103
* provider name. BYOK usage is governed by the customer's provider account rather than the user's
84-
* Copilot plan, so callers should suppress the plan-driven {@link #forPlan(CopilotPlan)} actions
85-
* and substitute the BYOK-specific message in this case.
104+
* Copilot plan, so callers should suppress the plan-driven
105+
* {@link #forPlan(CopilotPlan, boolean, Boolean)} actions and substitute the BYOK-specific
106+
* message in this case.
86107
*
87108
* @param code the error code from the language server
88109
* @param modelProviderName the BYOK model-provider name from the server payload, or {@code null}

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626

2727
/**
2828
* Widget that displays a warning message under a chat turn, optionally followed by plan-driven action buttons sourced
29-
* from {@link QuotaActions#forPlan(CopilotPlan, boolean)}. Presentation-only: the caller decides the message and
30-
* whether to pass a plan.
29+
* from {@link QuotaActions#forPlan(CopilotPlan, boolean, Boolean)}. Presentation-only: the caller decides the message
30+
* and whether to pass a plan.
3131
*/
3232
public class WarnWidget extends Composite {
3333
private int buttonLeftMargin;
@@ -41,8 +41,11 @@ public class WarnWidget extends Composite {
4141
* @param userPlan the user's Copilot plan to render plan-driven action buttons, or {@code null} for no buttons
4242
* @param overageEnabled whether additional paid usage is already enabled for the user; switches the
4343
* "Enable Additional Usage" label to "Increase Budget"
44+
* @param canUpgradePlan whether the user can upgrade their Copilot plan, or {@code null} when the language
45+
* server did not supply this field; forwarded to {@link QuotaActions#forPlan(CopilotPlan, boolean, Boolean)}
4446
*/
45-
public WarnWidget(Composite parent, int style, String message, CopilotPlan userPlan, boolean overageEnabled) {
47+
public WarnWidget(Composite parent, int style, String message, CopilotPlan userPlan, boolean overageEnabled,
48+
Boolean canUpgradePlan) {
4649
super(parent, style | SWT.BORDER);
4750
GridLayout outerLayout = new GridLayout(1, true);
4851
outerLayout.verticalSpacing = 0;
@@ -52,7 +55,7 @@ public WarnWidget(Composite parent, int style, String message, CopilotPlan userP
5255
buildWarnLabelWithIcon(message);
5356

5457
if (userPlan != null) {
55-
buildActionButtons(userPlan, overageEnabled);
58+
buildActionButtons(userPlan, overageEnabled, canUpgradePlan);
5659
}
5760
parent.layout();
5861
}
@@ -128,8 +131,8 @@ private void buildWarnLabelWithIcon(String message) {
128131
/**
129132
* Render plan-driven action buttons for a quota-exceeded warning, kept in sync with the quota {@link StaticBanner}.
130133
*/
131-
private void buildActionButtons(CopilotPlan userPlan, boolean overageEnabled) {
132-
List<QuotaAction> actions = QuotaActions.forPlan(userPlan, overageEnabled);
134+
private void buildActionButtons(CopilotPlan userPlan, boolean overageEnabled, Boolean canUpgradePlan) {
135+
List<QuotaAction> actions = QuotaActions.forPlan(userPlan, overageEnabled, canUpgradePlan);
133136
if (actions.isEmpty()) {
134137
return;
135138
}

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowMenuBarMenuHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ private void addCopilotUsageItemsTbb(List<IContributionItem> items, CheckQuotaRe
234234

235235
// For free / individual / individual_pro users, show an Upgrade Plan row. When the overage row is
236236
// already showing the upgrade icon directly above, this row uses the blank icon to avoid duplication.
237-
if (MenuUtils.shouldShowUpgradePlanRow(plan)) {
237+
if (MenuUtils.shouldShowUpgradePlanRow(plan, quotaStatus.canUpgradePlan())) {
238238
ImageDescriptor upgradePlanIcon = hasNonOrgPremiumQuota ? blankIcon : upgradeIcon;
239239
items.add(createCommandItem("com.microsoft.copilot.eclipse.commands.upgradeCopilotPlan",
240240
Messages.menu_quota_upgradePlan, upgradePlanIcon));

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ private void addCopilotUsageActionTbb(MenuManager menuManager, CheckQuotaResult
339339

340340
// For free / individual / individual_pro users, show an Upgrade Plan row. When the overage row is
341341
// already showing the upgrade icon directly above, this row uses the blank icon to avoid duplication.
342-
if (MenuUtils.shouldShowUpgradePlanRow(plan)) {
342+
if (MenuUtils.shouldShowUpgradePlanRow(plan, quotaStatus.canUpgradePlan())) {
343343
ImageDescriptor upgradePlanIcon = hasNonOrgPremiumQuota ? blankIcon : upgradeIcon;
344344
MenuActionFactory.createMenuAction(menuManager, Messages.menu_quota_upgradePlan, upgradePlanIcon, handlerService,
345345
"com.microsoft.copilot.eclipse.commands.upgradeCopilotPlan", true);

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/MenuUtils.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,20 @@ public static boolean hasNonOrgPremiumQuota(CheckQuotaResult quotaStatus) {
126126

127127
/**
128128
* True when the "Upgrade Plan" row should be shown for the given plan.
129+
*
130+
* <p>{@code canUpgradePlan} is the language server's authoritative signal of whether the user is eligible
131+
* to upgrade. When supplied (non-{@code null}) it takes precedence over the plan-based default; when
132+
* {@code null} (older language server that does not yet send this field) we fall back to the previous
133+
* plan-only heuristic.
134+
*
135+
* @param plan the user's Copilot plan
136+
* @param canUpgradePlan whether the user can upgrade their Copilot plan, or {@code null} when the language
137+
* server did not supply this field
129138
*/
130-
public static boolean shouldShowUpgradePlanRow(CopilotPlan plan) {
139+
public static boolean shouldShowUpgradePlanRow(CopilotPlan plan, Boolean canUpgradePlan) {
140+
if (canUpgradePlan != null) {
141+
return canUpgradePlan;
142+
}
131143
return plan == CopilotPlan.free || plan == CopilotPlan.individual || plan == CopilotPlan.individual_pro;
132144
}
133145

0 commit comments

Comments
 (0)