From 01672b429733d1d0c178c01e4f84aa30ba2c4ff5 Mon Sep 17 00:00:00 2001 From: raghucssit Date: Mon, 27 Apr 2026 17:12:00 +0200 Subject: [PATCH] In Agent mode: Auto scroll to prompts like 'Continue'. User must be made aware some action is needed from them to continue the work by agent. Usually this happens when "Too many requests" or "Command line run prompt" etc. see https://github.com/microsoft/copilot-eclipse-feedback/issues/184 --- .../eclipse/ui/chat/BaseTurnWidget.java | 19 +++++++ .../eclipse/ui/chat/ChatContentViewer.java | 49 ++++++++++++++++--- .../ui/chat/InvokeToolConfirmationDialog.java | 15 ++++++ .../copilot/eclipse/ui/utils/SwtUtils.java | 22 +++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java index e1de0abc..18fb062a 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java @@ -590,6 +590,25 @@ public CompletableFuture requestToolExecuti this.getParent().layout(); + // Ensure the chat content viewer scrolls to show the newly created confirmation + // dialog/footer area. 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) { + viewer.refreshScrollerLayout(); + // Prefer showing the specific confirmation dialog control if available + if (this.confirmDialog != null && !this.confirmDialog.isDisposed()) { + viewer.showControl(this.confirmDialog); + } else { + // Fallback: force-scrolling to bottom + viewer.forceScrollToBottom(); + } + } + + }, this.getParent()); + + return toolConfirmationFuture; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java index a36a66cb..120b64d6 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java @@ -3,6 +3,7 @@ package com.microsoft.copilot.eclipse.ui.chat; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -11,6 +12,7 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.e4.core.services.events.IEventBroker; +import org.eclipse.jface.util.Throttler; import org.eclipse.lsp4j.WorkDoneProgressKind; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; @@ -59,6 +61,8 @@ public class ChatContentViewer extends ScrolledComposite { private BaseTurnWidget latestTurnWidget; // Auto-scroll state management private boolean autoScrollEnabled; + // Throttler for forceScrollToBottom – coalesces rapid calls during streaming + private Throttler forceScrollThrottler; /** * Create the composite. @@ -111,6 +115,12 @@ public void controlResized(ControlEvent e) { this.serviceManager = serviceManager; this.autoScrollEnabled = true; + this.forceScrollThrottler = new Throttler(this.getDisplay(), Duration.ofMillis(350), () -> { + if (!this.isDisposed()) { + cmpContent.layout(true, true); + scrollToBottom(); + } + }); } /** @@ -204,9 +214,17 @@ public void processTurnEvent(ChatProgressValue value) { } refreshScrollerLayout(); - // Auto-scroll to bottom if enabled - if (shouldAutoScrollToBottom()) { - scrollToBottom(); + // For agent-mode responses (agent rounds/tool calls) we always force the view + // to scroll to the bottom so prompts that require user action (e.g. Continue, + // permission dialogs) are visible even if the user previously scrolled away. + if (value.getAgentRounds() != null && !value.getAgentRounds().isEmpty()) { + // Use a forced scroll to ensure visibility regardless of manual scroll state. + forceScrollToBottom(); + } else { + // Auto-scroll to bottom if enabled for regular chat-mode responses + if (shouldAutoScrollToBottom()) { + scrollToBottom(); + } } String errMsg = value.getErrorMessage(); @@ -377,10 +395,29 @@ private boolean shouldAutoScrollToBottom() { * Scroll to the bottom. */ private void scrollToBottom() { - ScrollBar verticalBar = this.getVerticalBar(); - if (verticalBar != null) { - this.setOrigin(0, verticalBar.getMaximum()); + if (this.isDisposed()) { + return; } + + Rectangle clientArea = this.getClientArea(); + // compute content height with current client width + Point containerSize = cmpContent.computeSize(clientArea.width, SWT.DEFAULT); + int contentHeight = containerSize.y; + int originY = Math.max(0, contentHeight - clientArea.height); + this.setOrigin(0, originY); + } + + /** + * Force the view to scroll to the bottom regardless of the user's manual scroll state. This is + * used for important UI prompts (like tool confirmations) to ensure they are visible even if the + * user had scrolled away. + *

+ * Calls are throttled via {@link Throttler}: rapid successive calls (e.g. during streaming + * agent-round progress events) are coalesced so that only one scroll task fires per burst, + * avoiding jank. + */ + public void forceScrollToBottom() { + forceScrollThrottler.throttledExec(); } /** diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/InvokeToolConfirmationDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/InvokeToolConfirmationDialog.java index 745f62a5..5ac6de11 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/InvokeToolConfirmationDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/InvokeToolConfirmationDialog.java @@ -203,6 +203,11 @@ private void createButtons() { // Check if parent is still valid before using it if (parent != null && !parent.isDisposed()) { parent.layout(); + // Ensure the chat content viewer scrolls to bottom after layout so that any + // newly revealed content is visible to the user. + SwtUtils.invokeOnDisplayThreadAsync(() -> { + scrollToCancel(parent); + }, parent); } }); continueButton.setData(CssConstants.CSS_CLASS_NAME_KEY, "btn-primary"); @@ -245,11 +250,21 @@ public void cancelConfirmation() { // Check if parent is still valid before using it if (parent != null && !parent.isDisposed()) { parent.layout(); + // Scroll to bottom to reveal cancel label if it was created + scrollToCancel(parent); } }, this); } } + private void scrollToCancel(Composite parent) { + ChatContentViewer viewer = SwtUtils.findParentOfType(parent, ChatContentViewer.class); + if (viewer != null) { + viewer.refreshScrollerLayout(); + viewer.forceScrollToBottom(); + } + } + /** * Apply the chat font (bold) to the title label. */ diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java index ea44da6f..3d9e2fac 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/SwtUtils.java @@ -38,8 +38,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 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 findParentOfType(Control control, Class type) { + 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. */