Skip to content

Commit f19aa4d

Browse files
authored
feat: [New billing] Add CopilotUsageBar & ContextWindowBar for visualizing token usage (#1623)
1 parent 13b9160 commit f19aa4d

5 files changed

Lines changed: 238 additions & 33 deletions

File tree

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/contextwindow/ContextWindowPopup.java

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.microsoft.copilot.eclipse.core.lsp.protocol.ContextSizeInfo;
2222
import com.microsoft.copilot.eclipse.ui.i18n.Messages;
23+
import com.microsoft.copilot.eclipse.ui.swt.ContextWindowBar;
2324
import com.microsoft.copilot.eclipse.ui.swt.CssConstants;
2425
import com.microsoft.copilot.eclipse.ui.utils.UiUtils;
2526

@@ -38,8 +39,6 @@ class ContextWindowPopup {
3839
private static final int BORDER_ARC = 8;
3940
private static final int H_MARGIN = 8;
4041
private static final int V_MARGIN = 8;
41-
private static final int PROGRESS_BAR_HEIGHT = 4;
42-
private static final int PROGRESS_BAR_ARC = 4;
4342
private static final int POLL_INTERVAL_MS = 100;
4443

4544
private Shell shell;
@@ -51,7 +50,7 @@ class ContextWindowPopup {
5150
// Updatable labels
5251
private Label tokenUsageLabel;
5352
private Label percentageLabel;
54-
private Composite progressBar;
53+
private ContextWindowBar progressBar;
5554
private Label systemInstructionsValue;
5655
private Label toolDefinitionsValue;
5756
private Label messagesValue;
@@ -77,7 +76,6 @@ public void open(Control anchor) {
7776

7877
Shell parentShell = anchor.getShell();
7978
shell = new Shell(parentShell, SWT.NO_TRIM | SWT.ON_TOP);
80-
applyCssId(shell, DROPDOWN_POPUP_CSS_ID);
8179

8280
GridLayout shellLayout = new GridLayout(1, false);
8381
shellLayout.marginWidth = H_MARGIN;
@@ -86,6 +84,7 @@ public void open(Control anchor) {
8684
shell.setLayout(shellLayout);
8785

8886
buildContent(shell, info);
87+
applyCssId(shell, DROPDOWN_POPUP_CSS_ID);
8988
addBorder(shell);
9089

9190
shell.pack();
@@ -187,7 +186,9 @@ private void buildContent(Composite parent, ContextSizeInfo info) {
187186
percentageLabel = createSecondaryTextLabel(tokenRow, formatPercentage(info.utilizationPercentage()));
188187
percentageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.NONE, false, false));
189188

190-
progressBar = createProgressBar(parent);
189+
progressBar = new ContextWindowBar(parent, SWT.NONE);
190+
progressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
191+
progressBar.setPercentage((int) Math.round(info.utilizationPercentage()));
191192

192193
addSeparator(parent, SECTION_SPACING);
193194

@@ -221,7 +222,7 @@ private void updateLabels(ContextSizeInfo info) {
221222
setLabelText(attachedFilesValue, percentageOf(info.attachedFilesTokens(), info.totalTokenLimit()));
222223
setLabelText(toolResultsValue, percentageOf(info.toolResultsTokens(), info.totalTokenLimit()));
223224
if (progressBar != null && !progressBar.isDisposed()) {
224-
progressBar.redraw();
225+
progressBar.setPercentage((int) Math.round(info.utilizationPercentage()));
225226
}
226227
shell.requestLayout();
227228
}
@@ -276,33 +277,6 @@ private Label addKeyValueRow(Composite parent, String key, String value) {
276277
return valueLabel;
277278
}
278279

279-
private Composite createProgressBar(Composite parent) {
280-
Composite bar = new Composite(parent, SWT.NONE);
281-
applyCssId(bar, DROPDOWN_POPUP_CSS_ID);
282-
GridData pbGd = new GridData(SWT.FILL, SWT.CENTER, true, false);
283-
pbGd.heightHint = PROGRESS_BAR_HEIGHT;
284-
bar.setLayoutData(pbGd);
285-
Display display = parent.getDisplay();
286-
bar.addPaintListener(e -> {
287-
ContextSizeInfo state = contextWindowService.getState();
288-
if (state == null) {
289-
return;
290-
}
291-
Rectangle r = bar.getClientArea();
292-
Color restColor = CssConstants.getDonutTrackColor(display);
293-
e.gc.setAntialias(SWT.ON);
294-
e.gc.setBackground(restColor);
295-
e.gc.fillRoundRectangle(0, 0, r.width, r.height, PROGRESS_BAR_ARC, PROGRESS_BAR_ARC);
296-
int fill = (int) (r.width * Math.min(state.utilizationPercentage(), 100) / 100.0);
297-
if (fill > 0) {
298-
Color usedColor = CssConstants.getDonutFilledColor(display);
299-
e.gc.setBackground(usedColor);
300-
e.gc.fillRoundRectangle(0, 0, fill, r.height, PROGRESS_BAR_ARC, PROGRESS_BAR_ARC);
301-
}
302-
});
303-
return bar;
304-
}
305-
306280
private Label createSecondaryTextLabel(Composite parent, String text) {
307281
Label label = new Label(parent, SWT.NONE);
308282
label.setText(text);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.microsoft.copilot.eclipse.ui.swt;
2+
3+
import org.eclipse.swt.SWT;
4+
import org.eclipse.swt.events.PaintEvent;
5+
import org.eclipse.swt.graphics.Color;
6+
import org.eclipse.swt.graphics.GC;
7+
import org.eclipse.swt.graphics.Rectangle;
8+
import org.eclipse.swt.layout.GridData;
9+
import org.eclipse.swt.layout.GridLayout;
10+
import org.eclipse.swt.widgets.Canvas;
11+
import org.eclipse.swt.widgets.Composite;
12+
13+
/**
14+
* Abstract base for color-coded usage bars. Subclasses provide colors and threshold logic via {@link #getTrackColor()}
15+
* and {@link #getFillColor()}.
16+
*
17+
* <p>The bar renders a filled portion (left) over a track background (right), both with rounded
18+
* ends.
19+
*/
20+
public abstract class AbstractUsageBar extends Composite {
21+
22+
private static final int BAR_HEIGHT = 4;
23+
24+
private int percentage;
25+
private Canvas barCanvas;
26+
27+
/**
28+
* Creates a new usage bar.
29+
*
30+
* @param parent the parent composite
31+
* @param style the SWT style bits
32+
*/
33+
protected AbstractUsageBar(Composite parent, int style) {
34+
super(parent, style);
35+
this.percentage = 0;
36+
createControls();
37+
}
38+
39+
private void createControls() {
40+
GridLayout layout = new GridLayout(1, false);
41+
layout.marginWidth = 0;
42+
layout.marginHeight = 0;
43+
setLayout(layout);
44+
45+
barCanvas = new Canvas(this, SWT.DOUBLE_BUFFERED);
46+
GridData barGd = new GridData(SWT.FILL, SWT.NONE, true, false);
47+
barGd.heightHint = BAR_HEIGHT;
48+
barCanvas.setLayoutData(barGd);
49+
barCanvas.addPaintListener(this::paintBar);
50+
}
51+
52+
/**
53+
* Sets the percentage value (0–100) and redraws the bar.
54+
*
55+
* @param percentage the percentage used, clamped to [0, 100]
56+
*/
57+
public void setPercentage(int percentage) {
58+
this.percentage = Math.max(0, Math.min(100, percentage));
59+
barCanvas.redraw();
60+
}
61+
62+
/**
63+
* Returns the current percentage value.
64+
*/
65+
protected int getPercentage() {
66+
return percentage;
67+
}
68+
69+
/**
70+
* Returns the background track color for the unused portion of the bar.
71+
*/
72+
protected abstract Color getTrackColor();
73+
74+
/**
75+
* Returns the fill color for the used portion of the bar based on the current percentage.
76+
*/
77+
protected abstract Color getFillColor();
78+
79+
private void paintBar(PaintEvent e) {
80+
GC gc = e.gc;
81+
gc.setAdvanced(true);
82+
gc.setAntialias(SWT.ON);
83+
84+
Rectangle bounds = barCanvas.getClientArea();
85+
int width = bounds.width;
86+
int height = bounds.height;
87+
int arc = height;
88+
89+
// Draw remaining background
90+
gc.setBackground(getTrackColor());
91+
gc.fillRoundRectangle(0, 0, width, height, arc, arc);
92+
93+
// Draw filled portion
94+
if (percentage > 0) {
95+
int fillWidth = (int) Math.round(width * percentage / 100.0);
96+
fillWidth = Math.max(arc, Math.min(fillWidth, width));
97+
98+
gc.setBackground(getFillColor());
99+
gc.fillRoundRectangle(0, 0, fillWidth, height, arc, arc);
100+
}
101+
}
102+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.microsoft.copilot.eclipse.ui.swt;
2+
3+
import org.eclipse.swt.graphics.Color;
4+
import org.eclipse.swt.widgets.Composite;
5+
6+
/**
7+
* A usage bar colored to match the {@link com.microsoft.copilot.eclipse.ui.chat.contextwindow.ContextSizeDonut} color
8+
* scheme:
9+
* <ul>
10+
* <li>Filled color when percentage &lt; 90%</li>
11+
* <li>Warning color when percentage &ge; 90%</li>
12+
* </ul>
13+
* The unused portion is rendered using the donut track color.
14+
*/
15+
public class ContextWindowBar extends AbstractUsageBar {
16+
17+
private static final int THRESHOLD_WARNING = 90;
18+
19+
private Color colorFilled;
20+
private Color colorWarning;
21+
private Color colorTrack;
22+
23+
/**
24+
* Creates a new ContextWindowBar.
25+
*
26+
* @param parent the parent composite
27+
* @param style the SWT style bits
28+
*/
29+
public ContextWindowBar(Composite parent, int style) {
30+
super(parent, style);
31+
colorFilled = CssConstants.getDonutFilledColor(getDisplay());
32+
colorWarning = CssConstants.getDonutWarningColor(getDisplay());
33+
colorTrack = CssConstants.getDonutTrackColor(getDisplay());
34+
}
35+
36+
@Override
37+
protected Color getTrackColor() {
38+
return colorTrack;
39+
}
40+
41+
@Override
42+
protected Color getFillColor() {
43+
if (getPercentage() >= THRESHOLD_WARNING) {
44+
return colorWarning;
45+
}
46+
return colorFilled;
47+
}
48+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.microsoft.copilot.eclipse.ui.swt;
2+
3+
import org.eclipse.swt.graphics.Color;
4+
import org.eclipse.swt.widgets.Composite;
5+
6+
/**
7+
* A usage bar colored for Copilot quota display:
8+
* <ul>
9+
* <li>Blue (#3574F0) when percentage &lt; 75%</li>
10+
* <li>Yellow (#FFB824) when 75% &le; percentage &lt; 90%</li>
11+
* <li>Red (#E05151) when percentage &ge; 90%</li>
12+
* </ul>
13+
* The unused portion is rendered in gray (#DFE1E5).
14+
*/
15+
public class CopilotUsageBar extends AbstractUsageBar {
16+
17+
private static final int THRESHOLD_WARNING = 75;
18+
private static final int THRESHOLD_CRITICAL = 90;
19+
20+
private Color colorActive;
21+
private Color colorApproaching;
22+
private Color colorExhausted;
23+
private Color colorRemaining;
24+
25+
/**
26+
* Creates a new CopilotUsageBar.
27+
*
28+
* @param parent the parent composite
29+
* @param style the SWT style bits
30+
*/
31+
public CopilotUsageBar(Composite parent, int style) {
32+
super(parent, style);
33+
colorActive = CssConstants.getUsageBarActiveColor(getDisplay());
34+
colorApproaching = CssConstants.getUsageBarApproachingColor(getDisplay());
35+
colorExhausted = CssConstants.getUsageBarExhaustedColor(getDisplay());
36+
colorRemaining = CssConstants.getUsageBarRemainingColor(getDisplay());
37+
}
38+
39+
@Override
40+
protected Color getTrackColor() {
41+
return colorRemaining;
42+
}
43+
44+
@Override
45+
protected Color getFillColor() {
46+
if (getPercentage() >= THRESHOLD_CRITICAL) {
47+
return colorExhausted;
48+
} else if (getPercentage() >= THRESHOLD_WARNING) {
49+
return colorApproaching;
50+
}
51+
return colorActive;
52+
}
53+
}

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/CssConstants.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,32 @@ public static Color getDonutTrackColor(Display display) {
185185
return new Color(display, 223, 225, 229); // #DFE1E5
186186
}
187187

188+
/**
189+
* Returns the active (blue) fill color for the usage bar.
190+
*/
191+
public static Color getUsageBarActiveColor(Display display) {
192+
return new Color(display, 53, 116, 240);
193+
}
194+
195+
/**
196+
* Returns the approaching (yellow) fill color for the usage bar.
197+
*/
198+
public static Color getUsageBarApproachingColor(Display display) {
199+
return new Color(display, 255, 184, 36);
200+
}
201+
202+
/**
203+
* Returns the exhausted (red) fill color for the usage bar.
204+
*/
205+
public static Color getUsageBarExhaustedColor(Display display) {
206+
return new Color(display, 224, 81, 81);
207+
}
208+
209+
/**
210+
* Returns the remaining (gray) track color for the usage bar.
211+
*/
212+
public static Color getUsageBarRemainingColor(Display display) {
213+
return new Color(display, 223, 225, 229);
214+
}
215+
188216
}

0 commit comments

Comments
 (0)