diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css index bec837f2640d0..4d6484b28e590 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatThinkingContent.css @@ -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 { @@ -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); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts new file mode 100644 index 0000000000000..6f0f6b6fafddd --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/rubyCommandLinePresenter.ts @@ -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+"(?.+)"$/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+'(?.+)'$/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; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index a9a95bde247ec..6bbe90a640c1d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -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'; @@ -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 diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index be29b27e9e6ac..89e4ada38b858 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -163,6 +163,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary { + 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"'); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index a8412b10371ae..e3c99bbd3459b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -201,6 +201,7 @@ suite('RunInTerminalTool', () => { 'echo "abc"', 'echo \'abc\'', 'ls -la', + 'dir', 'pwd', 'cat file.txt', 'head -n 10 file.txt',