Skip to content

Commit 6c0c423

Browse files
authored
feat: implement rate limit warning banner and associated parameters (#17)
1 parent 3e72216 commit 6c0c423

9 files changed

Lines changed: 247 additions & 3 deletions

File tree

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,9 @@ public class CopilotEventConstants {
155155
* Event when NES suggestion is rejected.
156156
*/
157157
public static final String TOPIC_NES_REJECT_SUGGESTION = TOPIC_NES + "REJECT_SUGGESTION";
158-
}
158+
159+
/**
160+
* Event when a rate limit warning is received from the language server.
161+
*/
162+
public static final String TOPIC_RATE_LIMIT_WARNING = TOPIC_CHAT + "RATE_LIMIT_WARNING";
163+
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import com.microsoft.copilot.eclipse.core.lsp.protocol.LanguageModelToolResult;
5555
import com.microsoft.copilot.eclipse.core.lsp.protocol.LanguageModelToolResult.ToolInvocationStatus;
5656
import com.microsoft.copilot.eclipse.core.lsp.protocol.OnChangeMcpServerToolsParams;
57+
import com.microsoft.copilot.eclipse.core.lsp.protocol.RateLimitWarningParams;
5758
import com.microsoft.copilot.eclipse.core.lsp.protocol.ReadFileResult;
5859
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageRequestParams;
5960
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageResult;
@@ -242,6 +243,16 @@ public void mcpRuntimeLogs(McpRuntimeLog mcpRuntimeLog) {
242243
}
243244
}
244245

