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 66e64742..c067694f 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 @@ -478,6 +478,28 @@ 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(() -> { + Control parent = this.getParent(); + while (parent != null && !(parent instanceof ChatContentViewer)) { + parent = parent.getParent(); + } + if (parent instanceof ChatContentViewer viewer) { + 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..a1bb3b8f 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 @@ -204,9 +204,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(); @@ -375,12 +383,75 @@ private boolean shouldAutoScrollToBottom() { /** * Scroll to the bottom. + * Made public so child widgets can request scrolling when they show dialogs or + * other interactive controls that should be visible to the user. */ - private void scrollToBottom() { - ScrollBar verticalBar = this.getVerticalBar(); - if (verticalBar != null) { - this.setOrigin(0, verticalBar.getMaximum()); + public void scrollToBottom() { + // Ensure layout is settled and compute an explicit origin based on content size + // rather than relying on scroll bar metrics which can be inconsistent during + // rapid layout changes (dialog creation/disposal). Run on UI thread. + SwtUtils.invokeOnDisplayThreadAsync(() -> { + 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); + }, this); + } + + /** + * 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. The implementation + * performs a two-phase scroll to be robust against layout timing issues. + */ + public void forceScrollToBottom() { + SwtUtils.invokeOnDisplayThreadAsync(() -> { + if (this.isDisposed()) { + return; + } + // Temporarily enable auto-scroll to allow setOrigin to take effect. + boolean previousAuto = this.autoScrollEnabled; + this.autoScrollEnabled = true; + // Run layout on content to ensure sizes are accurate before measuring. + cmpContent.layout(true, true); + + // Initial immediate scroll + doScrollToBottom(); + + // Schedule follow-up scrolls after short delays to handle any remaining + // pending layout operations or asynchronous children updates. + SwtUtils.getDisplay().timerExec(120, () -> { + doScrollToBottom(); + }); + SwtUtils.getDisplay().timerExec(350, () -> { + try { + if (!this.isDisposed()) { + doScrollToBottom(); + } + } catch (Exception ignore) { + // ignore + } finally { + // Restore previous state after the delayed scrolls + this.autoScrollEnabled = previousAuto; + } + }); + }, this); + } + + private void doScrollToBottom() { + if (this.isDisposed()) { + return; } + Rectangle clientArea = this.getClientArea(); + Point containerSize = cmpContent.computeSize(clientArea.width, SWT.DEFAULT); + int contentHeight = containerSize.y; + int originY = Math.max(0, contentHeight - clientArea.height); + this.setOrigin(0, originY); } /** 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..7fc590db 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,24 @@ 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) { + org.eclipse.swt.widgets.Control p = parent; + while (p != null && !(p instanceof ChatContentViewer)) { + p = p.getParent(); + } + if (p instanceof ChatContentViewer viewer) { + viewer.refreshScrollerLayout(); + viewer.forceScrollToBottom(); + } + } + /** * Apply the chat font (bold) to the title label. */