Skip to content
Open
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 @@ -478,6 +478,28 @@ public CompletableFuture<LanguageModelToolConfirmationResult> 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();
}
Comment on lines +481 to +488
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are now multiple copies of the same "walk up the parent chain to find ChatContentViewer" logic (here and in InvokeToolConfirmationDialog.scrollToCancel). To reduce duplication and make future scrolling changes safer, consider extracting a shared helper (e.g., SwtUtils.findParentOfType(Control, Class) or a ChatContentViewer.findAncestor(Control) utility).

Copilot uses AI. Check for mistakes.
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment on lines +207 to +213
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processTurnEvent calls forceScrollToBottom() for every agent-round progress event. Since forceScrollToBottom() schedules delayed timerExec callbacks, this can enqueue a large number of UI-thread tasks during streaming/rapid progress updates and cause jank. Consider debouncing/coalescing forced scroll requests (e.g., keep one pending scheduled scroll per viewer/turn) or only forcing scroll when an actual user-action prompt is rendered.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Auto-scroll to bottom if enabled for regular chat-mode responses
if (shouldAutoScrollToBottom()) {
scrollToBottom();
}
}

String errMsg = value.getErrorMessage();
Expand Down Expand Up @@ -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.
Comment on lines +408 to +410
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc for forceScrollToBottom() is written as very long single lines and is likely to violate the repo's Checkstyle LineLength (max 120). Please wrap the Javadoc text onto multiple lines to satisfy the 120-char limit.

Suggested change
* 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.
* 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.

Copilot uses AI. Check for mistakes.
*/
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);
Comment on lines +417 to +421
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forceScrollToBottom() temporarily overwrites autoScrollEnabled and restores it later via a delayed timer. This can clobber the user's scroll state if they manually scroll during the delay, and it can also restore the wrong value if forceScrollToBottom() is invoked again before the 350ms timer runs. Consider avoiding mutation of autoScrollEnabled here, or restoring conditionally (e.g., only if it wasn't changed by user input) / using a request counter so only the latest invocation restores state.

Copilot uses AI. Check for mistakes.

// 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
Comment on lines +432 to +439
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forceScrollToBottom() schedules delayed timerExec callbacks but the 350ms callback catches a generic Exception and ignores it. This makes real SWT issues harder to diagnose and can mask failures. Since doScrollToBottom() already guards disposal, consider removing the catch entirely or at least logging unexpected exceptions (and also ensure restoration of autoScrollEnabled happens even if the timer callback is never reached).

Suggested change
try {
if (!this.isDisposed()) {
doScrollToBottom();
}
} catch (Exception ignore) {
// ignore
} finally {
// Restore previous state after the delayed scrolls
if (!this.isDisposed()) {
doScrollToBottom();
}
});
SwtUtils.getDisplay().timerExec(350, () -> {
if (!this.isDisposed()) {
// Restore previous state after the delayed scrolls.

Copilot uses AI. Check for mistakes.
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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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.
*/
Expand Down
Loading