246+
/**
247+
* Notify when rate limit usage warning is received from the language server.
248+
*/
249+
@JsonNotification("$/copilot/rateLimitWarning")
250+
public void onRateLimitWarning(RateLimitWarningParams params) {
251+
if (eventBroker != null) {
252+
eventBroker.post(CopilotEventConstants.TOPIC_RATE_LIMIT_WARNING, params);
253+
}
254+
}
255+
245256
/**
246257
* Handles the Dynamic OAuth request for MCP.
247258
* Shows a dialog with multiple input fields and returns the user's input values.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package com.microsoft.copilot.eclipse.core.lsp.protocol;
5+
6+
/**
7+
* Parameters for the "$/copilot/rateLimitWarning" notification.
8+
* Sent by the language server when usage rate limits are approaching thresholds.
9+
*
10+
* @param type the rate limit type ("weekly" or "session")
11+
* @param rateLimit the rate limit details
12+
* @param message the human-readable warning message
13+
*/
14+
public record RateLimitWarningParams(String type, RateLimit rateLimit, String message) {
15+
16+
/**
17+
* Rate limit details including entitlement, remaining percentage, and reset date.
18+
*
19+
* @param entitlement the total entitlement
20+
* @param percentRemaining the percentage of quota remaining
21+
* @param resetDate the date when the rate limit resets
22+
*/
23+
public record RateLimit(int entitlement, double percentRemaining, String resetDate) {
24+
}
25+
}

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ private UiConstants() {
4040
public static final String GITHUB_COPILOT_CODING_AGENT_SLUG = "github-copilot-coding-agent";
4141
public static final String GITHUB_COPILOT_CODING_AGENT_LEARN_MORE_URL = "https://aka.ms/learn-copilot-coding-agent";
4242
public static final String TERMINAL_DEPENDENCY_GUIDE_URL = "https://aka.ms/terminal-dependency-guide";
43+
public static final String COPILOT_RATE_LIMIT_INFO_URL = "https://aka.ms/github-copilot-rate-limit-error";
4344
}

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public class ActionBar extends Composite implements NewConversationListener {
117117
private Image autoBreakpointImage;
118118
private Image autoBreakpointDisabledImage;
119119
private ContextSizeDonut contextSizeDonut;
120+
private StaticBanner staticBanner;
120121

121122
private ChatServiceManager chatServiceManager;
122123
IEventBroker eventBroker;
@@ -700,6 +701,7 @@ private boolean isJavaDebuggerToolEnabledForCurrentMode() {
700701
@Override
701702
public void onNewConversation() {
702703
resetSendButton();
704+
disposeStaticBanner();
703705
}
704706

705707
/**
@@ -944,6 +946,43 @@ private List<IFile> selectFile() {
944946
return result;
945947
}
946948

949+
/**
950+
* Show the static banner above the bordered action bar area.
951+
*
952+
* @param message the message to display
953+
*/
954+
public void createStaticBanner(String message) {
955+
if (isDisposed()) {
956+
return;
957+
}
958+
if (this.staticBanner != null && !this.staticBanner.isDisposed()) {
959+
this.staticBanner.dispose();
960+
}
961+
962+
this.staticBanner = new StaticBanner(this, SWT.NONE, message, Messages.chat_rateLimitBanner_getMoreInfo,
963+
UiConstants.COPILOT_RATE_LIMIT_INFO_URL, Messages.chat_rateLimitBanner_closeTooltip);
964+
// Position the banner above the first child (the bordered action bar composite)
965+
if (getChildren().length > 0) {
966+
this.staticBanner.moveAbove(getChildren()[0]);
967+
}
968+
this.staticBanner.show();
969+
requestLayout();
970+
}
971+
972+
/**
973+
* Dispose the current static banner, if present.
974+
*/
975+
public void disposeStaticBanner() {
976+
if (isDisposed()) {
977+
return;
978+
}
979+
if (this.staticBanner != null && !this.staticBanner.isDisposed()) {
980+
this.staticBanner.dispose();
981+
}
982+
this.staticBanner = null;
983+
requestLayout();
984+
}
985+
947986
private void refreshLayout() {
948987
Composite parent = ActionBar.this.getParent();
949988
if (parent != null) {

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.apache.commons.lang3.StringUtils;
1515
import org.eclipse.core.resources.IFile;
1616
import org.eclipse.core.resources.IResource;
17-
import org.eclipse.core.runtime.Platform;
1817
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
1918
import org.eclipse.core.runtime.jobs.Job;
2019
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
@@ -36,7 +35,6 @@
3635
import org.eclipse.ui.contexts.IContextActivation;
3736
import org.eclipse.ui.contexts.IContextService;
3837
import org.eclipse.ui.part.ViewPart;
39-
import org.osgi.framework.Bundle;
4038
import org.osgi.service.event.EventHandler;
4139

4240
import com.microsoft.copilot.eclipse.core.Constants;
@@ -59,6 +57,7 @@
5957
import com.microsoft.copilot.eclipse.core.lsp.protocol.ContextSizeInfo;
6058
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel;
6159
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult;
60+
import com.microsoft.copilot.eclipse.core.lsp.protocol.RateLimitWarningParams;
6261
import com.microsoft.copilot.eclipse.core.lsp.protocol.TodoItem;
6362
import com.microsoft.copilot.eclipse.core.lsp.protocol.Turn;
6463
import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageRequestParams;
@@ -134,6 +133,7 @@ public class ChatView extends ViewPart implements ChatProgressListener, MessageL
134133
private EventHandler conversationTitleUpdatedHandler;
135134
private EventHandler codingAgentMessageHandler;
136135
private EventHandler autoBreakpointToggleHandler;
136+
private EventHandler rateLimitWarningHandler;
137137

138138
// Context activation for chat view keyboard shortcuts
139139
private static final String CHAT_VIEW_CONTEXT = "com.microsoft.copilot.eclipse.chatViewContext";
@@ -243,6 +243,10 @@ public void done(IJobChangeEvent event) {
243243

244244
clearCurrentConversation();
245245

246+
if (actionBar != null && !actionBar.isDisposed()) {
247+
actionBar.disposeStaticBanner();
248+
}
249+
246250
if (conversation == null) {
247251
// Handle "New Chat" selection.
248252
hideChatHistory();
@@ -342,6 +346,18 @@ public void done(IJobChangeEvent event) {
342346
this.eventBroker.subscribe(CopilotEventConstants.TOPIC_CHAT_AUTO_BREAKPOINT_TOGGLE,
343347
this.autoBreakpointToggleHandler);
344348

349+
this.rateLimitWarningHandler = event -> {
350+
Object data = event.getProperty(IEventBroker.DATA);
351+
if (data instanceof RateLimitWarningParams params) {
352+
SwtUtils.invokeOnDisplayThreadAsync(() -> {
353+
if (actionBar != null && !actionBar.isDisposed()) {
354+
actionBar.createStaticBanner(params.message());
355+
}
356+
}, parent);
357+
}
358+
};
359+
this.eventBroker.subscribe(CopilotEventConstants.TOPIC_RATE_LIMIT_WARNING, this.rateLimitWarningHandler);
360+
345361
// Register part listener to activate/deactivate chat view context for keyboard shortcuts
346362
registerPartListener();
347363
}
@@ -1290,6 +1306,10 @@ public void dispose() {
12901306
this.eventBroker.unsubscribe(this.autoBreakpointToggleHandler);
12911307
autoBreakpointToggleHandler = null;
12921308
}
1309+
if (rateLimitWarningHandler != null) {
1310+
this.eventBroker.unsubscribe(this.rateLimitWarningHandler);
1311+
rateLimitWarningHandler = null;
1312+
}
12931313
}
12941314

12951315
if (this.chatServiceManager != null) {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package com.microsoft.copilot.eclipse.ui.chat;
5+
6+
import org.apache.commons.lang3.StringUtils;
7+
import org.eclipse.osgi.util.NLS;
8+
import org.eclipse.swt.SWT;
9+
import org.eclipse.swt.events.MouseAdapter;
10+
import org.eclipse.swt.events.MouseEvent;
11+
import org.eclipse.swt.events.SelectionAdapter;
12+
import org.eclipse.swt.events.SelectionEvent;
13+
import org.eclipse.swt.graphics.Image;
14+
import org.eclipse.swt.layout.GridData;
15+
import org.eclipse.swt.layout.GridLayout;
16+
import org.eclipse.swt.widgets.Composite;
17+
import org.eclipse.swt.widgets.Label;
18+
import org.eclipse.swt.widgets.Link;
19+
import org.eclipse.ui.ISharedImages;
20+
import org.eclipse.ui.PlatformUI;
21+
22+
import com.microsoft.copilot.eclipse.ui.utils.UiUtils;
23+
24+
/**
25+
* A reusable banner widget that displays an informational message with an inline link and a close button. Shows an info
26+
* icon, the provided message with an appended link, and a dismiss (×) button.
27+
*
28+
* <p>Usage example:
29+
*
30+
* <pre>var banner = new StaticBanner(parent, SWT.NONE, "You've used 90% of your rate limit.", "Get more info",
31+
* "https://example.com", "Dismiss");
32+
* banner.show();
33+
* </pre>
34+
*/
35+
public class StaticBanner extends Composite {
36+
private Link messageLink;
37+
38+
/**
39+
* Create a static informational banner.
40+
*
41+
* @param parent the parent composite
42+
* @param style the SWT style
43+
* @param message the informational message to display
44+
* @param linkText the text for the inline link (e.g. "Get more info")
45+
* @param linkUrl the URL to open when the link is clicked
46+
* @param closeTooltip the tooltip for the close button
47+
*/
48+
public StaticBanner(Composite parent, int style, String message, String linkText, String linkUrl,
49+
String closeTooltip) {
50+
super(parent, style | SWT.BORDER);
51+
52+
GridLayout layout = new GridLayout(3, false);
53+
layout.marginWidth = 10;
54+
layout.marginHeight = 8;
55+
layout.horizontalSpacing = 6;
56+
setLayout(layout);
57+
setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
58+
59+
// Info icon
60+
Label iconLabel = new Label(this, SWT.NONE);
61+
Image infoImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_INFO_TSK);
62+
iconLabel.setImage(infoImage);
63+
iconLabel.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
64+
65+
// Message + inline link
66+
this.messageLink = new Link(this, SWT.WRAP);
67+
this.messageLink.setText(buildMessageText(message, linkText, linkUrl));
68+
this.messageLink.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
69+
this.messageLink.addSelectionListener(new SelectionAdapter() {
70+
@Override
71+
public void widgetSelected(SelectionEvent e) {
72+
if (StringUtils.isNotBlank(linkUrl)) {
73+
UiUtils.openLink(linkUrl);
74+
}
75+
}
76+
});
77+
78+
// Close button
79+
Label closeButton = new Label(this, SWT.NONE);
80+
Image closeImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ELCL_REMOVE);
81+
closeButton.setImage(closeImage);
82+
closeButton.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false));
83+
closeButton.setToolTipText(closeTooltip);
84+
closeButton.setCursor(getDisplay().getSystemCursor(SWT.CURSOR_HAND));
85+
closeButton.addMouseListener(new MouseAdapter() {
86+
@Override
87+
public void mouseUp(MouseEvent e) {
88+
disposeBanner();
89+
}
90+
});
91+
92+
setVisible(false);
93+
GridData gd = (GridData) getLayoutData();
94+
gd.exclude = true;
95+
}
96+
97+
/**
98+
* Show the banner.
99+
*/
100+
public void show() {
101+
if (isDisposed()) {
102+
return;
103+
}
104+
setVisible(true);
105+
GridData gd = (GridData) getLayoutData();
106+
gd.exclude = false;
107+
getParent().requestLayout();
108+
}
109+
110+
private void disposeBanner() {
111+
if (isDisposed()) {
112+
return;
113+
}
114+
Composite parent = getParent();
115+
dispose();
116+
if (parent != null && !parent.isDisposed()) {
117+
parent.requestLayout();
118+
}
119+
}
120+
121+
private static String buildMessageText(String message, String linkText, String linkUrl) {
122+
String safeMessage = escapeForLink(message);
123+
if (StringUtils.isBlank(linkText) || StringUtils.isBlank(linkUrl)) {
124+
return safeMessage;
125+
}
126+
return NLS.bind(com.microsoft.copilot.eclipse.ui.i18n.Messages.chat_staticBanner_messageWithLink, safeMessage,
127+
escapeForLink(linkText));
128+
}
129+
130+
private static String escapeForLink(String text) {
131+
if (text == null) {
132+
return StringUtils.EMPTY;
133+
}
134+
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
135+
}
136+
}

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ public final class Messages extends NLS {
198198
public static String context_window_messages;
199199
public static String context_window_files;
200200
public static String context_window_tool_results;
201+
public static String chat_staticBanner_messageWithLink;
202+
public static String chat_rateLimitBanner_getMoreInfo;
203+
public static String chat_rateLimitBanner_closeTooltip;
201204

202205
static {
203206
// initialize resource bundle

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,7 @@ context_window_user_context=User Context
194194
context_window_messages=Messages
195195
context_window_files=Attached Files
196196
context_window_tool_results=Tool Results
197+
198+
chat_staticBanner_messageWithLink={0} <a>{1}</a>
199+
chat_rateLimitBanner_getMoreInfo=Get more info
200+
chat_rateLimitBanner_closeTooltip=Dismiss

0 commit comments

Comments
 (0)