Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
overflow: hidden;

.chat-tool-invocation-part {
padding: 3px 12px 4px 18px;
padding: 4px 12px 4px 18px;
position: relative;

.chat-used-context {
Expand Down Expand Up @@ -79,7 +79,7 @@
}

.chat-thinking-item.markdown-content {
padding: 5px 12px 6px 24px;
padding: 6px 12px 6px 24px;
position: relative;
font-size: var(--vscode-chat-font-size-body-s);

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

import { OperatingSystem } from '../../../../../../../base/common/platform.js';
import { isPowerShell } from '../../runInTerminalHelpers.js';
import type { ICommandLinePresenter, ICommandLinePresenterOptions, ICommandLinePresenterResult } from './commandLinePresenter.js';

/**
* Command line presenter for Ruby inline commands (`ruby -e "..."`).
* Extracts the Ruby code and sets up Ruby syntax highlighting.
*/
export class RubyCommandLinePresenter implements ICommandLinePresenter {
present(options: ICommandLinePresenterOptions): ICommandLinePresenterResult | undefined {
const extractedRuby = extractRubyCommand(options.commandLine, options.shell, options.os);
if (extractedRuby) {
return {
commandLine: extractedRuby,
language: 'ruby',
languageDisplayName: 'Ruby',
};
}
return undefined;
}
}

/**
* Extracts the Ruby code from a `ruby -e "..."` or `ruby -e '...'` command,
* returning the code with properly unescaped quotes.
*
* @param commandLine The full command line to parse
* @param shell The shell path (to determine quote escaping style)
* @param os The operating system
* @returns The extracted Ruby code, or undefined if not a ruby -e command
*/
export function extractRubyCommand(commandLine: string, shell: string, os: OperatingSystem): string | undefined {
// Match ruby -e "..." pattern (double quotes)
const doubleQuoteMatch = commandLine.match(/^ruby\s+-e\s+"(?<code>.+)"$/s);
if (doubleQuoteMatch?.groups?.code) {
let rubyCode = doubleQuoteMatch.groups.code.trim();

// Return undefined if the trimmed code is empty
if (!rubyCode) {
return undefined;
}

// Unescape quotes based on shell type
if (isPowerShell(shell, os)) {
// PowerShell uses backtick-quote (`") to escape quotes inside double-quoted strings
rubyCode = rubyCode.replace(/`"/g, '"');
} else {
// Bash/sh/zsh use backslash-quote (\")
rubyCode = rubyCode.replace(/\\"/g, '"');
}

return rubyCode;
}

// Match ruby -e '...' pattern (single quotes)
// Single quotes in bash/sh/zsh are literal - no escaping inside
// Single quotes in PowerShell are also literal
const singleQuoteMatch = commandLine.match(/^ruby\s+-e\s+'(?<code>.+)'$/s);
if (singleQuoteMatch?.groups?.code) {
const rubyCode = singleQuoteMatch.groups.code.trim();

// Return undefined if the trimmed code is empty
if (!rubyCode) {
return undefined;
}

return rubyCode;
}

return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh } fro
import type { ICommandLinePresenter } from './commandLinePresenter/commandLinePresenter.js';
import { NodeCommandLinePresenter } from './commandLinePresenter/nodeCommandLinePresenter.js';
import { PythonCommandLinePresenter } from './commandLinePresenter/pythonCommandLinePresenter.js';
import { RubyCommandLinePresenter } from './commandLinePresenter/rubyCommandLinePresenter.js';
import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js';
import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js';
import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js';
Expand Down Expand Up @@ -339,6 +340,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
this._commandLinePresenters = [
new NodeCommandLinePresenter(),
new PythonCommandLinePresenter(),
new RubyCommandLinePresenter(),
];

// Clear out warning accepted state if the setting is disabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
cd: true,
echo: true,
ls: true,
dir: true,
pwd: true,
cat: true,
head: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ok, strictEqual } from 'assert';
import { extractRubyCommand, RubyCommandLinePresenter } from '../../browser/tools/commandLinePresenter/rubyCommandLinePresenter.js';
import { OperatingSystem } from '../../../../../../base/common/platform.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';

suite('extractRubyCommand', () => {
ensureNoDisposablesAreLeakedInTestSuite();

suite('basic extraction', () => {
test('should extract simple ruby -e command with double quotes', () => {
const result = extractRubyCommand(`ruby -e "puts 'hello'"`, 'bash', OperatingSystem.Linux);
strictEqual(result, `puts 'hello'`);
});

test('should return undefined for non-ruby commands', () => {
const result = extractRubyCommand('echo hello', 'bash', OperatingSystem.Linux);
strictEqual(result, undefined);
});

test('should return undefined for ruby without -e flag', () => {
const result = extractRubyCommand('ruby script.rb', 'bash', OperatingSystem.Linux);
strictEqual(result, undefined);
});

test('should extract ruby -e with single quotes', () => {
const result = extractRubyCommand(`ruby -e 'puts "hello"'`, 'bash', OperatingSystem.Linux);
strictEqual(result, 'puts "hello"');
});
});

suite('quote unescaping - Bash', () => {
test('should unescape backslash-escaped quotes in bash', () => {
const result = extractRubyCommand('ruby -e "puts \\"hello\\""', 'bash', OperatingSystem.Linux);
strictEqual(result, 'puts "hello"');
});

test('should handle multiple escaped quotes', () => {
const result = extractRubyCommand('ruby -e "x = \\"hello\\"; puts x"', 'bash', OperatingSystem.Linux);
strictEqual(result, 'x = "hello"; puts x');
});
});

suite('single quotes - literal content', () => {
test('should preserve content literally in single quotes (no unescaping)', () => {
const result = extractRubyCommand(`ruby -e 'puts \\"hello\\"'`, 'bash', OperatingSystem.Linux);
strictEqual(result, 'puts \\"hello\\"');
});

test('should handle single quotes in PowerShell', () => {
const result = extractRubyCommand(`ruby -e 'puts "hello"'`, 'pwsh', OperatingSystem.Windows);
strictEqual(result, 'puts "hello"');
});

test('should extract multiline code in single quotes', () => {
const code = `ruby -e '3.times do |i|\n puts i\nend'`;
const result = extractRubyCommand(code, 'bash', OperatingSystem.Linux);
strictEqual(result, `3.times do |i|\n puts i\nend`);
});
});

suite('quote unescaping - PowerShell', () => {
test('should unescape backtick-escaped quotes in PowerShell', () => {
const result = extractRubyCommand('ruby -e "puts `"hello`""', 'pwsh', OperatingSystem.Windows);
strictEqual(result, 'puts "hello"');
});

test('should handle multiple backtick-escaped quotes', () => {
const result = extractRubyCommand('ruby -e "x = `"hello`"; puts x"', 'pwsh', OperatingSystem.Windows);
strictEqual(result, 'x = "hello"; puts x');
});

test('should not unescape backslash quotes in PowerShell', () => {
const result = extractRubyCommand('ruby -e "puts \\"hello\\""', 'pwsh', OperatingSystem.Windows);
strictEqual(result, 'puts \\"hello\\"');
});
});

suite('multiline code', () => {
test('should extract multiline Ruby code', () => {
const code = `ruby -e "3.times do |i|\n puts i\nend"`;
const result = extractRubyCommand(code, 'bash', OperatingSystem.Linux);
strictEqual(result, `3.times do |i|\n puts i\nend`);
});
});

suite('edge cases', () => {
test('should handle code with trailing whitespace trimmed', () => {
const result = extractRubyCommand('ruby -e " puts 1 "', 'bash', OperatingSystem.Linux);
strictEqual(result, 'puts 1');
});

test('should return undefined for empty code', () => {
const result = extractRubyCommand('ruby -e ""', 'bash', OperatingSystem.Linux);
strictEqual(result, undefined);
});

test('should return undefined when quotes are unmatched', () => {
const result = extractRubyCommand('ruby -e "puts 1', 'bash', OperatingSystem.Linux);
strictEqual(result, undefined);
});
});
});

