Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
14e4eed
wip rendering xterm instead of html
meganrogge Nov 21, 2025
2e2448a
determine when empty message appears
meganrogge Nov 21, 2025
1e9135a
rm unneeded check
meganrogge Nov 21, 2025
b3a58a2
rm css
meganrogge Nov 21, 2025
130d862
revert another change
meganrogge Nov 21, 2025
6ec21ef
polish
meganrogge Nov 21, 2025
8bd0bac
rm trunchation
meganrogge Nov 21, 2025
d93fa22
fix declared properties for the third time
meganrogge Nov 21, 2025
ef9ea67
rm line
meganrogge Nov 21, 2025
201015e
move file
meganrogge Nov 21, 2025
3a84c57
Merge branch 'main' into anxious-cuckoo
meganrogge Nov 21, 2025
928d333
trim prompt from end
meganrogge Nov 21, 2025
8beee67
set proper height
meganrogge Nov 21, 2025
11893d6
rm hack
meganrogge Nov 21, 2025
db19c94
Merge branch 'main' into anxious-cuckoo
meganrogge Nov 21, 2025
0dcf9db
rename
meganrogge Nov 21, 2025
0d2609f
Add skip last line option
meganrogge Nov 21, 2025
49c7df4
add jsdoc
meganrogge Nov 21, 2025
0a1d251
rm overview ruler via options
meganrogge Nov 21, 2025
aaec56f
rm unneeded code
meganrogge Nov 21, 2025
45b4b7c
rm something
meganrogge Nov 21, 2025
536d3bd
Ref rows instead of pixels even tho pixels used in the end
meganrogge Nov 21, 2025
e0ddb45
rm resize observer rename variable
meganrogge Nov 21, 2025
cf6f18a
rename element from host to terminalContainer
meganrogge Nov 21, 2025
1a6bd99
add detachedXtermOption, use h to create elements
meganrogge Nov 21, 2025
7c64b49
private readonly
meganrogge Nov 21, 2025
14292bd
rm replace call
meganrogge Nov 22, 2025
88ae085
rm hasRendered
meganrogge Nov 22, 2025
c373654
don't pass in title
meganrogge Nov 22, 2025
4dc64ce
refactor: streamline ChatTerminalToolOutputSection DOM handling
meganrogge Nov 22, 2025
16783b2
rm displaycommand
meganrogge Nov 22, 2025
97c9a27
inline focus events, add css instead of inline style
meganrogge Nov 22, 2025
957415f
rm raw, some unnecessary calls
meganrogge Nov 22, 2025
584733a
rm cast
meganrogge Nov 22, 2025
7b2d6e6
address mirror feedback
meganrogge Nov 22, 2025
5c7565b
hide scrollbar
meganrogge Nov 22, 2025
aa2f8d6
Merge branch 'main' into anxious-cuckoo
meganrogge Nov 22, 2025
84bce02
Update src/vs/workbench/contrib/terminal/browser/chatTerminalCommandM…
meganrogge Nov 22, 2025
149a652
Update src/vs/workbench/contrib/chat/browser/chatContentParts/toolInv…
meganrogge Nov 22, 2025
013dd12
Update src/vs/workbench/contrib/terminal/browser/terminal.ts
meganrogge Nov 22, 2025
7328205
address most of feedback
meganrogge Nov 22, 2025
865f6db
Add support for disabling overview ruler in terminal options
meganrogge Nov 22, 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 @@ -151,16 +151,18 @@
.chat-terminal-output-container > .monaco-scrollable-element {
width: 100%;
}
.chat-terminal-output-container: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-content {
display: flex;
flex-direction: column;
gap: 6px;
.chat-terminal-output-terminal.chat-terminal-output-terminal-no-output {
display: none;
}
.chat-terminal-output {
margin: 0;
Expand All @@ -169,10 +171,18 @@
}

.chat-terminal-output-empty {
display: none;
font-style: italic;
color: var(--vscode-descriptionForeground);
line-height: normal;
}
.chat-terminal-output-terminal.chat-terminal-output-terminal-no-output ~ .chat-terminal-output-empty {
display: block;
}

.chat-terminal-output-container .xterm-scrollable-element .scrollbar {
display: none;
}

.chat-terminal-output div,
.chat-terminal-output span {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable } from '../../../../base/common/lifecycle.js';
import type { ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js';
import { ITerminalService, type IDetachedTerminalInstance } from './terminal.js';
import { DetachedProcessInfo } from './detachedTerminal.js';
import { XtermTerminal } from './xterm/xtermTerminal.js';
import { TERMINAL_BACKGROUND_COLOR } from '../common/terminalColorRegistry.js';
import { PANEL_BACKGROUND } from '../../../common/theme.js';

interface IDetachedTerminalCommandMirror {
attach(container: HTMLElement): Promise<void>;
renderCommand(): Promise<{ lineCount?: number } | undefined>;
}

/**
* Mirrors a terminal command's output into a detached terminal instance.
* Used in the chat terminal tool progress part to show command output for example.
*/
export class DetachedTerminalCommandMirror extends Disposable implements IDetachedTerminalCommandMirror {
Copy link
Member

Choose a reason for hiding this comment

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

We want some unit tests for this, they'll be a lot more important in the next PR though. Basically they should write stuff to XtermTerminal, pass in the command and verify they both serialize to the same text. For the streaming PR we want it to check as things are progressing.

private _detachedTerminal: Promise<IDetachedTerminalInstance>;
private _attachedContainer?: HTMLElement;

constructor(
private readonly _xtermTerminal: XtermTerminal,
private readonly _command: ITerminalCommand,
@ITerminalService private readonly _terminalService: ITerminalService,
) {
super();
this._detachedTerminal = this._createTerminal();
}

async attach(container: HTMLElement): Promise<void> {
const terminal = await this._detachedTerminal;
if (this._attachedContainer !== container) {
container.classList.add('chat-terminal-output-terminal');
terminal.attachToElement(container);
this._attachedContainer = container;
}
}

async renderCommand(): Promise<{ lineCount?: number } | undefined> {
const vt = await this._getCommandOutputAsVT();
if (!vt) {
return undefined;
}
if (!vt.text) {
return { lineCount: 0 };
}
const detached = await this._detachedTerminal;
detached.xterm.write(vt.text);
return { lineCount: vt.lineCount };
}

private async _getCommandOutputAsVT(): Promise<{ text: string; lineCount: number } | undefined> {
const executedMarker = this._command.executedMarker;
const endMarker = this._command.endMarker;
if (!executedMarker || executedMarker.isDisposed || !endMarker || endMarker.isDisposed) {
return undefined;
}

const startLine = executedMarker.line;
const endLine = endMarker.line - 1;
const lineCount = Math.max(endLine - startLine + 1, 0);

const text = await this._xtermTerminal.getRangeAsVT(executedMarker, endMarker, true);
if (!text) {
return { text: '', lineCount: 0 };
}

return { text, lineCount };
}

private async _createTerminal(): Promise<IDetachedTerminalInstance> {
const detached = await this._terminalService.createDetachedTerminal({
cols: this._xtermTerminal.raw!.cols,
rows: 10,
readonly: true,
Copy link
Member

Choose a reason for hiding this comment

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

We'll want this to forward data events when the content is streamed.

processInfo: new DetachedProcessInfo({ initialCwd: '' }),
disableOverviewRuler: true,
colorProvider: {
getBackgroundColor: theme => {
const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR);
if (terminalBackground) {
return terminalBackground;
}
return theme.getColor(PANEL_BACKGROUND);
},
}
});
return this._register(detached);
}

}
9 changes: 9 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export interface IDetachedXTermOptions {
capabilities?: ITerminalCapabilityStore;
readonly?: boolean;
processInfo: ITerminalProcessInfo;
disableOverviewRuler?: boolean;
}

/**
Expand Down Expand Up @@ -1343,6 +1344,14 @@ export interface IXtermTerminal extends IDisposable {
*/
getFont(): ITerminalFont;

/**
* Gets the content between two markers as VT sequences.
* @param startMarker The marker to start from.
* @param endMarker The marker to end at.
* @param skipLastLine Whether the last line should be skipped (e.g. when it's the prompt line)
*/
getRangeAsVT(startMarker: IXtermMarker, endMarker?: IXtermMarker, skipLastLine?: boolean): Promise<string>;

/**
* Gets whether there's any terminal selection.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,7 @@ export class TerminalService extends Disposable implements ITerminalService {
rows: options.rows,
xtermColorProvider: options.colorProvider,
capabilities: options.capabilities || new TerminalCapabilityStore(),
disableOverviewRuler: options.disableOverviewRuler,
}, undefined);

if (options.readonly) {
Expand Down
31 changes: 26 additions & 5 deletions src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { equals } from '../../../../../base/common/objects.js';
import type { IProgressState } from '@xterm/addon-progress';
import type { CommandDetectionCapability } from '../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js';
import { URI } from '../../../../../base/common/uri.js';
import { assert } from '../../../../../base/common/assert.js';

const enum RenderConstants {
SmoothScrollDuration = 125
Expand Down Expand Up @@ -83,6 +84,8 @@ export interface IXtermTerminalOptions {
disableShellIntegrationReporting?: boolean;
/** The object that imports xterm addons, set this to inject an importer in tests. */
xtermAddonImporter?: XtermAddonImporter;
/** Whether to disable the overview ruler. */
disableOverviewRuler?: boolean;
}

