fix: remove backslash escaping from terminal command labels (#303856)

* fix: remove escapeMarkdownSyntaxTokens from terminal command labels

Fixes #303844

The command text in ChatTerminalThinkingCollapsibleWrapper was being
escaped with escapeMarkdownSyntaxTokens(), which adds backslashes before
chars like - * # etc. This is unnecessary because the text is always
rendered inside markdown code spans or via .textContent, both of which
treat content as literal.

Also adds a component fixture for the terminal collapsible wrapper to
enable visual regression testing of command label rendering.

* fix: use DOM nodes instead of MarkdownString for sandbox command labels

Addresses review feedback: commands containing backticks (common in
PowerShell) would break the inline-code markdown spans. Now both
sandbox and non-sandbox paths use text nodes + <code> elements with
.textContent, which is always safe for arbitrary command text.

Also adds fixture cases for backtick-containing commands to catch
this class of issue.

* fix: remove colons from fixture names to fix CI artifact paths

* add screenshot baselines for terminal collapsible fixtures
This commit is contained in:
Alexandru Dima
2026-03-22 15:24:40 +01:00
committed by GitHub
parent 12e343fccb
commit a7e3a4e1e5
20 changed files with 168 additions and 11 deletions

View File

@@ -5,7 +5,7 @@
import { h } from '../../../../../../../base/browser/dom.js';
import { ActionBar } from '../../../../../../../base/browser/ui/actionbar/actionbar.js';
import { escapeMarkdownSyntaxTokens, isMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js';
import { isMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js';
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
import { ChatConfiguration } from '../../../../common/constants.js';
@@ -1624,7 +1624,7 @@ export class ContinueInBackgroundAction extends Action implements IAction {
}
}
class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart {
export class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart {
private readonly _terminalContentElement: HTMLElement;
private readonly _commandText: string;
private readonly _isSandboxWrapped: boolean;
@@ -1640,11 +1640,13 @@ class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart
@IHoverService hoverService: IHoverService,
@IConfigurationService configurationService: IConfigurationService,
) {
const title = isComplete ? `Ran \`${escapeMarkdownSyntaxTokens(commandText)}\`` : `Running \`${escapeMarkdownSyntaxTokens(commandText)}\``;
const title = isComplete
? localize('chat.terminal.ran.plain', "Ran {0}", commandText)
: localize('chat.terminal.running.plain', "Running {0}", commandText);
super(title, context, undefined, hoverService, configurationService);
this._terminalContentElement = contentElement;
this._commandText = escapeMarkdownSyntaxTokens(commandText);
this._commandText = commandText;
this._isSandboxWrapped = isSandboxWrapped;
this._isComplete = isComplete;
@@ -1663,16 +1665,22 @@ class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart
return;
}
if (this._isSandboxWrapped) {
this._collapseButton.label = new MarkdownString(this._isComplete
? localize('chat.terminal.ranInSandbox', "Ran `{0}` in sandbox", this._commandText)
: localize('chat.terminal.runningInSandbox', "Running `{0}` in sandbox", this._commandText));
return;
}
const labelElement = this._collapseButton.labelElement;
labelElement.textContent = '';
if (this._isSandboxWrapped) {
const prefixText = this._isComplete
? localize('chat.terminal.ranInSandbox.prefix', "Ran ")
: localize('chat.terminal.runningInSandbox.prefix', "Running ");
const suffixText = localize('chat.terminal.sandbox.suffix', " in sandbox");
labelElement.appendChild(document.createTextNode(prefixText));
const codeElement = document.createElement('code');
codeElement.textContent = this._commandText;
labelElement.appendChild(codeElement);
labelElement.appendChild(document.createTextNode(suffixText));
return;
}
const prefixText = this._isComplete
? localize('chat.terminal.ran.prefix', "Ran ")
: localize('chat.terminal.running.prefix', "Running ");

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from '../../../../base/browser/dom.js';
import { Event } from '../../../../base/common/event.js';
import { observableValue } from '../../../../base/common/observable.js';
import { mock, upcastPartial } from '../../../../base/test/common/mock.js';
import type { IChatContentPartRenderContext, InlineTextModelCollection } from '../../../contrib/chat/browser/widget/chatContentParts/chatContentParts.js';
import type { IChatResponseViewModel } from '../../../contrib/chat/common/model/chatViewModel.js';
import { ChatTerminalThinkingCollapsibleWrapper } from '../../../contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.js';
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js';
import '../../../contrib/chat/browser/widget/media/chat.css';
function createMockContext(): IChatContentPartRenderContext {
return {
element: new class extends mock<IChatResponseViewModel>() { }(),
elementIndex: 0,
container: document.createElement('div'),
content: [],
contentIndex: 0,
editorPool: undefined!,
codeBlockStartIndex: 0,
treeStartIndex: 0,
diffEditorPool: undefined!,
codeBlockModelCollection: undefined!,
currentWidth: observableValue('currentWidth', 400),
onDidChangeVisibility: Event.None,
inlineTextModels: upcastPartial<InlineTextModelCollection>({}),
};
}
function renderCollapsible(context: ComponentFixtureContext, commandText: string, isSandboxWrapped: boolean, isComplete: boolean): void {
const { container, disposableStore } = context;
const instantiationService = createEditorServices(disposableStore, {
colorTheme: context.theme,
});
container.style.width = '500px';
container.style.padding = '8px';
container.classList.add('monaco-workbench');
const session = dom.$('.interactive-session');
container.appendChild(session);
const contentElement = dom.$('.chat-terminal-output-placeholder');
contentElement.textContent = '(terminal output would appear here)';
contentElement.style.padding = '8px';
contentElement.style.color = 'var(--vscode-descriptionForeground)';
const wrapper = disposableStore.add(instantiationService.createInstance(
ChatTerminalThinkingCollapsibleWrapper,
commandText,
isSandboxWrapped,
contentElement,
createMockContext(),
false,
isComplete,
));
session.appendChild(wrapper.domNode);
}
export default defineThemedFixtureGroup({ path: 'chat/terminalCollapsible/' }, {
'Ran - simple command': defineComponentFixture({
render: ctx => renderCollapsible(ctx, 'ls -lh', false, true),
}),
'Running - simple command': defineComponentFixture({
render: ctx => renderCollapsible(ctx, 'ls -lh', false, false),
}),
'Ran sandbox - simple command': defineComponentFixture({
render: ctx => renderCollapsible(ctx, 'ls -lh', true, true),
}),
'Running sandbox - simple command': defineComponentFixture({
render: ctx => renderCollapsible(ctx, 'ls -lh', true, false),
}),
'Ran - special chars': defineComponentFixture({
render: ctx => renderCollapsible(ctx, 'grep -rn "hello" ./src --include="*.ts"', false, true),
}),
'Ran sandbox - special chars': defineComponentFixture({
render: ctx => renderCollapsible(ctx, 'grep -rn "hello" ./src --include="*.ts"', true, true),
}),
'Ran - backticks': defineComponentFixture({
render: ctx => renderCollapsible(ctx, 'echo `date` && echo `hostname`', false, true),
}),
'Ran sandbox - backticks': defineComponentFixture({
render: ctx => renderCollapsible(ctx, 'echo `date` && echo `hostname`', true, true),
}),
'Ran sandbox - powershell backticks': defineComponentFixture({
render: ctx => renderCollapsible(ctx, 'Get-Process | Where-Object {$_.Name -eq `"notepad`"}', true, true),
}),
});

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ae8dab0fa799afaadf186c93436bca9ac39c36b99b8f9846ed0d7284494be93
size 2288

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:89f27caedd1e45d22e90243691bcc0153195d121dc2c2bc58be4a1d50ace828f
size 2229

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:793aa216b116d5b028adf16db5e8780f7b844bfbdd38657f943c6427de3e320b
size 945

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6500291e4787ee83dbe70cf44c1b6cb0c5c96f4311cf865cba26ce86a92581b4
size 938

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4e88d70bf076040be9311a96a39717699e59e2bd86a9b2ca7e32b2424bfbf273
size 2442

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f267b5737a1cbfeddd000065fa66735c058cdaa090b60445f583db282eb4787d
size 2359

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae563a7c9f939d3fefa4a30e91e1a514ae0de7b881813827110b62def6bd05ef
size 3061

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1d87074216734a0e6a15683060271590bb6ec594dca7ca84c6ae3b0180df2a66
size 2988

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dce5773e95961f082c034816bc63fbf79b8cedecaec9750840fe483d4855f32f
size 4476

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a05a111b62dbdd2f355f31f08a2c5c6d3731a1ce9607d95e66c42e092a1f099
size 4354

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eda8e2a9eeb0b53fee694039861e868d40b711c4f26b95748605f0a14a8a7811
size 1759

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d71509185136eced7961758fde339f267f38966a4e35939c8fa11fde64e641e2
size 1757

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a2a089a8a290b9ccb52d42cdd987ca8ccccdbb0a47394e82f0d6e40b447663a7
size 3156

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:927527224e48e702d81b44beedc1a2395c638dbaaf952d1a1c926d576d2bf15f
size 3152

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3647c8c57337952dd845f9151b3ddfbb074857e4036d76dd6c7d7beda734c676
size 1170

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c0008e3885e2e6bf569757ace983c635feb9da86d470f2455e4a6884d7d22b6e
size 1139

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a1f340aaeaa112ab63fa6a6048fc76618097114ffbf9ada8a4e8f7736df5c5dd
size 1932

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f3a5c44027dd8659083344aa7f15acd02d2e59d53913b4ca58e5345204470f0d
size 1893