Skip to content

Commit 60706b4

Browse files
authored
render xterm instead of html for the chat terminal output (microsoft#278684)
1 parent cc7cded commit 60706b4

File tree

8 files changed

+326
-303
lines changed

8 files changed

+326
-303
lines changed

src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatTerminalToolProgressPart.css

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,18 @@
151151
.chat-terminal-output-container > .monaco-scrollable-element {
152152
width: 100%;
153153
}
154+
.chat-terminal-output-container:focus-visible {
155+
outline: 1px solid var(--vscode-focusBorder);
156+
outline-offset: 2px;
157+
}
154158
.chat-terminal-output-body {
155159
padding: 4px 6px;
156160
max-width: 100%;
157-
height: 100%;
158161
box-sizing: border-box;
162+
min-height: 0;
159163
}
160-
.chat-terminal-output-content {
161-
display: flex;
162-
flex-direction: column;
163-
gap: 6px;
164+
.chat-terminal-output-terminal.chat-terminal-output-terminal-no-output {
165+
display: none;
164166
}
165167
.chat-terminal-output {
166168
margin: 0;
@@ -169,10 +171,18 @@
169171
}
170172

171173
.chat-terminal-output-empty {
174+
display: none;
172175
font-style: italic;
173176
color: var(--vscode-descriptionForeground);
174177
line-height: normal;
175178
}
179+
.chat-terminal-output-terminal.chat-terminal-output-terminal-no-output ~ .chat-terminal-output-empty {
180+
display: block;
181+
}
182+
183+
.chat-terminal-output-container .xterm-scrollable-element .scrollbar {
184+
display: none;
185+
}
176186

177187
.chat-terminal-output div,
178188
.chat-terminal-output span {

src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts

Lines changed: 168 additions & 253 deletions
Large diffs are not rendered by default.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Disposable } from '../../../../base/common/lifecycle.js';
7+
import type { ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js';
8+
import { ITerminalService, type IDetachedTerminalInstance } from './terminal.js';
9+
import { DetachedProcessInfo } from './detachedTerminal.js';
10+
import { XtermTerminal } from './xterm/xtermTerminal.js';
11+
import { TERMINAL_BACKGROUND_COLOR } from '../common/terminalColorRegistry.js';
12+
import { PANEL_BACKGROUND } from '../../../common/theme.js';
13+
14+
interface IDetachedTerminalCommandMirror {
15+
attach(container: HTMLElement): Promise<void>;
16+
renderCommand(): Promise<{ lineCount?: number } | undefined>;
17+
}
18+
19+
/**
20+
* Mirrors a terminal command's output into a detached terminal instance.
21+
* Used in the chat terminal tool progress part to show command output for example.
22+
*/
23+
export class DetachedTerminalCommandMirror extends Disposable implements IDetachedTerminalCommandMirror {
24+
private _detachedTerminal: Promise<IDetachedTerminalInstance>;
25+
private _attachedContainer?: HTMLElement;
26+
27+
constructor(
28+
private readonly _xtermTerminal: XtermTerminal,
29+
private readonly _command: ITerminalCommand,
30+
@ITerminalService private readonly _terminalService: ITerminalService,
31+
) {
32+
super();
33+
this._detachedTerminal = this._createTerminal();
34+
}
35+
36+
async attach(container: HTMLElement): Promise<void> {
37+
const terminal = await this._detachedTerminal;
38+
if (this._attachedContainer !== container) {
39+
container.classList.add('chat-terminal-output-terminal');
40+
terminal.attachToElement(container);
41+
this._attachedContainer = container;
42+
}
43+
}
44+
45+
async renderCommand(): Promise<{ lineCount?: number } | undefined> {
46+
const vt = await this._getCommandOutputAsVT();
47+
if (!vt) {
48+
return undefined;
49+
}
50+
if (!vt.text) {
51+
return { lineCount: 0 };
52+
}
53+
const detached = await this._detachedTerminal;
54+
detached.xterm.write(vt.text);
55+
return { lineCount: vt.lineCount };
56+
}
57+
58+
private async _getCommandOutputAsVT(): Promise<{ text: string; lineCount: number } | undefined> {
59+
const executedMarker = this._command.executedMarker;
60+
const endMarker = this._command.endMarker;
61+
if (!executedMarker || executedMarker.isDisposed || !endMarker || endMarker.isDisposed) {
62+
return undefined;
63+
}
64+
65+
const startLine = executedMarker.line;
66+
const endLine = endMarker.line - 1;
67+
const lineCount = Math.max(endLine - startLine + 1, 0);
68+
69+
const text = await this._xtermTerminal.getRangeAsVT(executedMarker, endMarker, true);
70+
if (!text) {
71+
return { text: '', lineCount: 0 };
72+
}
73+
74+
return { text, lineCount };
75+
}
76+
77+
private async _createTerminal(): Promise<IDetachedTerminalInstance> {
78+
const detached = await this._terminalService.createDetachedTerminal({
79+
cols: this._xtermTerminal.raw!.cols,
80+
rows: 10,
81+
readonly: true,
82+
processInfo: new DetachedProcessInfo({ initialCwd: '' }),
83+
disableOverviewRuler: true,
84+
colorProvider: {
85+
getBackgroundColor: theme => {
86+
const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR);
87+
if (terminalBackground) {
88+
return terminalBackground;
89+
}
90+
return theme.getColor(PANEL_BACKGROUND);
91+
},
92+
}
93+
});
94+
return this._register(detached);
95+
}
96+
97+
}

src/vs/workbench/contrib/terminal/browser/terminal.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ export interface IDetachedXTermOptions {
332332
capabilities?: ITerminalCapabilityStore;
333333
readonly?: boolean;
334334
processInfo: ITerminalProcessInfo;
335+
disableOverviewRuler?: boolean;
335336
}
336337

337338
/**
@@ -1343,6 +1344,14 @@ export interface IXtermTerminal extends IDisposable {
13431344
*/
13441345
getFont(): ITerminalFont;
13451346

1347+
/**
1348+
* Gets the content between two markers as VT sequences.
1349+
* @param startMarker The marker to start from.
1350+
* @param endMarker The marker to end at.
1351+
* @param skipLastLine Whether the last line should be skipped (e.g. when it's the prompt line)
1352+
*/
1353+
getRangeAsVT(startMarker: IXtermMarker, endMarker?: IXtermMarker, skipLastLine?: boolean): Promise<string>;
1354+
13461355
/**
13471356
* Gets whether there's any terminal selection.
13481357
*/

src/vs/workbench/contrib/terminal/browser/terminalService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,7 @@ export class TerminalService extends Disposable implements ITerminalService {
10991099
rows: options.rows,
11001100
xtermColorProvider: options.colorProvider,
11011101
capabilities: options.capabilities || new TerminalCapabilityStore(),
1102+
disableOverviewRuler: options.disableOverviewRuler,
11021103
}, undefined);
11031104

11041105
if (options.readonly) {

src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { equals } from '../../../../../base/common/objects.js';
4646
import type { IProgressState } from '@xterm/addon-progress';
4747
import type { CommandDetectionCapability } from '../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js';
4848
import { URI } from '../../../../../base/common/uri.js';
49+
import { assert } from '../../../../../base/common/assert.js';
4950

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

8891
/**
@@ -230,7 +233,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
230233
scrollSensitivity: config.mouseWheelScrollSensitivity,
231234
scrollOnEraseInDisplay: true,
232235
wordSeparator: config.wordSeparators,
233-
overviewRuler: {
236+
overviewRuler: options.disableOverviewRuler ? { width: 0 } : {
234237
width: 14,
235238
showTopBorder: true,
236239
},
@@ -531,10 +534,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
531534
this.raw.options.customGlyphs = config.customGlyphs;
532535
this.raw.options.ignoreBracketedPasteMode = config.ignoreBracketedPasteMode;
533536
this.raw.options.rescaleOverlappingGlyphs = config.rescaleOverlappingGlyphs;
534-
this.raw.options.overviewRuler = {
535-
width: 14,
536-
showTopBorder: true,
537-
};
537+
538538
this._updateSmoothScrolling();
539539
if (this._attached) {
540540
if (this._attached.options.enableGpu) {
@@ -891,6 +891,27 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
891891
this._onDidRequestRefreshDimensions.fire();
892892
}
893893

894+
async getRangeAsVT(startMarker: IXtermMarker, endMarker?: IXtermMarker, skipLastLine?: boolean): Promise<string> {
895+
if (!this._serializeAddon) {
896+
const Addon = await this._xtermAddonLoader.importAddon('serialize');
897+
this._serializeAddon = new Addon();
898+
this.raw.loadAddon(this._serializeAddon);
899+
}
900+
901+
assert(startMarker.line !== -1);
902+
let end = endMarker?.line ?? this.raw.buffer.active.length - 1;
903+
if (skipLastLine) {
904+
end = end - 1;
905+
}
906+
return this._serializeAddon.serialize({
907+
range: {
908+
start: startMarker.line,
909+
end: end
910+
}
911+
});
912+
}
913+
914+
894915
getXtermTheme(theme?: IColorTheme): ITheme {
895916
if (!theme) {
896917
theme = this._themeService.getColorTheme();

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
566566
throw new CancellationError();
567567
}
568568

569-
await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId, pollingResult?.output);
569+
await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId);
570570
const state = toolSpecificData.terminalCommandState ?? {};
571571
state.timestamp = state.timestamp ?? timingStart;
572572
toolSpecificData.terminalCommandState = state;
@@ -665,7 +665,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
665665
throw new CancellationError();
666666
}
667667

668-
await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId, executeResult.output);
668+
await this._commandArtifactCollector.capture(toolSpecificData, toolTerminal.instance, commandId);
669669
{
670670
const state = toolSpecificData.terminalCommandState ?? {};
671671
state.timestamp = state.timestamp ?? timingStart;

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/terminalCommandArtifactCollector.ts

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import { URI } from '../../../../../../base/common/uri.js';
77
import { IChatTerminalToolInvocationData } from '../../../../chat/common/chatService.js';
8-
import { CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES } from '../../../../chat/common/constants.js';
98
import { ITerminalInstance } from '../../../../terminal/browser/terminal.js';
109
import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
1110
import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js';
@@ -19,7 +18,6 @@ export class TerminalCommandArtifactCollector {
1918
toolSpecificData: IChatTerminalToolInvocationData,
2019
instance: ITerminalInstance,
2120
commandId: string | undefined,
22-
fallbackOutput?: string
2321
): Promise<void> {
2422
if (commandId) {
2523
try {
@@ -28,24 +26,19 @@ export class TerminalCommandArtifactCollector {
2826
this._logService.warn(`RunInTerminalTool: Failed to create terminal command URI for ${commandId}`, error);
2927
}
3028

31-
const serialized = await this._tryGetSerializedCommandOutput(toolSpecificData, instance, commandId);
32-
if (serialized) {
33-
toolSpecificData.terminalCommandOutput = { text: serialized.text, truncated: serialized.truncated };
29+
const command = await this._tryGetCommand(instance, commandId);
30+
if (command) {
3431
toolSpecificData.terminalCommandState = {
35-
exitCode: serialized.exitCode,
36-
timestamp: serialized.timestamp,
37-
duration: serialized.duration
32+
exitCode: command.exitCode,
33+
timestamp: command.timestamp,
34+
duration: command.duration
3835
};
3936
this._applyTheme(toolSpecificData, instance);
4037
return;
4138
}
4239
}
4340

44-
if (fallbackOutput !== undefined) {
45-
const normalized = fallbackOutput.replace(/\r\n/g, '\n');
46-
toolSpecificData.terminalCommandOutput = { text: normalized, truncated: false };
47-
this._applyTheme(toolSpecificData, instance);
48-
}
41+
this._applyTheme(toolSpecificData, instance);
4942
}
5043

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

64-
private async _tryGetSerializedCommandOutput(toolSpecificData: IChatTerminalToolInvocationData, instance: ITerminalInstance, commandId: string): Promise<{ text: string; truncated?: boolean; exitCode?: number; timestamp?: number; duration?: number } | undefined> {
57+
private async _tryGetCommand(instance: ITerminalInstance, commandId: string) {
6558
const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection);
66-
const command = commandDetection?.commands.find(c => c.id === commandId);
67-
68-
if (!command?.endMarker) {
69-
return undefined;
70-
}
71-
72-
const xterm = await instance.xtermReadyPromise;
73-
if (!xterm) {
74-
return undefined;
75-
}
76-
77-
try {
78-
const result = await xterm.getCommandOutputAsHtml(command, CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES);
79-
return {
80-
text: result.text,
81-
truncated: result.truncated,
82-
exitCode: command.exitCode,
83-
timestamp: command.timestamp,
84-
duration: command.duration
85-
};
86-
} catch (error) {
87-
this._logService.warn(`RunInTerminalTool: Failed to serialize command output for ${commandId}`, error);
88-
return undefined;
89-
}
59+
return commandDetection?.commands.find(c => c.id === commandId);
9060
}
9161
}

0 commit comments

Comments
 (0)