Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2e5850c
rm throttle
meganrogge Nov 18, 2025
7586f5e
fix error
meganrogge Nov 18, 2025
e14f190
fix issue
meganrogge Nov 18, 2025
7052042
get it mostly working
meganrogge Nov 18, 2025
b9bd619
get no output element to show up
meganrogge Nov 19, 2025
15201e8
simplify
meganrogge Nov 19, 2025
b45bf85
hide overview ruler
meganrogge Nov 19, 2025
aa7a509
fix height of no output element
meganrogge Nov 19, 2025
b3f3817
improve impl
meganrogge Nov 19, 2025
b2d715f
make it accessible
meganrogge Nov 19, 2025
ec4937f
use cols from instance, rows 10
meganrogge Nov 19, 2025
e2023fe
Use getRangeAsVT instead of data from onData
meganrogge Nov 19, 2025
cc346cd
rm isStreaming check
meganrogge Nov 19, 2025
5803923
add todo
meganrogge Nov 19, 2025
d5c66a5
handle lines in consolidated way
meganrogge Nov 19, 2025
8bf36ca
swap order
meganrogge Nov 19, 2025
8e9b174
use consts
meganrogge Nov 19, 2025
1bce512
fix bug
meganrogge Nov 19, 2025
0ab490c
Merge branch 'main' into merogge/stream
meganrogge Nov 19, 2025
2755882
clean up
meganrogge Nov 19, 2025
259bf5e
use terminal config service
meganrogge Nov 19, 2025
a7ef216
Update src/vs/workbench/contrib/chat/browser/chatContentParts/toolInv…
meganrogge Nov 19, 2025
0ac870b
Update src/vs/workbench/contrib/chat/browser/chatContentParts/toolInv…
meganrogge Nov 19, 2025
fe1035c
rm a bunch of code
meganrogge Nov 19, 2025
cbb7d77
use instantiation service
meganrogge Nov 19, 2025
e38cd09
Update src/vs/workbench/contrib/chat/browser/chatContentParts/toolInv…
meganrogge Nov 19, 2025
58eb264
use IDisposable vs store, rm trunchation
meganrogge Nov 19, 2025
3078661
Extract mirroring/streaming into class, adopt streamingQueue, deal wi…
meganrogge Nov 20, 2025
8ccbc4b
prevent next prompt from being rendered
meganrogge Nov 20, 2025
cebf1dd
track cursor, data to know if there's output
meganrogge Nov 20, 2025
5299e21
expose xterm getElement and adopt it
meganrogge Nov 20, 2025
0fc0252
rm unneeded
meganrogge Nov 20, 2025
a6d990d
move stuff near the code
meganrogge Nov 20, 2025
539a512
polish
meganrogge Nov 20, 2025
74d93b6
also make function for command finished
meganrogge Nov 20, 2025
fe9a493
rm line
meganrogge Nov 20, 2025
e3b1f85
simplify
meganrogge Nov 20, 2025
be813dd
fix height getting cutoff
meganrogge Nov 20, 2025
cec1aea
Update src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts
meganrogge Nov 20, 2025
47c0a8a
Update src/vs/workbench/contrib/chat/browser/chatContentParts/media/c…
meganrogge Nov 20, 2025
a92e246
rm something
meganrogge Nov 20, 2025
03e5497
rm css
meganrogge Nov 20, 2025
008bc58
fix height
meganrogge Nov 20, 2025
2ce231f
move chat streaming model out of file
meganrogge Nov 20, 2025
cb5c471
declare raw? on detached terminal
meganrogge Nov 20, 2025
adaa65c
greatly simplify, fix bug where scrolling would cause data to appear …
meganrogge Nov 20, 2025
8b216dd
rm weak map
meganrogge Nov 20, 2025
3102162
extract all streaming, add ChatTerminalCommandStreamer
meganrogge Nov 20, 2025
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 @@ -157,14 +157,36 @@
}

