Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Conversation

@Abrifq
Copy link
Contributor

@Abrifq Abrifq commented Oct 21, 2025

Closes #272416.
I have tested the changes manually via codespaces.

@Abrifq Abrifq force-pushed the Abrifq/shell-integration-prompt-input-visual-fix branch from 479b5dd to 6a9c04d Compare October 22, 2025 07:19
Copy link
Contributor Author

@Abrifq Abrifq left a comment

Choose a reason for hiding this comment

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

OoT/pet peeve:
Prompt input sometimes gets an extra space appended.

Need to check _doSync() for what could cause it:

private _doSync() {
if (this._state !== PromptInputState.Input) {
return;
}
let commandStartY = this._commandStartMarker?.line;
if (commandStartY === undefined) {
return;
}
const buffer = this._xterm.buffer.active;
let line = buffer.getLine(commandStartY);
const absoluteCursorY = buffer.baseY + buffer.cursorY;
let cursorIndex: number | undefined;
let commandLine = line?.translateToString(true, this._commandStartX);
if (this._shellType === PosixShellType.Fish && (!line || !commandLine)) {
commandStartY += 1;
line = buffer.getLine(commandStartY);
if (line) {
commandLine = line.translateToString(true);
cursorIndex = absoluteCursorY === commandStartY ? buffer.cursorX : commandLine?.trimEnd().length;
}
}
if (line === undefined || commandLine === undefined) {
this._logService.trace(`PromptInputModel#_sync: no line`);
return;
}
let value = commandLine;
let ghostTextIndex = -1;
if (cursorIndex === undefined) {
if (absoluteCursorY === commandStartY) {
cursorIndex = Math.min(this._getRelativeCursorIndex(this._commandStartX, buffer, line), commandLine.length);
} else {
cursorIndex = commandLine.trimEnd().length;
}
}
// From command start line to cursor line
for (let y = commandStartY + 1; y <= absoluteCursorY; y++) {
const nextLine = buffer.getLine(y);
const lineText = nextLine?.translateToString(true);
if (lineText && nextLine) {
// Check if the line wrapped without a new line (continuation) or
// we're on the last line and the continuation prompt is not present, so we need to add the value
if (nextLine.isWrapped || (absoluteCursorY === y && this._continuationPrompt && !this._lineContainsContinuationPrompt(lineText))) {
value += `${lineText}`;
const relativeCursorIndex = this._getRelativeCursorIndex(0, buffer, nextLine);
if (absoluteCursorY === y) {
cursorIndex += relativeCursorIndex;
} else {
cursorIndex += lineText.length;
}
} else if (this._shellType === PosixShellType.Fish) {
if (value.endsWith('\\')) {
// Trim off the trailing backslash
value = value.substring(0, value.length - 1);
value += `${lineText.trim()}`;
cursorIndex += lineText.trim().length - 1;
} else {
if (/^ {6,}/.test(lineText)) {
// Was likely a new line
value += `\n${lineText.trim()}`;
cursorIndex += lineText.trim().length + 1;
} else {
value += lineText;
cursorIndex += lineText.length;
}
}
}
// Verify continuation prompt if we have it, if this line doesn't have it then the
// user likely just pressed enter.
else if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) {
const trimmedLineText = this._trimContinuationPrompt(lineText);
value += `\n${trimmedLineText}`;
if (absoluteCursorY === y) {
const continuationCellWidth = this._getContinuationPromptCellWidth(nextLine, lineText);
const relativeCursorIndex = this._getRelativeCursorIndex(continuationCellWidth, buffer, nextLine);
cursorIndex += relativeCursorIndex + 1;
} else {
cursorIndex += trimmedLineText.length + 1;
}
}
}
}
// Below cursor line
for (let y = absoluteCursorY + 1; y < buffer.baseY + this._xterm.rows; y++) {
const belowCursorLine = buffer.getLine(y);
const lineText = belowCursorLine?.translateToString(true);
if (lineText && belowCursorLine) {
if (this._shellType === PosixShellType.Fish) {
value += `${lineText}`;
} else if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) {
value += `\n${this._trimContinuationPrompt(lineText)}`;
} else {
value += lineText;
}
} else {
break;
}
}
if (this._logService.getLevel() === LogLevel.Trace) {
this._logService.trace(`PromptInputModel#_sync: ${this.getCombinedString()}`);
}
// Adjust trailing whitespace
{
let trailingWhitespace = this._value.length - this._value.trimEnd().length;
// Handle backspace key
if (this._lastUserInput === '\x7F') {
this._lastUserInput = '';
if (cursorIndex === this._cursorIndex - 1) {
// If trailing whitespace is being increased by removing a non-whitespace character
if (this._value.trimEnd().length > value.trimEnd().length && value.trimEnd().length <= cursorIndex) {
trailingWhitespace = Math.max((this._value.length - 1) - value.trimEnd().length, 0);
}
// Standard case; subtract from trailing whitespace
else {
trailingWhitespace = Math.max(trailingWhitespace - 1, 0);
}
}
}
// Handle delete key
if (this._lastUserInput === '\x1b[3~') {
this._lastUserInput = '';
if (cursorIndex === this._cursorIndex) {
trailingWhitespace = Math.max(trailingWhitespace - 1, 0);
}
}
const valueLines = value.split('\n');
const isMultiLine = valueLines.length > 1;
const valueEndTrimmed = value.trimEnd();
if (!isMultiLine) {
// Adjust trimmed whitespace value based on cursor position
if (valueEndTrimmed.length < value.length) {
// Handle space key
if (this._lastUserInput === ' ') {
this._lastUserInput = '';
if (cursorIndex > valueEndTrimmed.length && cursorIndex > this._cursorIndex) {
trailingWhitespace++;
}
}
trailingWhitespace = Math.max(cursorIndex - valueEndTrimmed.length, trailingWhitespace, 0);
}
// Handle case where a non-space character is inserted in the middle of trailing whitespace
const charBeforeCursor = cursorIndex === 0 ? '' : value[cursorIndex - 1];
if (trailingWhitespace > 0 && cursorIndex === this._cursorIndex + 1 && this._lastUserInput !== '' && charBeforeCursor !== ' ') {
trailingWhitespace = this._value.length - this._cursorIndex;
}
}
if (isMultiLine) {
valueLines[valueLines.length - 1] = valueLines.at(-1)?.trimEnd() ?? '';
const continuationOffset = (valueLines.length - 1) * (this._continuationPrompt?.length ?? 0);
trailingWhitespace = Math.max(0, cursorIndex - value.length - continuationOffset);
}
value = valueLines.map(e => e.trimEnd()).join('\n') + ' '.repeat(trailingWhitespace);
}
ghostTextIndex = this._scanForGhostText(buffer, line, cursorIndex);
if (this._value !== value || this._cursorIndex !== cursorIndex || this._ghostTextIndex !== ghostTextIndex) {
this._value = value;
this._cursorIndex = cursorIndex;
this._ghostTextIndex = ghostTextIndex;
this._onDidChangeInput.fire(this._createStateObject());
}
}

