Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
93b0c0c
fixes #271390
meganrogge Jan 5, 2026
ed10bcf
revert unneeded change
meganrogge Jan 5, 2026
bb5f9f1
register toDisposable
meganrogge Jan 5, 2026
c344f1f
use const enums
meganrogge Jan 5, 2026
1ec5df7
pass in xterm, terminal instance ref instead of terminal instance
meganrogge Jan 5, 2026
6ade6a0
go back to overriding dispose to fix disposable leak, add tests
meganrogge Jan 5, 2026
ad1a84f
go back to toDisposable
meganrogge Jan 5, 2026
9d426ab
don't pass in terminal location
meganrogge Jan 5, 2026
a25f4b1
fix bug
meganrogge Jan 5, 2026
97cf64d
add tests
meganrogge Jan 6, 2026
3ee2eeb
Merge branch 'main' into merogge/streaming-output-3
meganrogge Jan 6, 2026
e1782a7
fix height issues
meganrogge Jan 6, 2026
54a4607
Update src/vs/workbench/contrib/terminal/browser/chatTerminalCommandM…
meganrogge Jan 8, 2026
7bbe282
Update src/vs/workbench/contrib/terminal/browser/chatTerminalCommandM…
meganrogge Jan 8, 2026
ef48b00
Update src/vs/workbench/contrib/chat/browser/widget/chatContentParts/…
meganrogge Jan 8, 2026
3fc5325
Update src/vs/workbench/contrib/terminal/browser/terminal.ts
meganrogge Jan 8, 2026
ec88ac4
Update src/vs/workbench/contrib/terminal/browser/chatTerminalCommandM…
meganrogge Jan 8, 2026
0e61780
Merge branch 'main' into merogge/streaming-output-3
meganrogge Jan 8, 2026
65cf759
fix potential race problem
meganrogge Jan 8, 2026
3abd1d6
fix issue
meganrogge Jan 8, 2026
380b7fa
add streaming tests
meganrogge Jan 8, 2026
8638d57
use queueMicrotask
meganrogge Jan 8, 2026
4a2ad66
dispose of emitters
meganrogge Jan 8, 2026
a8c87c5
fix height issues for real
meganrogge Jan 8, 2026
c8f25ba
fix issues
meganrogge Jan 8, 2026
cc226ae
Update src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts
meganrogge Jan 10, 2026
4bb9241
Merge branch 'main' into merogge/streaming-output-3
meganrogge Jan 10, 2026
4cd1b1e
Merge branch 'main' into merogge/streaming-output-3
meganrogge Jan 12, 2026
cbf233d
implement scroll lock
meganrogge Jan 12, 2026
2091246
move comment into class
meganrogge Jan 12, 2026
577fcaa
rm isDisposed, use store.isDisposed
meganrogge Jan 12, 2026
d39241a
use isNumber
meganrogge Jan 12, 2026
ad86ac6
fixes #286593
meganrogge Jan 12, 2026
c7952b1
IBufferSet
meganrogge Jan 12, 2026
b31dad0
fix for start marker
meganrogge Jan 12, 2026
02090f2
enable input
meganrogge Jan 12, 2026
894f18b
enable input
meganrogge Jan 12, 2026
f36563a
simplify tests
meganrogge Jan 12, 2026
cfbea37
tweak height
meganrogge Jan 12, 2026
5c4976e
rm unneeded test
meganrogge Jan 12, 2026
557e47d
fix issue
meganrogge Jan 12, 2026
37a6ff7
enhance auto-expand behavior for terminal output based on content ava…
meganrogge Jan 12, 2026
b973117
fix bug, simplify
meganrogge Jan 12, 2026
e93018f
Merge branch 'main' into merogge/streaming-output-3
meganrogge Jan 12, 2026
e1bdf84
Merge branch 'main' into merogge/streaming-output-3
meganrogge Jan 12, 2026
a24bd7a
Rm unused
meganrogge Jan 12, 2026
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 @@ -20,7 +20,7 @@ import '../media/chatTerminalToolProgressPart.css';
import type { ICodeBlockRenderOptions } from '../codeBlockPart.js';
import { Action, IAction } from '../../../../../../../base/common/actions.js';
import { IChatTerminalToolProgressPart, ITerminalChatService, ITerminalConfigurationService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../../../../terminal/browser/terminal.js';
import { Disposable, MutableDisposable, toDisposable, type IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { Disposable, DisposableStore, MutableDisposable, toDisposable, type IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { Emitter } from '../../../../../../../base/common/event.js';
import { ThemeIcon } from '../../../../../../../base/common/themables.js';
import { DecorationSelector, getTerminalCommandDecorationState, getTerminalCommandDecorationTooltip } from '../../../../../terminal/browser/xterm/decorationStyles.js';
Expand Down Expand Up @@ -213,6 +213,8 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
private readonly _isSerializedInvocation: boolean;
private _terminalInstance: ITerminalInstance | undefined;
private readonly _decoration: TerminalCommandDecoration;
private _autoExpandTimeout: ReturnType<typeof setTimeout> | undefined;
private _userToggledOutput: boolean = false;

private markdownPart: ChatMarkdownContentPart | undefined;
public get codeblocks(): IChatCodeBlockInfo[] {
Expand Down Expand Up @@ -506,16 +508,51 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
return;
}

commandDetectionListener.value = commandDetection.onCommandFinished(() => {
const store = new DisposableStore();
store.add(commandDetection.onCommandExecuted(() => {
this._addActions(terminalInstance, this._terminalData.terminalToolSessionId);
// Auto-expand if there's output, checking periodically for up to 1 second
if (!this._outputView.isExpanded && !this._userToggledOutput && !this._autoExpandTimeout) {
let attempts = 0;
const maxAttempts = 5;
const checkForOutput = () => {
this._autoExpandTimeout = undefined;
if (this._store.isDisposed || this._outputView.isExpanded || this._userToggledOutput) {
return;
}
if (this._hasOutput(terminalInstance)) {
this._toggleOutput(true);
return;
}
attempts++;
if (attempts < maxAttempts) {
this._autoExpandTimeout = setTimeout(checkForOutput, 200);
}
};
this._autoExpandTimeout = setTimeout(checkForOutput, 200);
}
}));
store.add(commandDetection.onCommandFinished(() => {
this._addActions(terminalInstance, this._terminalData.terminalToolSessionId);
const resolvedCommand = this._getResolvedCommand(terminalInstance);

// Auto-collapse on success
if (resolvedCommand?.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput) {
this._toggleOutput(false);
}
if (resolvedCommand?.endMarker) {
commandDetectionListener.clear();
}
});
}));
commandDetectionListener.value = store;

const resolvedImmediately = await tryResolveCommand();
if (resolvedImmediately?.endMarker) {
commandDetectionListener.clear();
// Auto-collapse on success
if (resolvedImmediately.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput) {
this._toggleOutput(false);
}
return;
}
};
Expand Down Expand Up @@ -595,6 +632,10 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
}

private _handleDispose(): void {
if (this._autoExpandTimeout) {
clearTimeout(this._autoExpandTimeout);
this._autoExpandTimeout = undefined;
}
this._terminalOutputContextKey.reset();
this._terminalChatService.clearFocusedProgressPart(this);
}
Expand Down Expand Up @@ -623,6 +664,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
}