suite('RubyCommandLinePresenter', () => {
ensureNoDisposablesAreLeakedInTestSuite();

const presenter = new RubyCommandLinePresenter();

test('should return Ruby presentation for ruby -e command', () => {
const result = presenter.present({
commandLine: `ruby -e "puts 'hello'"`,
shell: 'bash',
os: OperatingSystem.Linux
});
ok(result);
strictEqual(result.commandLine, `puts 'hello'`);
strictEqual(result.language, 'ruby');
strictEqual(result.languageDisplayName, 'Ruby');
});

test('should return undefined for non-ruby commands', () => {
const result = presenter.present({
commandLine: 'echo hello',
shell: 'bash',
os: OperatingSystem.Linux
});
strictEqual(result, undefined);
});

test('should return undefined for regular ruby script execution', () => {
const result = presenter.present({
commandLine: 'ruby script.rb',
shell: 'bash',
os: OperatingSystem.Linux
});
strictEqual(result, undefined);
});

test('should handle PowerShell backtick escaping', () => {
const result = presenter.present({
commandLine: 'ruby -e "puts `"hello`""',
shell: 'pwsh',
os: OperatingSystem.Windows
});
ok(result);
strictEqual(result.commandLine, 'puts "hello"');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ suite('RunInTerminalTool', () => {
'echo "abc"',
'echo \'abc\'',
'ls -la',
'dir',
'pwd',
'cat file.txt',
'head -n 10 file.txt',
Expand Down
Loading