Skip to content

Commit 567e669

Browse files
committed
fix ui deadlock and infinite retry loop on 402 quota error
1 parent 63b3c1b commit 567e669

3 files changed

Lines changed: 33 additions & 9 deletions

File tree

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,17 @@ public void processTurnEvent(ChatProgressValue value) {
217217
errMsg = Messages.chat_model_unsupported_message;
218218
}
219219
if (StringUtils.isNotEmpty(errMsg)) {
220-
// TODO: remove this error message replacement if statement when the CLS side warn message is aligned.
221-
if (value.getCode() == 402) {
220+
// Check once whether we're already on the fallback model
221+
CopilotModel fallback = this.serviceManager.getModelService().getFallbackModel();
222+
CopilotModel current = this.serviceManager.getModelService().getActiveModel();
223+
boolean alreadyOnFallback = fallback != null && current != null
224+
&& fallback.getModelName().equals(current.getModelName());
225+
226+
// Only replace the error message with "switched to fallback" when we're NOT already
227+
// on the fallback model. When already on fallback, show the original CLS message.
228+
if (value.getCode() == 402 && !alreadyOnFallback) {
222229
CopilotPlan userPlan = this.serviceManager.getAuthStatusManager().getQuotaStatus().copilotPlan();
223-
CopilotModel fallbackModel = this.serviceManager.getModelService().getFallbackModel();
224-
String fallbackModelName = fallbackModel != null ? fallbackModel.getModelName()
230+
String fallbackModelName = fallback != null ? fallback.getModelName()
225231
: Messages.chat_noQuotaView_fallbackModel;
226232

227233
if (MenuUtils.isCfiPlan(userPlan)) {
@@ -235,15 +241,19 @@ public void processTurnEvent(ChatProgressValue value) {
235241

236242
renderWarnMessageWithUpgradePlanButton(errMsg, value.getCode());
237243

238-
if (value.getCode() == 402
244+
// Switch to fallback model and auto-retry, but only if not already on the fallback model
245+
// to avoid an infinite loop of 402 → switch → resend → 402 → ...
246+
if (value.getCode() == 402 && !alreadyOnFallback
239247
&& this.serviceManager.getAuthStatusManager().getQuotaStatus().copilotPlan() != CopilotPlan.free) {
240248
this.serviceManager.getModelService().setFallBackModelAsActiveModel();
241249
this.serviceManager.getAuthStatusManager().checkQuota();
242250

243-
String previousInput = this.serviceManager.getUserPreferenceService().getPreviousInput(StringUtils.EMPTY);
251+
String previousInput =
252+
this.serviceManager.getUserPreferenceService().getPreviousInput(StringUtils.EMPTY);
244253
if (StringUtils.isNotEmpty(previousInput)) {
245254
IEventBroker eventBroker = PlatformUI.getWorkbench().getService(IEventBroker.class);
246-
Map<String, Object> properties = Map.of("previousInput", previousInput, "needCreateUserTurn", false);
255+
Map<String, Object> properties =
256+
Map.of("previousInput", previousInput, "needCreateUserTurn", false);
247257
eventBroker.post(CopilotEventConstants.TOPIC_CHAT_ON_SEND, properties);
248258
}
249259
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
* A custom widget that displays a turn for the copilot.
2626
*/
2727
public class CopilotTurnWidget extends BaseTurnWidget {
28+
29+
private Label modelInfoLabel;
30+
2831
/**
2932
* Create the widget.
3033
*/
@@ -71,8 +74,11 @@ public void renderModelInfo(String modelName, double billingMultiplier) {
7174
if (footer == null || footer.isDisposed()) {
7275
createFooter();
7376
}
77+
if (modelInfoLabel != null && !modelInfoLabel.isDisposed()) {
78+
modelInfoLabel.dispose();
79+
}
7480
if (StringUtils.isNotBlank(modelName)) {
75-
Label modelInfoLabel = new Label(footer, SWT.NONE);
81+
modelInfoLabel = new Label(footer, SWT.NONE);
7682
// When token-based billing is enabled on the language server, the per-turn billing
7783
// multiplier is no longer a meaningful price signal, so render the model name on its
7884
// own. Fall back to the legacy "{model} - {multiplier}" format otherwise.
@@ -91,6 +97,9 @@ public void renderModelInfo(String modelName, double billingMultiplier) {
9197
modelInfoLabel.setLayoutData(labelGridData);
9298
modelInfoLabel.setData(CssConstants.CSS_CLASS_NAME_KEY, "model-info-label");
9399

100+
// Ensure footer stays at the bottom of the turn widget, even when
101+
// warn widgets are appended after it (e.g. quota fallback retry).
102+
footer.moveBelow(null);
94103
footer.requestLayout();
95104
}
96105
}, this);

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelService.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.HashMap;
88
import java.util.List;
99
import java.util.Map;
10+
import java.util.concurrent.CompletableFuture;
1011
import java.util.concurrent.ExecutionException;
1112

1213
import org.apache.commons.lang3.StringUtils;
@@ -348,7 +349,11 @@ public void setActiveModel(String modelName) {
348349
// Persist using the composite key for proper identification
349350
UserPreference preference = getUserPreference();
350351
preference.setChatModel(compositeKey);
351-
persistUserPreference();
352+
// Persist asynchronously to avoid deadlock: persistUserPreference() calls
353+
// persistence().get() which blocks waiting for the LSP listener thread.
354+
// If called on the UI thread while the listener is in syncExec, both threads
355+
// deadlock.
356+
CompletableFuture.runAsync(this::persistUserPreference);
352357

353358
// Update observable
354359
ensureRealm(() -> activeModelObservable.setValue(model));

0 commit comments

Comments
 (0)