@Abrifq Abrifq force-pushed the Abrifq/shell-integration-prompt-input-visual-fix branch from 6a9c04d to 7b0877c Compare October 22, 2025 09:49
@Abrifq Abrifq marked this pull request as ready for review October 22, 2025 09:49
@Abrifq
Copy link
Contributor Author

Abrifq commented Oct 22, 2025

Hi, I have tested my PR within codespaces' VNC, it does work now.

I had to enable supportHtml to wrap with <code> though, so you may want to check if another input could inject links/html.

const shellProcessString = getShellProcessTooltip(instance, !!showDetailed);
const content = new MarkdownString(instance.title + shellProcessString + statusString, { supportThemeIcons: true, supportHtml: true });
return { content, actions };


Prompt input sometimes gets an extra space appended.

There might be a desync causing this, needs more investigation. As I started that comment, it is out of topic.

@Abrifq Abrifq force-pushed the Abrifq/shell-integration-prompt-input-visual-fix branch from ab88aa6 to 5e797a9 Compare October 22, 2025 10:10
@Abrifq Abrifq changed the title Replace Shell Integration's Prompt Input input's backtick wrapping with pre element Replace Shell Integration's Prompt Input input's backtick wrapping with <code>wrapping</code> Oct 27, 2025
@Abrifq Abrifq force-pushed the Abrifq/shell-integration-prompt-input-visual-fix branch from 5e797a9 to fc5842d Compare October 27, 2025 12:16
@meganrogge
Copy link
Contributor

Thanks for the PR. Can you pls provide a screenshot of this working for you in testing?

@Abrifq Abrifq force-pushed the Abrifq/shell-integration-prompt-input-visual-fix branch from fc5842d to 3a92ee4 Compare October 28, 2025 05:23
@Abrifq

This comment was marked as outdated.

@Abrifq Abrifq force-pushed the Abrifq/shell-integration-prompt-input-visual-fix branch from 3a92ee4 to 4e717e0 Compare October 28, 2025 05:47
@Abrifq
Copy link
Contributor Author