.chat-terminal-output-container.expanded { display: block; }
.chat-terminal-output-container > .monaco-scrollable-element {
.chat-terminal-output-container > .monaco-scrollable-element,
.chat-terminal-output-container > .chat-terminal-output-scroll-host {
width: 100%;
}
.chat-terminal-output-container:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
.chat-terminal-output-scroll-host {
display: block;
outline: none;
}
.chat-terminal-output-scroll-host:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
.chat-terminal-output-body {
padding: 4px 6px;
max-width: 100%;
height: 100%;
box-sizing: border-box;
min-height: 0;
}
.chat-terminal-output-terminal {
min-height: 40px;
}
.chat-terminal-output-terminal.chat-terminal-output-terminal-no-output {
display: none;
}
.chat-terminal-output-terminal.chat-terminal-output-terminal .xterm-decoration-overview-ruler {
display: none;
}
.chat-terminal-output-content {
display: flex;
Expand Down
Copy link
Member

Choose a reason for hiding this comment

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

Testing out and it doesn't handle commands with lots of output well

Finished inline:

Image

Actual command:

Image

Copy link
Contributor Author

@meganrogge meganrogge Nov 20, 2025

Choose a reason for hiding this comment

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

I can repro

Copy link
Member

Choose a reason for hiding this comment

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

I recommending taking a step back from this and first replacing the HTML with the xterm instance in its own PR, focusing on maintainability and small modules. At 2100 lines that makes this larger than the entirety of terminalTaskSystem.ts or terminalService.ts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moving ChatTerminalStreamingModel to its own file helps with this, but yes, can do

Large diffs are not rendered by default.

200 changes: 200 additions & 0 deletions src/vs/workbench/contrib/chat/browser/chatTerminalCommandStreamer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { MutableDisposable, DisposableStore, Disposable } from '../../../../base/common/lifecycle.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js';
import { ITerminalInstance } from '../../terminal/browser/terminal.js';
import { IStreamingSnapshotRequest } from './chatTerminalStreamingModel.js';
import type { IMarker as IXtermMarker } from '@xterm/xterm';


export class ChatTerminalCommandStreamer extends Disposable {
private readonly _commandStreamingListener = this._register(new MutableDisposable<DisposableStore>());
private _streamingCommand: ITerminalCommand | undefined;
private _trackedCommandId: string | undefined;
private _streamingQueue: IStreamingSnapshotRequest[] = [];
private _emptySnapshotRetries = 0;
private _isDrainingStreamingQueue = false;
private _streamingDrainScheduled = false;

constructor(
private readonly _logService: ILogService,
private readonly _isDisposed: () => boolean,
private readonly _syncSnapshot: (instance: ITerminalInstance, command: ITerminalCommand, force: boolean) => Promise<void>
) {
super();
}

public get listener(): MutableDisposable<DisposableStore> {
return this._commandStreamingListener;
}

public get streamingCommand(): ITerminalCommand | undefined {
return this._streamingCommand;
}

public beginStreaming(command: ITerminalCommand, expectedId: string | undefined): DisposableStore {
this._streamingCommand = command;
this._trackedCommandId = command.id ?? expectedId;
this._emptySnapshotRetries = 0;
const store = new DisposableStore();
this._commandStreamingListener.value = store;
return store;
}

public endStreaming(): void {
this._commandStreamingListener.clear();
this._streamingCommand = undefined;
this._trackedCommandId = undefined;
this._emptySnapshotRetries = 0;
this._clearStreamingQueue();
}

public resetForCommandDetection(initialTrackedId: string | undefined): void {
this._commandStreamingListener.clear();
this._streamingCommand = undefined;
this._trackedCommandId = initialTrackedId;
this._emptySnapshotRetries = 0;
this._clearStreamingQueue();
}

public clearAllCommandState(): void {
this._commandStreamingListener.clear();
this._streamingCommand = undefined;
this._trackedCommandId = undefined;
this._emptySnapshotRetries = 0;
this._clearStreamingQueue();
}

public get trackedCommandId(): string | undefined {
return this._trackedCommandId;
}

public set trackedCommandId(value: string | undefined) {
this._trackedCommandId = value;
}

public isTrackedCommand(candidate: string | undefined): boolean {
return !!candidate && candidate === this._trackedCommandId;
}

public queueStreaming(instance: ITerminalInstance, command: ITerminalCommand, force = false): Promise<void> {
if (this._isDisposed() || (!force && this._streamingCommand !== command)) {
return Promise.resolve();
}

const executedMarker = (command as unknown as { executedMarker?: IXtermMarker; commandExecutedMarker?: IXtermMarker }).executedMarker
?? (command as unknown as { executedMarker?: IXtermMarker; commandExecutedMarker?: IXtermMarker }).commandExecutedMarker;
if (!executedMarker) {
const commandId = command.id ?? 'unknown';
this._logService.trace('chatTerminalToolProgressPart.queueStreaming.waitForExecutedMarker', { commandId, force });
setTimeout(() => {
if (this._isDisposed() || (!force && this._streamingCommand !== command)) {
return;
}
void this.queueStreaming(instance, command, force);
}, 0);
return Promise.resolve();
}

const commandId = command.id ?? 'unknown';
this._logService.trace('chatTerminalToolProgressPart.queueStreaming', { commandId, pending: this._streamingQueue.length + 1, force });

return new Promise<void>((resolve, reject) => {
this._streamingQueue.push({ instance, command, force, resolve, reject });
if (!this._isDrainingStreamingQueue) {
this._scheduleStreamingFlush();
}
});
}

public handleEmptySnapshotNoChange(force: boolean, storedLength: number, hasOutput: boolean, command: ITerminalCommand, requeue: () => void): boolean {
if (!(force && storedLength === 0 && hasOutput)) {
this._emptySnapshotRetries = 0;
return false;
}

this._emptySnapshotRetries++;
if (this._emptySnapshotRetries <= 60) {
const attempt = this._emptySnapshotRetries;
this._logService.trace('chatTerminalToolProgressPart.syncStreamingSnapshot.retryPendingOutput', { commandId: command.id, attempt });
requeue();
return true;
}

this._logService.trace('chatTerminalToolProgressPart.syncStreamingSnapshot.retryPendingOutput.maxAttempts', { commandId: command.id, attempt: this._emptySnapshotRetries });
this._emptySnapshotRetries = 0;
return false;
}

public resetEmptySnapshotRetries(): void {
this._emptySnapshotRetries = 0;
}

public override dispose(): void {
this._clearStreamingQueue();
super.dispose();
}

private _scheduleStreamingFlush(): void {
if (this._streamingDrainScheduled || this._isDrainingStreamingQueue || this._streamingQueue.length === 0) {
return;
}
this._streamingDrainScheduled = true;
this._logService.trace('chatTerminalToolProgressPart.scheduleStreamingFlush', { commandId: this._streamingCommand?.id, queued: this._streamingQueue.length });
void this._drainStreamingQueue();
}

private async _drainStreamingQueue(): Promise<void> {
this._streamingDrainScheduled = false;
if (this._isDrainingStreamingQueue || this._streamingQueue.length === 0) {
return;
}

this._isDrainingStreamingQueue = true;
this._logService.trace('chatTerminalToolProgressPart.drainStreamingQueue-start', { queued: this._streamingQueue.length, commandId: this._streamingCommand?.id });
try {
while (this._streamingQueue.length) {
const job = this._streamingQueue.shift()!;
if (this._isDisposed() || (!job.force && this._streamingCommand !== job.command)) {
job.resolve();
this._logService.trace('chatTerminalToolProgressPart.drainStreamingQueue-skip', { commandId: job.command.id, force: job.force });
continue;
}
try {
await this._syncSnapshot(job.instance, job.command, job.force);
job.resolve();
this._logService.trace('chatTerminalToolProgressPart.drainStreamingQueue-run', { commandId: job.command.id, force: job.force });
} catch (error) {
job.reject(error);
this._logService.trace('chatTerminalToolProgressPart.drainStreamingQueue-error', { commandId: job.command.id, force: job.force, message: error instanceof Error ? error.message : String(error) });
}
}
} finally {
this._isDrainingStreamingQueue = false;
this._logService.trace('chatTerminalToolProgressPart.drainStreamingQueue-end', { remaining: this._streamingQueue.length, commandId: this._streamingCommand?.id });
if (this._streamingQueue.length) {
this._scheduleStreamingFlush();
}
}
}

private _clearStreamingQueue(error?: unknown): void {
this._streamingDrainScheduled = false;
if (!this._streamingQueue.length) {
return;
}
this._logService.trace('chatTerminalToolProgressPart.clearStreamingQueue', { pending: this._streamingQueue.length, hasError: error !== undefined });
const pending = this._streamingQueue.splice(0, this._streamingQueue.length);
for (const job of pending) {
if (error !== undefined) {
job.reject(error);
} else {
job.resolve();
}
}
}
}
Loading
Loading