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. */