/**
Expand Down Expand Up @@ -230,7 +233,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
scrollSensitivity: config.mouseWheelScrollSensitivity,
scrollOnEraseInDisplay: true,
wordSeparator: config.wordSeparators,
overviewRuler: {
overviewRuler: options.disableOverviewRuler ? { width: 0 } : {
width: 14,
showTopBorder: true,
},
Expand Down Expand Up @@ -531,10 +534,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
this.raw.options.customGlyphs = config.customGlyphs;
this.raw.options.ignoreBracketedPasteMode = config.ignoreBracketedPasteMode;
this.raw.options.rescaleOverlappingGlyphs = config.rescaleOverlappingGlyphs;
this.raw.options.overviewRuler = {
width: 14,
showTopBorder: true,
};

this._updateSmoothScrolling();
if (this._attached) {
if (this._attached.options.enableGpu) {
Expand Down Expand Up @@ -891,6 +891,27 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
this._onDidRequestRefreshDimensions.fire();
}

async getRangeAsVT(startMarker: IXtermMarker, endMarker?: IXtermMarker, skipLastLine?: boolean): Promise<string> {
if (!this._serializeAddon) {
const Addon = await this._xtermAddonLoader.importAddon('serialize');
this._serializeAddon = new Addon();
this.raw.loadAddon(this._serializeAddon);
}

assert(startMarker.line !== -1);
let end = endMarker?.line ?? this.raw.buffer.active.length - 1;
if (skipLastLine) {
end = end - 1;
}
return this._serializeAddon.serialize({
range: {
start: startMarker.line,
end: end
}
});
}


getXtermTheme(theme?: IColorTheme): ITheme {
if (!theme) {
theme = this._themeService.getColorTheme();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
throw new CancellationError();
}

await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId, pollingResult?.output);
await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId);
const state = toolSpecificData.terminalCommandState ?? {};
state.timestamp = state.timestamp ?? timingStart;
toolSpecificData.terminalCommandState = state;
Expand Down Expand Up @@ -665,7 +665,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
throw new CancellationError();
}

await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId, executeResult.output);
await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId);
{
const state = toolSpecificData.terminalCommandState ?? {};
state.timestamp = state.timestamp ?? timingStart;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import { URI } from '../../../../../../base/common/uri.js';
import { IChatTerminalToolInvocationData } from '../../../../chat/common/chatService.js';
import { CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES } from '../../../../chat/common/constants.js';
import { ITerminalInstance } from '../../../../terminal/browser/terminal.js';
import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js';
Expand All @@ -19,7 +18,6 @@ export class TerminalCommandArtifactCollector {
toolSpecificData: IChatTerminalToolInvocationData,
instance: ITerminalInstance,
commandId: string | undefined,
fallbackOutput?: string
): Promise<void> {
if (commandId) {
try {
Expand All @@ -28,24 +26,19 @@ export class TerminalCommandArtifactCollector {
this._logService.warn(`RunInTerminalTool: Failed to create terminal command URI for ${commandId}`, error);
}

const serialized = await this._tryGetSerializedCommandOutput(toolSpecificData, instance, commandId);
if (serialized) {
toolSpecificData.terminalCommandOutput = { text: serialized.text, truncated: serialized.truncated };
const command = await this._tryGetCommand(instance, commandId);
if (command) {
toolSpecificData.terminalCommandState = {
exitCode: serialized.exitCode,
timestamp: serialized.timestamp,
duration: serialized.duration
exitCode: command.exitCode,
timestamp: command.timestamp,
duration: command.duration
};
this._applyTheme(toolSpecificData, instance);
return;
}
}

if (fallbackOutput !== undefined) {
const normalized = fallbackOutput.replace(/\r\n/g, '\n');
toolSpecificData.terminalCommandOutput = { text: normalized, truncated: false };
this._applyTheme(toolSpecificData, instance);
}
this._applyTheme(toolSpecificData, instance);
}

private _applyTheme(toolSpecificData: IChatTerminalToolInvocationData, instance: ITerminalInstance): void {
Expand All @@ -61,31 +54,8 @@ export class TerminalCommandArtifactCollector {
return instance.resource.with({ query: params.toString() });
}

private async _tryGetSerializedCommandOutput(toolSpecificData: IChatTerminalToolInvocationData, instance: ITerminalInstance, commandId: string): Promise<{ text: string; truncated?: boolean; exitCode?: number; timestamp?: number; duration?: number } | undefined> {
private async _tryGetCommand(instance: ITerminalInstance, commandId: string) {
const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection);
const command = commandDetection?.commands.find(c => c.id === commandId);

if (!command?.endMarker) {
return undefined;
}

const xterm = await instance.xtermReadyPromise;
if (!xterm) {
return undefined;
}

try {
const result = await xterm.getCommandOutputAsHtml(command, CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES);
return {
text: result.text,
truncated: result.truncated,
exitCode: command.exitCode,
timestamp: command.timestamp,
duration: command.duration
};
} catch (error) {
this._logService.warn(`RunInTerminalTool: Failed to serialize command output for ${commandId}`, error);
return undefined;
}
return commandDetection?.commands.find(c => c.id === commandId);
}
}
Loading