Abrifq commented Oct 28, 2025

Update:

Prefixing backslashes to backticks makes them show up again.
However, I think I'll have to escape most of the markdown characters because I can still escape things out:

image

I'll also check if just applying db999f4 onto main works.

@Abrifq
Copy link
Contributor Author

Abrifq commented Oct 28, 2025

I will send the patch in after also testing with links with titles, but here is how it looks right now:

image

If there is any other markdown escapes you would like me to check, I'll happily test but for now I changed the code to:

  1. If there are any < or >, which could cause html injections, they are replaced with &lt; and &gt;.
  2. If there is a markdown link, the link part gets wrapped with < and >, as escaping in step 3 will add a ) to the link's end.
  3. Escape [, ], (, ), -, *, !, # and ` characters by prefixing with backslashes, as markdown renderer doesn't skip markdown in <code> elements. (Maybe sub-issue?)

I thought your issue was backticks only. Why are you escaping everything else?

That was my first thought but I now realized I should be able to see what I wrote as visually unchanged as possible on the detail popup.

@Abrifq Abrifq force-pushed the Abrifq/shell-integration-prompt-input-visual-fix branch from 644016f to 3ccfea3 Compare October 28, 2025 08:01
@Abrifq
Copy link
Contributor Author

Abrifq commented Oct 28, 2025

I will send the patch in after also testing with links with titles

