/**
* VSCode Automation MCP Server - UI Action Tools
*
* Tools for interacting with VSCode UI elements: clicking, typing, and opening files.
*
* @author Sukarth Acharya
* @license MIT
*/
import { z } from 'zod';
import { getVSCodeDriver } from '../vscode-driver.js';
import type { SelectorType } from '../types.js';
/**
* Selector type enum for Zod validation
*/
const selectorTypeSchema = z.enum(['css', 'xpath', 'accessibility', 'text']).default('css');
/**
* Input schema for vscode_click_element tool
*/
export const clickElementInputSchema = {
selector: z.string().describe('The selector to find the element (CSS, XPath, accessibility label, or text content)'),
selectorType: selectorTypeSchema.describe('Type of selector: "css", "xpath", "accessibility", or "text"'),
doubleClick: z.boolean().optional().default(false).describe('Whether to perform a double-click'),
rightClick: z.boolean().optional().default(false).describe('Whether to perform a right-click (context menu)'),
timeout: z.number().optional().default(5000).describe('Timeout in milliseconds to wait for the element'),
};
/**
* Input schema for vscode_type_text tool
*/
export const typeTextInputSchema = {
text: z.string().describe('The text to type'),
selector: z.string().optional().describe('Optional selector to find a specific input element'),
selectorType: selectorTypeSchema.optional().describe('Type of selector if selector is provided'),
clear: z.boolean().optional().default(false).describe('Whether to clear existing text before typing'),
delay: z.number().optional().default(0).describe('Delay in milliseconds between keystrokes'),
pressEnter: z.boolean().optional().default(false).describe('Whether to press Enter after typing'),
};
/**
* Input schema for vscode_open_file tool
*/
export const openFileInputSchema = {
filePath: z.string().describe('The path to the file to open (absolute or relative to workspace)'),
line: z.number().optional().describe('Line number to navigate to (1-indexed)'),
column: z.number().optional().describe('Column number to navigate to (1-indexed)'),
preview: z.boolean().optional().default(false).describe('Whether to open in preview mode'),
};
/**
* Click a UI element in VSCode
*
* This tool allows you to click on any UI element in VSCode by providing
* a selector. Supports CSS selectors, XPath, accessibility labels, and
* text content matching.
*
* @example
* // Click by CSS selector
* await clickElement({ selector: '.explorer-viewlet', selectorType: 'css' });
*
* @example
* // Click by accessibility label
* await clickElement({ selector: 'Explorer', selectorType: 'accessibility' });
*
* @example
* // Double-click on an element
* await clickElement({ selector: '.file-name', selectorType: 'css', doubleClick: true });
*
* @example
* // Right-click for context menu
* await clickElement({ selector: '.file-item', selectorType: 'css', rightClick: true });
*/
export async function clickElement(input: {
selector: string;
selectorType?: SelectorType;
doubleClick?: boolean;
rightClick?: boolean;
timeout?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const result = await driver.clickElement(
{
value: input.selector,
type: input.selectorType || 'css',
timeout: input.timeout,
},
{
doubleClick: input.doubleClick,
rightClick: input.rightClick,
}
);
if (result.success) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message,
selector: input.selector,
selectorType: input.selectorType || 'css',
action: input.doubleClick ? 'double-click' : input.rightClick ? 'right-click' : 'click',
}, null, 2),
}],
};
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: result.error,
selector: input.selector,
selectorType: input.selectorType || 'css',
}, null, 2),
}],
};
}
/**
* Type text into an input field or the currently focused element
*
* This tool allows you to type text into VSCode. If a selector is provided,
* it will first click on that element to focus it. Otherwise, it types into
* the currently focused element.
*
* @example
* // Type into the currently focused element
* await typeText({ text: 'Hello, World!' });
*
* @example
* // Type into a specific input field
* await typeText({ selector: 'input.search-input', text: 'search term' });
*
* @example
* // Clear and type with Enter
* await typeText({ text: 'new content', clear: true, pressEnter: true });
*
* @example
* // Type slowly (useful for autocomplete)
* await typeText({ text: 'import', delay: 50 });
*/
export async function typeText(input: {
text: string;
selector?: string;
selectorType?: SelectorType;
clear?: boolean;
delay?: number;
pressEnter?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const elementSelector = input.selector
? { value: input.selector, type: input.selectorType || 'css' as SelectorType }
: undefined;
const result = await driver.typeText(
input.text,
elementSelector,
{
clear: input.clear,
delay: input.delay,
pressEnter: input.pressEnter,
}
);
if (result.success) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message,
text: input.text.length > 50 ? input.text.substring(0, 50) + '...' : input.text,
textLength: input.text.length,
options: {
selector: input.selector || null,
clear: input.clear || false,
delay: input.delay || 0,
pressEnter: input.pressEnter || false,
},
}, null, 2),
}],
};
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: result.error,
}, null, 2),
}],
};
}
/**
* Open a file in the VSCode editor
*
* This tool opens a file in VSCode. You can optionally navigate to a specific
* line and column position after opening the file.
*
* @example
* // Open a file
* await openFile({ filePath: '/path/to/file.ts' });
*
* @example
* // Open a file and go to a specific line
* await openFile({ filePath: 'src/index.ts', line: 42 });
*
* @example
* // Open a file at a specific line and column
* await openFile({ filePath: 'src/utils.ts', line: 10, column: 5 });
*/
export async function openFile(input: {
filePath: string;
line?: number;
column?: number;
preview?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const result = await driver.openFile(input.filePath, {
line: input.line,
column: input.column,
preview: input.preview,
});
if (result.success) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message,
file: {
path: input.filePath,
line: input.line || null,
column: input.column || null,
},
editorInfo: result.data,
}, null, 2),
}],
};
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: result.error,
filePath: input.filePath,
}, null, 2),
}],
};
}
/**
* Common UI element selectors for reference
*/
export const COMMON_SELECTORS = {
// Activity Bar
ACTIVITY_BAR: '.activitybar',
EXPLORER_ICON: '[aria-label="Explorer"]',
SEARCH_ICON: '[aria-label="Search"]',
SOURCE_CONTROL_ICON: '[aria-label="Source Control"]',
DEBUG_ICON: '[aria-label="Run and Debug"]',
EXTENSIONS_ICON: '[aria-label="Extensions"]',
// Side Bar
SIDEBAR: '.sidebar',
EXPLORER_VIEW: '#workbench\\.view\\.explorer',
// Editor Area
EDITOR_AREA: '.editor-group-container',
ACTIVE_EDITOR: '.editor.active',
EDITOR_TABS: '.tabs-container',
// Bottom Panel
BOTTOM_PANEL: '.panel',
TERMINAL_TAB: '[aria-label="Terminal"]',
PROBLEMS_TAB: '[aria-label="Problems"]',
OUTPUT_TAB: '[aria-label="Output"]',
DEBUG_CONSOLE_TAB: '[aria-label="Debug Console"]',
// Status Bar
STATUS_BAR: '.statusbar',
// Title Bar
TITLE_BAR: '.titlebar',
// Command Palette
COMMAND_PALETTE: '.quick-input-widget',
COMMAND_INPUT: '.quick-input-box input',
// Notifications
NOTIFICATIONS: '.notifications-center',
// Dialogs
DIALOG: '.dialog-container',
} as const;