public async toggleOutputFromKeyboard(): Promise<void> {
this._userToggledOutput = true;
if (!this._outputView.isExpanded) {
await this._toggleOutput(true);
this.focusOutput();
Expand All @@ -632,6 +674,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
}

private async _toggleOutputFromAction(): Promise<void> {
this._userToggledOutput = true;
if (!this._outputView.isExpanded) {
await this._toggleOutput(true);
return;
Expand All @@ -646,17 +689,52 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart
this._focusChatInput();
}

private _hasOutput(terminalInstance: ITerminalInstance): boolean {
// Check for snapshot
if (this._terminalData.terminalCommandOutput?.text?.trim()) {
return true;
}
// Check for live output (cursor moved past executed marker)
const command = this._getResolvedCommand(terminalInstance);
if (!command?.executedMarker || terminalInstance.isDisposed) {
return false;
}
const buffer = terminalInstance.xterm?.raw.buffer.active;
if (!buffer) {
return false;
}
const cursorLine = buffer.baseY + buffer.cursorY;
return cursorLine > command.executedMarker.line;
}

private _resolveCommand(instance: ITerminalInstance): ITerminalCommand | undefined {
if (instance.isDisposed) {
return undefined;
}
const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection);
const commands = commandDetection?.commands;
if (!commands || commands.length === 0) {
if (!commandDetection) {
return undefined;
}

const targetId = this._terminalData.terminalCommandId;
if (!targetId) {
return undefined;
}

return commands.find(c => c.id === this._terminalData.terminalCommandId);
const commands = commandDetection.commands;
if (commands && commands.length > 0) {
const fromHistory = commands.find(c => c.id === targetId);
if (fromHistory) {
return fromHistory;
}
}

const executing = commandDetection.executingCommandObject;
if (executing && executing.id === targetId) {
return executing;
}

return undefined;
}
}

Expand All @@ -670,6 +748,8 @@ class ChatTerminalToolOutputSection extends Disposable {
private readonly _outputBody: HTMLElement;
private _scrollableContainer: DomScrollableElement | undefined;
private _renderedOutputHeight: number | undefined;
private _isAtBottom: boolean = true;
private _isProgrammaticScroll: boolean = false;
private _mirror: DetachedTerminalCommandMirror | undefined;
private _snapshotMirror: DetachedTerminalSnapshotMirror | undefined;
private readonly _contentContainer: HTMLElement;
Expand Down Expand Up @@ -738,6 +818,7 @@ class ChatTerminalToolOutputSection extends Disposable {

if (!expanded) {
this._renderedOutputHeight = undefined;
this._isAtBottom = true;
this._onDidChangeHeight();
return true;
}
Expand Down Expand Up @@ -825,6 +906,14 @@ class ChatTerminalToolOutputSection extends Disposable {
scrollableDomNode.tabIndex = 0;
this.domNode.appendChild(scrollableDomNode);
this.updateAriaLabel();

// Track scroll state to enable scroll lock behavior (only for user scrolls)
this._register(this._scrollableContainer.onScroll(() => {
if (this._isProgrammaticScroll) {
return;
}
this._isAtBottom = this._computeIsAtBottom();
}));
}

private async _updateTerminalContent(): Promise<void> {
Expand Down Expand Up @@ -858,9 +947,22 @@ class ChatTerminalToolOutputSection extends Disposable {
this._disposeLiveMirror();
return false;
}
this._mirror = this._register(this._instantiationService.createInstance(DetachedTerminalCommandMirror, liveTerminalInstance.xterm!, command));
await this._mirror.attach(this._terminalContainer);
const result = await this._mirror.renderCommand();
const mirror = this._register(this._instantiationService.createInstance(DetachedTerminalCommandMirror, liveTerminalInstance.xterm, command));
this._mirror = mirror;
this._register(mirror.onDidUpdate(lineCount => {
this._layoutOutput(lineCount);
if (this._isAtBottom) {
this._scrollOutputToBottom();
}
}));
// Forward input from the mirror terminal to the live terminal instance
this._register(mirror.onDidInput(data => {
if (!liveTerminalInstance.isDisposed) {
liveTerminalInstance.sendText(data, false);
}
}));
await mirror.attach(this._terminalContainer);
const result = await mirror.renderCommand();
if (!result || result.lineCount === 0) {
this._showEmptyMessage(localize('chat.terminalOutputEmpty', 'No output was produced by the command.'));
} else {
Expand Down Expand Up @@ -975,12 +1077,25 @@ class ChatTerminalToolOutputSection extends Disposable {
}
}

private _computeIsAtBottom(): boolean {
if (!this._scrollableContainer) {
return true;
}
const dimensions = this._scrollableContainer.getScrollDimensions();
const scrollPosition = this._scrollableContainer.getScrollPosition();
// Consider "at bottom" if within a small threshold to account for rounding
const threshold = 5;
return scrollPosition.scrollTop >= dimensions.scrollHeight - dimensions.height - threshold;
}

private _scrollOutputToBottom(): void {
if (!this._scrollableContainer) {
return;
}
this._isProgrammaticScroll = true;
const dimensions = this._scrollableContainer.getScrollDimensions();
this._scrollableContainer.setScrollPosition({ scrollTop: dimensions.scrollHeight });
this._isProgrammaticScroll = false;
}

private _getOutputContentHeight(lineCount: number, rowHeight: number, padding: number): number {
Expand Down
Loading
Loading