Skip to content
Merged
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 @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -325,7 +342,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
}

private handleScroll(scrollTop: number): void {
if (!this.scrollableElement) {
if (!this.scrollableElement || this.isUpdatingDimensions) {
return;
}

Expand All @@ -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;
}

Expand All @@ -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 });
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1129,7 +1191,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
this.appendToWrapper(itemWrapper);

if (this.fixedScrollingMode && this.scrollableElement) {
setTimeout(() => this.scrollToBottomIfEnabled(), 0);
this.syncDimensionsAndScheduleScroll();
}
}

Expand Down Expand Up @@ -1241,6 +1303,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
this.workingSpinnerElement = undefined;
this.workingSpinnerLabel = undefined;
}
this.pendingScrollDisposable?.dispose();
super.dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,8 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
truncatedCommand,
contentElement,
context,
initialExpanded
initialExpanded,
isComplete
));
this._thinkingCollapsibleWrapper = wrapper;

Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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');

Expand All @@ -1543,14 +1556,25 @@ 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;

labelElement.appendChild(ranText);
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);
Expand Down
Loading