I have tested (https://regexr.com/8hu9f) and it should be good for now.

Testing with this input:

echo `date`; echo </code>[escaped link?](https://youtu.be/dQw4w9WgXcQ "asdasdasd")

Main Branch (124710c)

image

My patch (3ccfea3)

image

@Abrifq
Copy link
Contributor Author

Abrifq commented Oct 28, 2025

I'll also check if just applying db999f4 onto main works.

It does not. Until someone confirms that markdown rendering inside <code> is a bug and gets that fixed, this seems like the best solution.

If I didn't misread the code when I first opened this issue/PR, the changes I made should run once per keystroke, so it should -hopefully- have only negligible performance impacts.

@Abrifq Abrifq force-pushed the Abrifq/shell-integration-prompt-input-visual-fix branch from 3ccfea3 to 9030ea6 Compare October 28, 2025 10:34
@Abrifq Abrifq force-pushed the Abrifq/shell-integration-prompt-input-visual-fix branch from 9030ea6 to d30c10e Compare October 28, 2025 10:36
@Abrifq
Copy link
Contributor Author

Abrifq commented Nov 3, 2025

I guess feature release from Github Universe could have created some urgent backlog but is there anything blocking this PR from being reviewed?

Edit: Well, the Monthly Endgame has started. Oh well.

@Abrifq
Copy link
Contributor Author

Abrifq commented Nov 12, 2025

Hello @meganrogge, while waiting for this PR to get reviewed, should I update the base branch to test against newer base or should I leave as is?

I'm asking this in general, it's not mentioned in contributing guide.

@meganrogge meganrogge removed their assignment Nov 12, 2025
@meganrogge meganrogge requested a review from Tyriar November 12, 2025 13:48

const shellProcessString = getShellProcessTooltip(instance, !!showDetailed);
const content = new MarkdownString(instance.title + shellProcessString + statusString, { supportThemeIcons: true });
const content = new MarkdownString(instance.title + shellProcessString + statusString, { supportThemeIcons: true, supportHtml: true });
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to supportHtml here to fix this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Current markdown parsing doesn't allow for escaping backticks via a backslash, and to workaround that I have changed the backtick wrapping with <code> wrapping.

Code wrapping does not work unless supportHtml is enabled, though. Which is why I enabled it.

Comment on lines 112 to 116
const escapedPromptInput = combinedString
.replaceAll('<', '&lt;').replaceAll('>', '&gt;') //Prevent escaping from wrapping
.replaceAll(/\((.+?)(\|?(?: (?:.+?)?)?)\)/g, '(<$1>$2)') //Escape links as clickable links
.replaceAll(/([\[\]\(\)\-\*\!\#\`])/g, '\\$1'); //Comment most of the markdown elements to not render them inside
detailedAdditions.push(`Prompt input: <code>\n${escapedPromptInput}\n</code>`);
Copy link
Member

Choose a reason for hiding this comment

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

isn't this just as simple as escaping inner backticks like this?

combinedString.replaceAll('`', '\\`')

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have tried it and it didn't work that time
( #272425 (comment) )

https://github.com/Abrifq/vscode/tree/Abrifq/alt-wrap-fix

But I will update the branch & try again and report here.

Copy link
Member

Choose a reason for hiding this comment

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

The current proposal seems far too complex for what this actually does imo, all it's doing is making debug information show a little nicer. Does this helper do the job?

export function escapeMarkdownSyntaxTokens(text: string): string {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
return text.replace(/[\\`*_{}[\]()#+\-!~]/g, '\\$&'); // CodeQL [SM02383] Backslash is escaped in the character class
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does this helper do the job?

export function escapeMarkdownSyntaxTokens(text: string): string {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
return text.replace(/[\\`*_{}[\]()#+\-!~]/g, '\\$&'); // CodeQL [SM02383] Backslash is escaped in the character class
}

Yeah, it should replace line 115. Thanks!

The current proposal seems far too complex for what this actually does imo, all it's doing is making debug information show a little nicer.

I need to check if lines 112, 113 & 116 themselves are good enough to ONLY fix the wrapping, but I faintly recall they were not.

As for line 114, I have stated in meganrogge's review that it's basically optional and can be removed.

I'll see what I can do within my lunch break, and thanks for the helper.

@Tyriar Tyriar self-assigned this Nov 13, 2025
@Tyriar Tyriar changed the title Replace Shell Integration's Prompt Input input's backtick wrapping with <code>wrapping</code> Fix terminal tab prompt input breaking when backticks are included Nov 13, 2025
@Tyriar Tyriar requested a review from meganrogge November 13, 2025 13:54
@Tyriar Tyriar added this to the November 2025 milestone Nov 13, 2025
Copy link
Member

@Tyriar Tyriar left a comment

Choose a reason for hiding this comment

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

@meganrogge simplified it, basically handling the edge case where backtick is included specially instead of adding HTML support/complicated regexes. Could you check it out?

@Tyriar Tyriar enabled auto-merge November 13, 2025 13:55
@Tyriar
Copy link
Member

Tyriar commented Nov 13, 2025

@Abrifq let me know if you're able to break this one, but I think it's solid

Copy link
Contributor

@meganrogge meganrogge left a comment

Choose a reason for hiding this comment

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

Works for me

@Tyriar Tyriar merged commit 6f121ab into microsoft:main Nov 13, 2025
17 checks passed
@Abrifq
Copy link
Contributor Author

Abrifq commented Nov 14, 2025

@Abrifq let me know if you're able to break this one, but I think it's solid

Sorry, I just became available to check, I will update but this feels weird on UX.

image

@Abrifq
Copy link
Contributor Author

Abrifq commented Nov 14, 2025

There is an even simpler version, I will make a new PR on top of this one.

@@ -109,15 +109,8 @@ export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) {
 	}
 	const combinedString = instance.capabilities.get(TerminalCapability.CommandDetection)?.promptInputModel.getCombinedString();
 	if (combinedString !== undefined) {
-		if (combinedString.includes('`')) {
-			detailedAdditions.push('Prompt input:' + [
-				'```',
-				combinedString, // No new lines so no need to escape ```
-				'```',
-			].map(e => `\n    ${e}`).join(''));
-		} else {
-			detailedAdditions.push(`Prompt input: \`${combinedString.replaceAll('`', '&#96;')}\``);
-		}
+		// Wrap with triple backticks so that single backticks can show up (command substitution in bash uses backticks, for example)
+		detailedAdditions.push(`Prompt input: \`\`\`${combinedString}\`\`\``);
 	}
 	const detailedAdditionsString = detailedAdditions.length > 0
 		? '\n\n' + detailedAdditions.map(e => `- ${e}`).join('\n')

@Abrifq
Copy link
Contributor Author

Abrifq commented Nov 14, 2025

#272425 (comment)
The simplest solution will be only wrapping with triple backticks and assume the user is not writing markdown in command line.

Using two backticks in a row is pretty much useless as it just makes an empty string.

Well, that's the simplest, the cleaner solution would be either adding some if not all css properties of <code> to <pre> or to tweak markdown string implementation (and usage) to append/add another instance of markdown string w/o changing the props of the parent one.

@Tyriar
Copy link
Member

Tyriar commented Nov 14, 2025

@Abrifq we definitely don't want to support HTML there. I also don't want to get into trying to change the markdown rendering. Again this is just debug info that's hidden by default, handling all edge cases perfectly isn't too important.

Tyriar added a commit that referenced this pull request Nov 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Command text on the Shell Integration details pop-up can escape the code block

3 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.