mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
fix scrolling as content expands in thinking (#291750)
* fix scrolling as content expands in thinking * add disposables?
This commit is contained in:
+71
-8
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $, clearNode, hide } from '../../../../../../base/browser/dom.js';
|
||||
import { $, clearNode, getWindow, hide, scheduleAtNextAnimationFrame } from '../../../../../../base/browser/dom.js';
|
||||
import { alert } from '../../../../../../base/browser/ui/aria/aria.js';
|
||||
import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollbar/scrollableElement.js';
|
||||
import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js';
|
||||
@@ -151,6 +151,9 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
private readonly toolWrappersByCallId = new Map<string, HTMLElement>();
|
||||
private readonly toolDisposables = this._register(new DisposableMap<string, DisposableStore>());
|
||||
private pendingRemovals: { toolCallId: string; toolLabel: string }[] = [];
|
||||
private pendingScrollDisposable: IDisposable | undefined;
|
||||
private mutationObserverDisposable: IDisposable | undefined;
|
||||
private isUpdatingDimensions: boolean = false;
|
||||
|
||||
private getRandomWorkingMessage(): string {
|
||||
if (this.availableWorkingMessages.length === 0) {
|
||||
@@ -310,11 +313,25 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
}));
|
||||
this._register(this.scrollableElement.onScroll(e => this.handleScroll(e.scrollTop)));
|
||||
|
||||
// check for content changes to update scroll dimensions
|
||||
const mutationObserver = new MutationObserver(() => {
|
||||
if (!this.streamingCompleted) {
|
||||
this.syncDimensionsAndScheduleScroll();
|
||||
}
|
||||
});
|
||||
mutationObserver.observe(this.wrapper, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
this.mutationObserverDisposable = { dispose: () => mutationObserver.disconnect() };
|
||||
this._register(this.mutationObserverDisposable);
|
||||
|
||||
this._register(this._onDidChangeHeight.event(() => {
|
||||
setTimeout(() => this.scrollToBottomIfEnabled(), 0);
|
||||
this.syncDimensionsAndScheduleScroll();
|
||||
}));
|
||||
|
||||
setTimeout(() => this.scrollToBottomIfEnabled(), 0);
|
||||
this.syncDimensionsAndScheduleScroll();
|
||||
|
||||
this.updateDropdownClickability();
|
||||
return this.scrollableElement.getDomNode();
|
||||
@@ -325,7 +342,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
}
|
||||
|
||||
private handleScroll(scrollTop: number): void {
|
||||
if (!this.scrollableElement) {
|
||||
if (!this.scrollableElement || this.isUpdatingDimensions) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -340,8 +357,39 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
}
|
||||
}
|
||||
|
||||
private scrollToBottomIfEnabled(): void {
|
||||
if (!this.scrollableElement || !this.autoScrollEnabled) {
|
||||
// try to schedule scroll
|
||||
private syncDimensionsAndScheduleScroll(): void {
|
||||
if (this.autoScrollEnabled && this.scrollableElement) {
|
||||
this.isUpdatingDimensions = true;
|
||||
try {
|
||||
this.updateScrollDimensions();
|
||||
this.scrollToBottom();
|
||||
} finally {
|
||||
this.isUpdatingDimensions = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// debounce animation
|
||||
if (this.pendingScrollDisposable) {
|
||||
return;
|
||||
}
|
||||
this.pendingScrollDisposable = scheduleAtNextAnimationFrame(getWindow(this.domNode), () => {
|
||||
this.pendingScrollDisposable = undefined;
|
||||
if (this._store.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this.isUpdatingDimensions = true;
|
||||
try {
|
||||
this.updateScrollDimensions();
|
||||
} finally {
|
||||
this.isUpdatingDimensions = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateScrollDimensions(): void {
|
||||
if (!this.scrollableElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -359,6 +407,15 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
height: viewportHeight,
|
||||
scrollHeight: contentHeight
|
||||
});
|
||||
}
|
||||
|
||||
private scrollToBottom(): void {
|
||||
if (!this.scrollableElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentHeight = this.wrapper.scrollHeight;
|
||||
const viewportHeight = Math.min(contentHeight, THINKING_SCROLL_MAX_HEIGHT);
|
||||
|
||||
if (contentHeight > viewportHeight) {
|
||||
this.scrollableElement.setScrollPosition({ scrollTop: contentHeight - viewportHeight });
|
||||
@@ -515,7 +572,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
this.renderMarkdown(next, reuseExisting);
|
||||
|
||||
if (this.fixedScrollingMode && this.scrollableElement) {
|
||||
setTimeout(() => this.scrollToBottomIfEnabled(), 0);
|
||||
this.syncDimensionsAndScheduleScroll();
|
||||
}
|
||||
|
||||
const extractedTitle = extractTitleFromThinkingContent(raw);
|
||||
@@ -561,6 +618,11 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
}
|
||||
this.streamingCompleted = true;
|
||||
|
||||
if (this.mutationObserverDisposable) {
|
||||
this.mutationObserverDisposable.dispose();
|
||||
this.mutationObserverDisposable = undefined;
|
||||
}
|
||||
|
||||
if (this.workingSpinnerElement) {
|
||||
this.workingSpinnerElement.remove();
|
||||
this.workingSpinnerElement = undefined;
|
||||
@@ -1129,7 +1191,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
this.appendToWrapper(itemWrapper);
|
||||
|
||||
if (this.fixedScrollingMode && this.scrollableElement) {
|
||||
setTimeout(() => this.scrollToBottomIfEnabled(), 0);
|
||||
this.syncDimensionsAndScheduleScroll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1241,6 +1303,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
this.workingSpinnerElement = undefined;
|
||||
this.workingSpinnerLabel = undefined;
|
||||
}
|
||||
this.pendingScrollDisposable?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
+27
-3
@@ -396,7 +396,8 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
||||
truncatedCommand,
|
||||
contentElement,
|
||||
context,
|
||||
initialExpanded
|
||||
initialExpanded,
|
||||
isComplete
|
||||
));
|
||||
this._thinkingCollapsibleWrapper = wrapper;
|
||||
|
||||
@@ -407,6 +408,10 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
||||
this._thinkingCollapsibleWrapper?.expand();
|
||||
}
|
||||
|
||||
public markCollapsibleWrapperComplete(): void {
|
||||
this._thinkingCollapsibleWrapper?.markComplete();
|
||||
}
|
||||
|
||||
private async _initializeTerminalActions(): Promise<void> {
|
||||
if (this._store.isDisposed) {
|
||||
return;
|
||||
@@ -638,6 +643,9 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
||||
this._addActions(terminalInstance, this._terminalData.terminalToolSessionId);
|
||||
const resolvedCommand = this._getResolvedCommand(terminalInstance);
|
||||
|
||||
// update title
|
||||
this.markCollapsibleWrapperComplete();
|
||||
|
||||
// Auto-collapse on success
|
||||
if (resolvedCommand?.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput) {
|
||||
this._toggleOutput(false);
|
||||
@@ -656,6 +664,8 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
|
||||
const resolvedImmediately = await tryResolveCommand();
|
||||
if (resolvedImmediately?.endMarker) {
|
||||
commandDetectionListener.clear();
|
||||
// update title
|
||||
this.markCollapsibleWrapperComplete();
|
||||
// Auto-collapse on success
|
||||
if (resolvedImmediately.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput) {
|
||||
this._toggleOutput(false);
|
||||
@@ -1515,19 +1525,22 @@ export class ContinueInBackgroundAction extends Action implements IAction {
|
||||
class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart {
|
||||
private readonly _terminalContentElement: HTMLElement;
|
||||
private readonly _commandText: string;
|
||||
private _isComplete: boolean;
|
||||
|
||||
constructor(
|
||||
commandText: string,
|
||||
contentElement: HTMLElement,
|
||||
context: IChatContentPartRenderContext,
|
||||
initialExpanded: boolean,
|
||||
isComplete: boolean,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
) {
|
||||
const title = `Ran \`${commandText}\``;
|
||||
const title = isComplete ? `Ran \`${commandText}\`` : `Running \`${commandText}\``;
|
||||
super(title, context, undefined, hoverService);
|
||||
|
||||
this._terminalContentElement = contentElement;
|
||||
this._commandText = commandText;
|
||||
this._isComplete = isComplete;
|
||||
|
||||
this.domNode.classList.add('chat-terminal-thinking-collapsible');
|
||||
|
||||
@@ -1543,7 +1556,10 @@ class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart
|
||||
const labelElement = this._collapseButton.labelElement;
|
||||
labelElement.textContent = '';
|
||||
|
||||
const ranText = document.createTextNode(localize('chat.terminal.ran.prefix', "Ran "));
|
||||
const prefixText = this._isComplete
|
||||
? localize('chat.terminal.ran.prefix', "Ran ")
|
||||
: localize('chat.terminal.running.prefix', "Running ");
|
||||
const ranText = document.createTextNode(prefixText);
|
||||
const codeElement = document.createElement('code');
|
||||
codeElement.textContent = this._commandText;
|
||||
|
||||
@@ -1551,6 +1567,14 @@ class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart
|
||||
labelElement.appendChild(codeElement);
|
||||
}
|
||||
|
||||
public markComplete(): void {
|
||||
if (this._isComplete) {
|
||||
return;
|
||||
}
|
||||
this._isComplete = true;
|
||||
this._setCodeFormattedTitle();
|
||||
}
|
||||
|
||||
protected override initContent(): HTMLElement {
|
||||
const listWrapper = dom.$('.chat-used-context-list.chat-terminal-thinking-content');
|
||||
listWrapper.appendChild(this._terminalContentElement);
|
||||
|
||||
Reference in New Issue
Block a user