Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,4 @@ void testOnDidChangeFeatureFlagsWithEmptyFeatureFlags() {
verify(mockFeatureFlags).setByokEnabled(true);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public class CopilotEventConstants {
*/
private static final String TOPIC_MCP = TOPIC_BASE + "MCP/";

/**
* Topic for quota events.
*/
private static final String TOPIC_QUOTA = TOPIC_BASE + "QUOTA/";

/**
* Topic for Next Edit Suggestion (NES) events.
*/
Expand Down Expand Up @@ -161,6 +166,11 @@ public class CopilotEventConstants {
*/
public static final String TOPIC_RATE_LIMIT_WARNING = TOPIC_CHAT + "RATE_LIMIT_WARNING";

/**
* Event when a quota warning notification is received from the language server.
*/
public static final String TOPIC_QUOTA_WARNING = TOPIC_QUOTA + "WARNING";

/**
* Event when custom prompts, skills, agents, or instructions change on the language server. Clients should re-fetch
* conversation templates on receipt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageRequestParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageResult;
import com.microsoft.copilot.eclipse.core.lsp.protocol.policy.DidChangePolicyParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.QuotaWarningParams;
import com.microsoft.copilot.eclipse.core.utils.FileUtils;
import com.microsoft.copilot.eclipse.core.utils.PlatformUtils;

Expand Down Expand Up @@ -353,6 +354,16 @@ public CompletableFuture<CodingAgentMessageResult> onCodingAgentMessage(CodingAg
return CompletableFuture.completedFuture(result);
}

/**
* Notify when a quota warning is received from the language server.
*/
@JsonNotification("copilot/quotaWarning")
Comment thread
jdneo marked this conversation as resolved.
public void onQuotaWarning(QuotaWarningParams params) {
if (eventBroker != null) {
eventBroker.post(CopilotEventConstants.TOPIC_QUOTA_WARNING, params);
}
}

/**
* Reads the contents and stats of a file given its URI.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ public String getErrorReason() {
return error != null ? error.getReason() : null;
}

/**
* Returns the BYOK model-provider name from the error payload, when present.
*
* @return the name of the BYOK model provider that produced the error, or {@code null} for built-in models.
*/
public String getErrorModelProviderName() {
return error != null ? error.getModelProviderName() : null;
}

public List<AgentRound> getAgentRounds() {
return editAgentRounds;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class ConversationError {
private String reason;
private boolean responseIsIncomplete;
private boolean responseIsFiltered;
private String modelProviderName;

public void setMessage(String message) {
this.message = message;
Expand All @@ -40,6 +41,10 @@ public void setResponseIsFiltered(boolean responseIsFiltered) {
this.responseIsFiltered = responseIsFiltered;
}

public void setModelProviderName(String modelProviderName) {
this.modelProviderName = modelProviderName;
}

public String getMessage() {
return message;
}
Expand All @@ -60,9 +65,17 @@ public boolean getResponseIsFiltered() {
return responseIsFiltered;
}

/**
* The name of the model provider that produced the error, when the failing request was routed to a custom
* Bring-Your-Own-Key (BYOK) model. {@code null} or blank for built-in Copilot models.
*/
public String getModelProviderName() {
return modelProviderName;
}

@Override
public int hashCode() {
return Objects.hash(message, code, reason, responseIsIncomplete, responseIsFiltered);
return Objects.hash(message, code, reason, responseIsIncomplete, responseIsFiltered, modelProviderName);
}

@Override
Expand All @@ -75,7 +88,8 @@ public boolean equals(Object o) {
}
ConversationError that = (ConversationError) o;
return Objects.equals(message, that.message) && code == that.code && Objects.equals(reason, that.reason)
&& responseIsIncomplete == that.responseIsIncomplete && responseIsFiltered == that.responseIsFiltered;
&& responseIsIncomplete == that.responseIsIncomplete && responseIsFiltered == that.responseIsFiltered
&& Objects.equals(modelProviderName, that.modelProviderName);
}

@Override
Expand All @@ -86,6 +100,7 @@ public String toString() {
builder.append("reason", reason);
builder.append("responseIsIncomplete", responseIsIncomplete);
builder.append("responseIsFiltered", responseIsFiltered);
builder.append("modelProviderName", modelProviderName);
return builder.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

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

import com.google.gson.annotations.SerializedName;

/**
* Parameters for the {@code copilot/quotaWarning} notification, sent by the language server when
* the user crosses a quota usage threshold or starts consuming overages.
*
* @param title warning title (e.g. "Copilot Quota Usage Alert")
* @param message human-readable warning message
* @param severity severity level, either {@code "warning"} or {@code "info"}
* @param chat current chat quota snapshot, when available
* @param completions current completions quota snapshot, when available
* @param premiumInteractions current premium interactions quota snapshot, when available
* @param copilotPlan the user's Copilot plan
*/
public record QuotaWarningParams(String title, String message, String severity, QuotaSnapshotParams chat,
Comment thread
ethanyhou marked this conversation as resolved.
QuotaSnapshotParams completions,
@SerializedName("premium_interactions") QuotaSnapshotParams premiumInteractions, CopilotPlan copilotPlan) {
}
Comment thread
ethanyhou marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ private void applyConversationError(ReplyData reply, ConversationError error) {
ErrorData errorData = new ErrorData();
errorData.setMessage(error.getMessage());
errorData.setCode(error.getCode());
errorData.setModelProviderName(error.getModelProviderName());
ErrorMessageData em = new ErrorMessageData();
em.setError(errorData);
reply.getErrorMessages().add(em);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ public String toString() {
public static class ErrorData {
private String message;
private int code;
private String modelProviderName;
private Map<String, Object> data;

public String getMessage() {
Expand All @@ -458,6 +459,18 @@ public void setCode(int code) {
this.code = code;
}

/**
* The BYOK model provider responsible for the error, or {@code null} when the failing model was a
* built-in Copilot model.
*/
public String getModelProviderName() {
return modelProviderName;
}

public void setModelProviderName(String modelProviderName) {
this.modelProviderName = modelProviderName;
}

public Map<String, Object> getData() {
return data;
}
Expand All @@ -468,7 +481,7 @@ public void setData(Map<String, Object> data) {

@Override
public int hashCode() {
return Objects.hash(code, data, message);
return Objects.hash(code, data, message, modelProviderName);
}

@Override
Expand All @@ -483,14 +496,16 @@ public boolean equals(Object obj) {
return false;
}
ErrorData other = (ErrorData) obj;
return code == other.code && Objects.equals(data, other.data) && Objects.equals(message, other.message);
return code == other.code && Objects.equals(data, other.data) && Objects.equals(message, other.message)
&& Objects.equals(modelProviderName, other.modelProviderName);
}

@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this);
builder.append("message", message);
builder.append("code", code);
builder.append("modelProviderName", modelProviderName);
builder.append("data", data);
return builder.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ void testMessagesInitialization() {
assertNotNull(Messages.menu_signToGitHub);
assertNotNull(Messages.menu_signOutOfGitHub);
}
}
}
2 changes: 1 addition & 1 deletion com.microsoft.copilot.eclipse.ui/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Require-Bundle: com.microsoft.copilot.eclipse.core;bundle-version="0.15.0",
org.eclipse.ui.editors;bundle-version="3.17.100",
org.eclipse.ui;bundle-version="3.205.0",
org.eclipse.ui.navigator;bundle-version="3.12.200",
org.eclipse.jface.text;bundle-version="3.24.200",
org.eclipse.jface.text;bundle-version="3.24.200",
org.eclipse.core.runtime;bundle-version="[3.30.0,4.0.0)",
org.eclipse.core.expressions,
org.eclipse.jdt.annotation;resolution:=optional,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import com.microsoft.copilot.eclipse.core.chat.CustomChatModeManager;
import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatMode;
import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.CopilotPlan;
import com.microsoft.copilot.eclipse.core.utils.ChatMessageUtils;
import com.microsoft.copilot.eclipse.core.utils.FileUtils;
import com.microsoft.copilot.eclipse.core.utils.PlatformUtils;
Expand Down Expand Up @@ -118,6 +119,7 @@ public class ActionBar extends Composite implements NewConversationListener {
private Image autoBreakpointDisabledImage;
private ContextSizeDonut contextSizeDonut;
private StaticBanner staticBanner;
private Composite inputArea;

private ChatServiceManager chatServiceManager;
IEventBroker eventBroker;
Expand Down Expand Up @@ -163,13 +165,28 @@ public ActionBar(Composite parent, int style, ChatServiceManager chatServiceMana
};
this.eventBroker.subscribe(CopilotEventConstants.TOPIC_CHAT_DID_CHANGE_FEATURE_FLAGS,
featureFlagsChangedEventHandler);
Composite actionBar = new Composite(this, style | SWT.BORDER);
// Transparent wrapper for the optional TodoListBar / WorkingSetBar and the bordered input below.
// StaticBanner is created as a sibling of inputArea so it stays structurally above the whole stack.
this.inputArea = new Composite(this, SWT.NONE);
GridLayout glInputArea = new GridLayout(1, false);
glInputArea.marginWidth = 0;
glInputArea.marginHeight = 0;
glInputArea.marginLeft = 0;
glInputArea.marginRight = 0;
glInputArea.marginTop = 0;
glInputArea.marginBottom = 0;
glInputArea.horizontalSpacing = 0;
glInputArea.verticalSpacing = 0;
this.inputArea.setLayout(glInputArea);
this.inputArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));

Composite borderedActionBar = new Composite(this.inputArea, style | SWT.BORDER);
GridLayout gl = new GridLayout(1, false);
gl.marginHeight = 5;
gl.verticalSpacing = 0;
actionBar.setLayout(gl);
actionBar.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
actionBar.setData(CssConstants.CSS_ID_KEY, "chat-action-bar");
borderedActionBar.setLayout(gl);
borderedActionBar.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
borderedActionBar.setData(CssConstants.CSS_ID_KEY, "chat-action-bar");

RowLayout rowLayout = new RowLayout();
rowLayout.wrap = true;
Expand All @@ -185,7 +202,7 @@ public ActionBar(Composite parent, int style, ChatServiceManager chatServiceMana
rowLayout.marginTop = 0;
rowLayout.marginBottom = 10;
rowLayout.center = true;
this.cmpFileRef = new Composite(actionBar, SWT.NONE);
this.cmpFileRef = new Composite(borderedActionBar, SWT.NONE);
this.cmpFileRef.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
this.cmpFileRef.setLayout(rowLayout);
new AddContextButton(this.cmpFileRef);
Expand All @@ -197,7 +214,7 @@ public ActionBar(Composite parent, int style, ChatServiceManager chatServiceMana
ModelService modelService = chatServiceManager.getModelService();
modelService.bindActionBarForSupportVisionChange(this);

ChatInputTextViewer tv = new ChatInputTextViewer(actionBar, chatServiceManager);
ChatInputTextViewer tv = new ChatInputTextViewer(borderedActionBar, chatServiceManager);
tv.setEditable(true);
tv.addTextListener(new ITextListener() {
@Override
Expand Down Expand Up @@ -279,7 +296,7 @@ private void updateTableLayout(Table table) {
glActionArea.marginLeft = 0;
glActionArea.marginTop = 5;
glActionArea.marginBottom = -5;
this.cmpActionArea = new Composite(actionBar, SWT.NONE);
this.cmpActionArea = new Composite(borderedActionBar, SWT.NONE);
this.cmpActionArea.setLayout(glActionArea);
this.cmpActionArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
Comment thread
jdneo marked this conversation as resolved.

Expand Down Expand Up @@ -947,28 +964,60 @@ private List<IFile> selectFile() {
}

/**
* Show the static banner above the bordered action bar area.
* Show the rate-limit static banner above the input area, with a single "Get more info" action link.
*
* @param message the message to display
* @param warning {@code true} for the warning icon; {@code false} for the info icon
*/
public void createStaticBanner(String message) {
public void createRateLimitBanner(String message, boolean warning) {
List<BannerAction> actions = List.of(
new BannerAction(Messages.chat_rateLimitBanner_getMoreInfo, UiConstants.COPILOT_RATE_LIMIT_INFO_URL));
showStaticBanner(message, actions, warning);
}

/**
* Show the quota-warning static banner above the input area. Action links are sourced from
* {@link QuotaActions#forPlan(CopilotPlan, boolean)} so they stay in sync with the inline {@link WarnWidget}.
*
* @param message the message to display
* @param plan the user's Copilot plan, or {@code null} for no action links
* @param overageEnabled whether additional paid usage is already enabled for the user; switches the
* "Enable Additional Usage" label to "Increase Budget"
* @param warning {@code true} for the warning icon; {@code false} for the info icon
*/
public void createQuotaWarningBanner(String message, CopilotPlan plan, boolean overageEnabled, boolean warning) {
List<BannerAction> bannerActions = QuotaActions.forPlan(plan, overageEnabled).stream()
.map(action -> new BannerAction(action.label(), action.url()))
.toList();
showStaticBanner(message, bannerActions, warning);
}

private void showStaticBanner(String message, List<BannerAction> actions, boolean warning) {
if (isDisposed()) {
return;
}
if (this.staticBanner != null && !this.staticBanner.isDisposed()) {
this.staticBanner.dispose();
}

this.staticBanner = new StaticBanner(this, SWT.NONE, message, Messages.chat_rateLimitBanner_getMoreInfo,
UiConstants.COPILOT_RATE_LIMIT_INFO_URL, Messages.chat_rateLimitBanner_closeTooltip);
// Position the banner above the first child (the bordered action bar composite)
if (getChildren().length > 0) {
this.staticBanner.moveAbove(getChildren()[0]);
this.staticBanner = new StaticBanner(this, SWT.NONE, message, actions,
Messages.chat_rateLimitBanner_closeTooltip, warning);
// Keep the banner above the inputArea sibling, the only other child of this composite.
if (this.inputArea != null && !this.inputArea.isDisposed()) {
this.staticBanner.moveAbove(this.inputArea);
}
this.staticBanner.show();
requestLayout();
}

/**
* Returns the input-area wrapper that owns {@code TodoListBar}, {@code WorkingSetBar}, and the bordered chat input.
* Services creating those top bars should parent them here so the sibling {@code StaticBanner} stays above.
*/
public Composite getInputArea() {
return this.inputArea;
}

/**
* Dispose the current static banner, if present.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.ui.chat;

/**
* A clickable action rendered below the message of a {@link StaticBanner}. Each action is a localized label paired
* with the URL that should be opened when the user activates it.
*
* @param text the visible link label
* @param url the target URL opened on activation
*/
public record BannerAction(String text, String url) {
}
Loading
Loading