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 @@ -23,6 +23,7 @@
import org.eclipse.ui.PlatformUI;
import org.osgi.service.event.EventHandler;

import com.microsoft.copilot.eclipse.core.Constants;
import com.microsoft.copilot.eclipse.core.CopilotCore;
import com.microsoft.copilot.eclipse.core.chat.ConfirmationContent;
import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants;
Expand All @@ -36,6 +37,7 @@
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.EditAgentRoundData;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ReplyData;
import com.microsoft.copilot.eclipse.core.persistence.CopilotTurnData.ToolCallData;
import com.microsoft.copilot.eclipse.ui.CopilotUi;
import com.microsoft.copilot.eclipse.ui.chat.services.AvatarService;
import com.microsoft.copilot.eclipse.ui.chat.services.ChatServiceManager;
import com.microsoft.copilot.eclipse.ui.utils.SwtUtils;
Expand Down Expand Up @@ -604,16 +606,16 @@ protected void ensureFooterAtBottom() {
* @param code the server error code
* @param modelProviderName the BYOK model-provider name, or {@code null} for built-in models
*/
protected void createWarnDialog(String message, int code, String modelProviderName) {
protected Composite createWarnDialog(String message, int code, String modelProviderName) {
// TODO: Remove this legacy fallback after TBB is officially released.
// When the language server has not enabled token-based billing yet, restore the original
// main-branch warning behavior (no plan-driven actions; single upgrade button on the legacy
// 30-day free trial message).
if (!this.serviceManager.getAuthStatusManager().getQuotaStatus().tokenBasedBillingEnabled()) {
new WarnWidget(this, SWT.BOTTOM, message, code);
WarnWidget warnWidget = new WarnWidget(this, SWT.BOTTOM, message, code);
ensureFooterAtBottom();
requestLayout();
return;
return warnWidget;
}
boolean byokQuotaExceeded = QuotaActions.isByokQuotaExceeded(code, modelProviderName);
String displayMessage = byokQuotaExceeded ? Messages.chat_warnWidget_byokQuotaUsageMessage : message;
Expand All @@ -627,9 +629,11 @@ protected void createWarnDialog(String message, int code, String modelProviderNa
&& quotaStatus.premiumInteractions().overagePermitted();
canUpgradePlan = quotaStatus.canUpgradePlan();
}
new WarnWidget(this, SWT.NONE, displayMessage, planForActions, overageEnabled, canUpgradePlan);
WarnWidget warnWidget =
new WarnWidget(this, SWT.NONE, displayMessage, planForActions, overageEnabled, canUpgradePlan);
ensureFooterAtBottom();
requestLayout();
return warnWidget;
}

/**
Expand Down Expand Up @@ -666,6 +670,19 @@ public CompletableFuture<LanguageModelToolConfirmationResult> requestToolExecuti

this.getParent().requestLayout();

// Ensure the chat content viewer scrolls to show the newly created confirmation
// dialog. Walk up the composite hierarchy to find a ChatContentViewer
// and request scrolling. Use async exec because layout needs to complete first.
SwtUtils.invokeOnDisplayThreadAsync(() -> {
ChatContentViewer viewer = SwtUtils.findParentOfType(this.getParent(), ChatContentViewer.class);
if (viewer != null) {
if (this.confirmDialog != null && !this.confirmDialog.isDisposed()) {
viewer.showControl(this.confirmDialog);
}
}

}, this.getParent());

return toolConfirmationFuture;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,9 +468,17 @@ public BaseTurnWidget getTurnWidget(String turnId) {
}

private void renderWarnMessageWithUpgradePlanButton(String errorMessage, int code, String modelProviderName) {
latestTurnWidget.createWarnDialog(errorMessage, code, modelProviderName);
Composite warnWidget = latestTurnWidget.createWarnDialog(errorMessage, code, modelProviderName);
refreshLayoutFull();
scrollToLatestUserTurn();
// Ensure the chat content viewer scrolls to show the newly created warning banner. Walk up the composite hierarchy
// to find a ChatContentViewer and request scrolling. Use async exec because layout needs to complete first.
SwtUtils.invokeOnDisplayThreadAsync(() -> {
if (warnWidget != null && !warnWidget.isDisposed()) {
showControl(warnWidget);
}

}, this.getParent());
}

/**
Expand All @@ -483,6 +491,12 @@ public void renderErrorMessage(String errorMessage) {
this.errorWidget = new ErrorWidget(cmpContent, SWT.BOTTOM, errorMessage);
refreshLayoutFull();
scrollToLatestUserTurn();
// Ensure the chat content viewer scrolls to show the newly created error banner.
SwtUtils.invokeOnDisplayThreadAsync(() -> {
if (this.errorWidget != null && !this.errorWidget.isDisposed()) {
this.showControl(this.errorWidget);
}
}, this.getParent());
}

/**
Expand Down Expand Up @@ -770,6 +784,50 @@ private int topOf(Control target) {
return running;
}

/**
* Scrolls the viewport to make {@code target} visible, equivalent to
* {@link org.eclipse.swt.custom.ScrolledComposite#showControl(Control)}.
*
* <p>Walks up the widget tree to find the direct child of {@code cmpContent} that contains
* {@code target}, computes the content-coordinate position of {@code target} by summing
* the turn's {@link #topOf} value with the local y offsets down to {@code target}, then
* adjusts {@link #scrollOffset} by the minimum amount needed to bring {@code target} fully
* into the viewport.</p>
*/
public void showControl(Composite target) {
if (target == null || target.isDisposed()) {
return;
}
// Walk up to find the direct child of cmpContent that is the ancestor of target.
Control ancestor = target;
while (ancestor != null && ancestor.getParent() != cmpContent) {
ancestor = ancestor.getParent();
}
if (ancestor == null || ancestor.getParent() != cmpContent) {
return;
}
// Content-coordinate top of the enclosing turn widget.
int ancestorTop = topOf(ancestor);
// Accumulate the local y offset by walking from target up to (but not including) ancestor.
int localY = 0;
for (Control c = target; c != ancestor; c = c.getParent()) {
localY += c.getLocation().y;
}
int targetTop = ancestorTop + localY;
int targetBottom = targetTop + target.getSize().y;
int viewport = getClientArea().height;
// Scroll the minimum amount: down if target is below the visible area, up if above.
int newOffset = scrollOffset;
if (targetBottom > scrollOffset + viewport) {
newOffset = targetBottom - viewport;
}
if (targetTop < newOffset) {
newOffset = targetTop;
}
scrollOffset = clampOffset(newOffset);
relayoutWindow();
}

@Override
public void dispose() {
pendingEvents.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,30 @@ private SwtUtils() {
}

private static final String INLINE_ANNOTATION_COLOR_KEY = "org.eclipse.ui.editors.inlineAnnotationColor";

private static final int DEFAULT_GHOST_TEXT_SCALE = 128;

/**
* Walks up the parent chain of the given control and returns the first ancestor that is an instance of the specified
* type, or {@code null} if none is found.
*
* @param <T> the target type
* @param control the starting control (may be {@code null})
* @param type the class to search for
* @return the first matching ancestor, or {@code null}
*/
@Nullable
public static <T> T findParentOfType(Control control, Class<T> type) {
Comment thread
raghucssit marked this conversation as resolved.
Control current = control;
while (current != null) {
if (type.isInstance(current)) {
return type.cast(current);
}
current = current.getParent();
}
return null;
}

/**
* Invokes the given runnable on the display thread.
*/
Expand Down
Loading