/**
* VSCode Automation MCP Server - Inspection Tools
*
* Tools for inspecting VSCode UI: screenshots, element info, diagnostics, and webviews.
*
* @author Sukarth Acharya
* @license MIT
*/
import { z } from 'zod';
import { getVSCodeDriver } from '../vscode-driver.js';
import type { SelectorType, ElementInfo, DiagnosticMessage } from '../types.js';
/**
* Selector type enum for Zod validation
*/
const selectorTypeSchema = z.enum(['css', 'xpath', 'accessibility', 'text']).default('css');
/**
* Input schema for vscode_take_screenshot tool
*/
export const takeScreenshotInputSchema = {
filename: z.string().optional().describe('Optional filename for the screenshot (will be saved with .png extension)'),
returnBase64: z.boolean().optional().default(true).describe('Whether to return base64-encoded image data'),
};
/**
* Input schema for vscode_get_element tool
*/
export const getElementInputSchema = {
selector: z.string().describe('The selector to find the element'),
selectorType: selectorTypeSchema.describe('Type of selector: "css", "xpath", "accessibility", or "text"'),
timeout: z.number().optional().default(5000).describe('Timeout in milliseconds to wait for the element'),
};
/**
* Input schema for vscode_open_webview tool
*/
export const openWebviewInputSchema = {
title: z.string().describe('The title or command of the webview to open'),
};
/**
* Input schema for vscode_get_diagnostics tool
*/
export const getDiagnosticsInputSchema = {
severity: z.enum(['all', 'error', 'warning', 'info']).optional().default('all')
.describe('Filter diagnostics by severity level'),
};
/**
* Input schema for vscode_get_dom tool
*/
export const getDomInputSchema = {
selector: z.string().optional().default('body').describe('CSS selector for the root element to inspect (default: "body" for full page)'),
depth: z.number().optional().default(10).describe('Maximum depth to traverse the DOM tree (default: 10)'),
includeStyles: z.boolean().optional().default(false).describe('Include computed styles for each element'),
includeHidden: z.boolean().optional().default(false).describe('Include hidden elements in the output'),
format: z.enum(['tree', 'html', 'json']).optional().default('tree').describe('Output format: "tree" (readable), "html" (raw HTML), or "json" (structured)'),
};
/**
* Input schema for vscode_get_ui_structure tool
*/
export const getUiStructureInputSchema = {
region: z.enum(['full', 'sidebar', 'editor', 'panel', 'activitybar', 'statusbar', 'titlebar', 'menubar']).optional().default('full')
.describe('VSCode UI region to inspect'),
depth: z.number().optional().default(5).describe('Maximum depth to traverse'),
};
/**
* Input schema for vscode_query_elements tool
*/
export const queryElementsInputSchema = {
selector: z.string().describe('CSS selector to find elements'),
limit: z.number().optional().default(20).describe('Maximum number of elements to return'),
attributes: z.array(z.string()).optional().describe('Specific attributes to include in the response'),
};
/**
* Input schema for vscode_get_accessibility_tree tool
*/
export const getAccessibilityTreeInputSchema = {
selector: z.string().optional().default('body').describe('CSS selector for the root element'),
depth: z.number().optional().default(8).describe('Maximum depth to traverse'),
};
/**
* Input schema for vscode_get_element_children tool
*/
export const getElementChildrenInputSchema = {
selector: z.string().describe('CSS selector for the parent element'),
directOnly: z.boolean().optional().default(true).describe('If true, only return direct children. If false, return all descendants up to depth.'),
depth: z.number().optional().default(1).describe('Depth of children to retrieve (only used when directOnly is false)'),
includeText: z.boolean().optional().default(true).describe('Include text content of children'),
};
/**
* Input schema for vscode_get_element_parents tool
*/
export const getElementParentsInputSchema = {
selector: z.string().describe('CSS selector for the element to find parents of'),
levels: z.number().optional().default(5).describe('Number of parent levels to traverse up'),
};
/**
* Input schema for vscode_get_element_siblings tool
*/
export const getElementSiblingsInputSchema = {
selector: z.string().describe('CSS selector for the element'),
direction: z.enum(['all', 'previous', 'next']).optional().default('all').describe('Which siblings to return'),
};
/**
* Input schema for vscode_find_interactive_elements tool
*/
export const findInteractiveElementsInputSchema = {
selector: z.string().optional().default('body').describe('CSS selector for the container to search within'),
types: z.array(z.enum(['button', 'input', 'link', 'select', 'checkbox', 'radio', 'tab', 'menu', 'treeitem'])).optional()
.describe('Types of interactive elements to find'),
visibleOnly: z.boolean().optional().default(true).describe('Only return visible elements'),
};
/**
* Input schema for vscode_dump_dom_to_file tool
*/
export const dumpDomToFileInputSchema = {
filePath: z.string().describe('Absolute path where to save the DOM dump'),
selector: z.string().optional().default('body').describe('CSS selector for the root element'),
format: z.enum(['tree', 'html', 'json']).optional().default('json').describe('Output format'),
depth: z.number().optional().default(20).describe('Maximum depth to traverse'),
};
/**
* Input schema for vscode_search_dom tool
*/
export const searchDomInputSchema = {
query: z.string().describe('Text to search for in element text, attributes, or IDs'),
searchIn: z.array(z.enum(['text', 'id', 'class', 'aria-label', 'title', 'role', 'placeholder'])).optional()
.default(['text', 'aria-label', 'title']).describe('Where to search for the query'),
limit: z.number().optional().default(20).describe('Maximum number of results'),
caseSensitive: z.boolean().optional().default(false).describe('Case-sensitive search'),
};
/**
* Input schema for vscode_execute_script tool
*/
export const executeScriptInputSchema = {
script: z.string().describe('JavaScript code to execute in the VSCode window context. Has access to document, window, etc.'),
returnType: z.enum(['json', 'string', 'none']).optional().default('json')
.describe('How to handle the return value: "json" (parse as JSON), "string" (toString), or "none" (ignore)'),
};
/**
* Input schema for vscode_query_selector tool
*/
export const querySelectorInputSchema = {
selector: z.string().describe('CSS selector to find element(s)'),
all: z.boolean().optional().default(false).describe('If true, use querySelectorAll and return multiple elements'),
properties: z.array(z.string()).optional()
.describe('Specific properties to extract (e.g., ["textContent", "value", "href"]). If not provided, returns common properties.'),
};
/**
* Input schema for vscode_get_element_by_id tool
*/
export const getElementByIdInputSchema = {
id: z.string().describe('The ID of the element to find (without # prefix)'),
properties: z.array(z.string()).optional()
.describe('Specific properties to extract. If not provided, returns common properties.'),
};
/**
* Input schema for vscode_get_elements_by_class tool
*/
export const getElementsByClassInputSchema = {
className: z.string().describe('The class name to search for (without . prefix)'),
limit: z.number().optional().default(50).describe('Maximum number of elements to return'),
properties: z.array(z.string()).optional()
.describe('Specific properties to extract. If not provided, returns common properties.'),
};
/**
* Input schema for vscode_get_elements_by_tag tool
*/
export const getElementsByTagInputSchema = {
tagName: z.string().describe('The HTML tag name (e.g., "button", "input", "div")'),
limit: z.number().optional().default(50).describe('Maximum number of elements to return'),
properties: z.array(z.string()).optional()
.describe('Specific properties to extract. If not provided, returns common properties.'),
};
/**
* Take a screenshot of the VSCode window
*
* This tool captures a screenshot of the current VSCode window.
* The screenshot can be saved to a file and/or returned as base64 data.
*
* @example
* // Take a screenshot with default settings
* await takeScreenshot({});
*
* @example
* // Take a screenshot and save with a specific filename
* await takeScreenshot({ filename: 'my-test-screenshot' });
*/
export async function takeScreenshot(input: {
filename?: string;
returnBase64?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }> }> {
const driver = getVSCodeDriver();
// Default returnBase64 to true so AI can always see the screenshot
const shouldReturnBase64 = input.returnBase64 !== false;
const result = await driver.takeScreenshot(input.filename);
if (result.success && result.data) {
const content: Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }> = [];
// Add metadata as text
content.push({
type: 'text' as const,
text: JSON.stringify({
success: true,
message: result.message,
filePath: result.data.filePath,
dimensions: {
width: result.data.width,
height: result.data.height,
},
}, null, 2),
});
// Include base64 image data (default: true)
if (shouldReturnBase64 && result.data.base64) {
content.push({
type: 'image' as const,
data: result.data.base64,
mimeType: result.data.mimeType || 'image/png',
});
}
return { content };
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: result.error,
}, null, 2),
}],
};
}
/**
* Get information about a UI element
*
* This tool retrieves detailed information about a UI element including
* its properties, text content, position, size, and attributes.
*
* @example
* // Get element by CSS selector
* await getElement({ selector: '.explorer-viewlet', selectorType: 'css' });
*
* @example
* // Get element by accessibility label
* await getElement({ selector: 'Explorer', selectorType: 'accessibility' });
*
* @example
* // Get element by text content
* await getElement({ selector: 'package.json', selectorType: 'text' });
*/
export async function getElement(input: {
selector: string;
selectorType?: SelectorType;
timeout?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const result = await driver.getElement({
value: input.selector,
type: input.selectorType || 'css',
timeout: input.timeout,
});
if (result.success && result.data) {
const elementInfo: ElementInfo = result.data;
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: input.selector,
selectorType: input.selectorType || 'css',
element: {
tagName: elementInfo.tagName,
text: elementInfo.text,
isDisplayed: elementInfo.isDisplayed,
isEnabled: elementInfo.isEnabled,
isSelected: elementInfo.isSelected,
location: elementInfo.location,
size: elementInfo.size,
cssClasses: elementInfo.cssClasses,
attributes: elementInfo.attributes,
},
}, null, 2),
}],
};
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: result.error,
selector: input.selector,
selectorType: input.selectorType || 'css',
}, null, 2),
}],
};
}
/**
* Open a webview panel
*
* This tool opens a webview panel in VSCode. Webviews are used by extensions
* to display custom UI content like settings pages, previews, or interactive panels.
*
* @example
* // Open a webview by its title
* await openWebview({ title: 'My Extension Settings' });
*
* @example
* // Open a webview by command
* await openWebview({ title: 'Markdown Preview' });
*/
export async function openWebview(input: {
title: string;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const result = await driver.openWebview(input.title);
if (result.success) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message,
title: input.title,
}, null, 2),
}],
};
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: result.error,
title: input.title,
}, null, 2),
}],
};
}
/**
* Get diagnostic messages from the Problems panel
*
* This tool retrieves all diagnostic messages (errors, warnings, info)
* from VSCode's Problems panel. This is useful for checking for
* compilation errors, linting issues, or other diagnostics.
*
* @example
* // Get all diagnostics
* await getDiagnostics({});
*
* @example
* // Get only errors
* await getDiagnostics({ severity: 'error' });
*
* @example
* // Get warnings
* await getDiagnostics({ severity: 'warning' });
*/
export async function getDiagnostics(input: {
severity?: 'all' | 'error' | 'warning' | 'info';
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const result = await driver.getDiagnostics();
if (result.success && result.data) {
let diagnostics: DiagnosticMessage[] = result.data;
// Filter by severity if specified
if (input.severity && input.severity !== 'all') {
diagnostics = diagnostics.filter(d => d.severity === input.severity);
}
// Group diagnostics by severity
const grouped = {
errors: diagnostics.filter(d => d.severity === 'error'),
warnings: diagnostics.filter(d => d.severity === 'warning'),
info: diagnostics.filter(d => d.severity === 'info'),
hints: diagnostics.filter(d => d.severity === 'hint'),
};
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
filter: input.severity || 'all',
summary: {
total: diagnostics.length,
errors: grouped.errors.length,
warnings: grouped.warnings.length,
info: grouped.info.length,
hints: grouped.hints.length,
},
diagnostics: diagnostics.map(d => ({
severity: d.severity,
message: d.message,
file: d.filePath || null,
location: d.line
? { line: d.line, column: d.column || 1 }
: null,
source: d.source || null,
})),
}, null, 2),
}],
};
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: result.error,
}, null, 2),
}],
};
}
/**
* Get the DOM structure of a container element
*
* This is a helper function that can be used to understand the
* structure of VSCode's UI for creating selectors.
*/
export async function getElementStructure(input: {
selector: string;
selectorType?: SelectorType;
depth?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
const By = (await import('selenium-webdriver')).By;
let by: ReturnType<typeof By.css>;
switch (input.selectorType) {
case 'xpath':
by = By.xpath(input.selector);
break;
case 'css':
default:
by = By.css(input.selector);
break;
}
const element = await webDriver.findElement(by);
// Get the outer HTML of the element
const html = await webDriver.executeScript<string>(
`
function getStructure(el, depth, maxDepth) {
if (depth > maxDepth) return '...';
const tag = el.tagName.toLowerCase();
const id = el.id ? '#' + el.id : '';
const classes = el.className ? '.' + el.className.split(' ').join('.') : '';
const attrs = Array.from(el.attributes)
.filter(a => !['id', 'class'].includes(a.name))
.slice(0, 3)
.map(a => a.name + '=' + JSON.stringify(a.value.slice(0, 30)))
.join(' ');
let result = '<' + tag + id + classes + (attrs ? ' ' + attrs : '') + '>';
if (el.children.length > 0 && depth < maxDepth) {
result += '\\n';
for (const child of el.children) {
result += ' '.repeat(depth + 1) + getStructure(child, depth + 1, maxDepth) + '\\n';
}
result += ' '.repeat(depth) + '</' + tag + '>';
} else if (el.textContent && el.textContent.trim()) {
result += el.textContent.trim().slice(0, 50) + '</' + tag + '>';
} else {
result = result.slice(0, -1) + ' />';
}
return result;
}
return getStructure(arguments[0], 0, arguments[1]);
`,
element,
input.depth || 3
);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: input.selector,
structure: html,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
selector: input.selector,
}, null, 2),
}],
};
}
}
/**
* Get the full DOM structure of VSCode window
*
* This tool retrieves the complete DOM tree of the VSCode window,
* allowing AI agents to understand the UI structure for automation.
*
* @example
* // Get the full page DOM as a tree
* await getDom({});
*
* @example
* // Get DOM of a specific element with custom depth
* await getDom({ selector: '.editor-container', depth: 5 });
*
* @example
* // Get raw HTML of the sidebar
* await getDom({ selector: '.sidebar', format: 'html' });
*/
export async function getDom(input: {
selector?: string;
depth?: number;
includeStyles?: boolean;
includeHidden?: boolean;
format?: 'tree' | 'html' | 'json';
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const selector = input.selector || 'body';
const depth = input.depth || 10;
const format = input.format || 'tree';
const includeStyles = input.includeStyles || false;
const includeHidden = input.includeHidden || false;
try {
const By = (await import('selenium-webdriver')).By;
const element = await webDriver.findElement(By.css(selector));
let result: string;
if (format === 'html') {
// Get raw outer HTML
result = await webDriver.executeScript<string>(
`return arguments[0].outerHTML;`,
element
);
} else if (format === 'json') {
// Get structured JSON representation
result = await webDriver.executeScript<string>(
`
function domToJson(el, currentDepth, maxDepth, includeStyles, includeHidden) {
if (currentDepth > maxDepth) return { truncated: true };
const style = window.getComputedStyle(el);
if (!includeHidden && (style.display === 'none' || style.visibility === 'hidden')) {
return null;
}
const rect = el.getBoundingClientRect();
const node = {
tag: el.tagName.toLowerCase(),
id: el.id || undefined,
classes: el.className ? el.className.split(' ').filter(c => c) : undefined,
attributes: {},
bounds: {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height)
},
text: undefined,
children: []
};
// Get important attributes
const importantAttrs = ['role', 'aria-label', 'aria-hidden', 'title', 'name', 'type', 'value', 'placeholder', 'href', 'src', 'data-*'];
for (const attr of el.attributes) {
if (importantAttrs.some(p => p === attr.name || (p.endsWith('*') && attr.name.startsWith(p.slice(0, -1))))) {
node.attributes[attr.name] = attr.value.slice(0, 200);
}
}
if (Object.keys(node.attributes).length === 0) delete node.attributes;
// Get computed styles if requested
if (includeStyles) {
node.styles = {
display: style.display,
visibility: style.visibility,
position: style.position,
color: style.color,
backgroundColor: style.backgroundColor,
fontSize: style.fontSize
};
}
// Get text content (only direct text, not from children)
const directText = Array.from(el.childNodes)
.filter(n => n.nodeType === Node.TEXT_NODE)
.map(n => n.textContent.trim())
.filter(t => t)
.join(' ');
if (directText) {
node.text = directText.slice(0, 200);
}
// Process children
if (el.children.length > 0 && currentDepth < maxDepth) {
for (const child of el.children) {
const childNode = domToJson(child, currentDepth + 1, maxDepth, includeStyles, includeHidden);
if (childNode) node.children.push(childNode);
}
}
if (node.children.length === 0) delete node.children;
return node;
}
return JSON.stringify(domToJson(arguments[0], 0, arguments[1], arguments[2], arguments[3]), null, 2);
`,
element,
depth,
includeStyles,
includeHidden
);
} else {
// Tree format - readable text representation
result = await webDriver.executeScript<string>(
`
function domToTree(el, currentDepth, maxDepth, indent, includeHidden) {
const style = window.getComputedStyle(el);
if (!includeHidden && (style.display === 'none' || style.visibility === 'hidden')) {
return '';
}
if (currentDepth > maxDepth) return indent + '... (truncated)\\n';
const tag = el.tagName.toLowerCase();
const id = el.id ? '#' + el.id : '';
const classes = el.className && typeof el.className === 'string'
? '.' + el.className.split(' ').filter(c => c).join('.')
: '';
// Get key attributes
const role = el.getAttribute('role');
const ariaLabel = el.getAttribute('aria-label');
const title = el.getAttribute('title');
const attrs = [];
if (role) attrs.push('role="' + role + '"');
if (ariaLabel) attrs.push('aria-label="' + ariaLabel.slice(0, 50) + '"');
if (title) attrs.push('title="' + title.slice(0, 50) + '"');
// Get direct text content
const directText = Array.from(el.childNodes)
.filter(n => n.nodeType === Node.TEXT_NODE)
.map(n => n.textContent.trim())
.filter(t => t)
.join(' ')
.slice(0, 50);
let line = indent + '<' + tag + id + classes;
if (attrs.length > 0) line += ' ' + attrs.join(' ');
if (directText) line += '> "' + directText + '"';
else if (el.children.length === 0) line += ' />';
else line += '>';
let result = line + '\\n';
if (el.children.length > 0 && currentDepth < maxDepth) {
for (const child of el.children) {
result += domToTree(child, currentDepth + 1, maxDepth, indent + ' ', includeHidden);
}
}
return result;
}
return domToTree(arguments[0], 0, arguments[1], '', arguments[2]);
`,
element,
depth,
includeHidden
);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: selector,
format: format,
depth: depth,
dom: format === 'json' ? JSON.parse(result) : result,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
selector: selector,
}, null, 2),
}],
};
}
}
/**
* Get the structure of a specific VSCode UI region
*
* This tool provides a focused view of specific VSCode UI regions,
* making it easier to understand and interact with different parts of the IDE.
*
* @example
* // Get the sidebar structure
* await getUiStructure({ region: 'sidebar' });
*
* @example
* // Get the editor area structure
* await getUiStructure({ region: 'editor', depth: 8 });
*/
export async function getUiStructure(input: {
region?: 'full' | 'sidebar' | 'editor' | 'panel' | 'activitybar' | 'statusbar' | 'titlebar' | 'menubar';
depth?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
// Map regions to CSS selectors
const regionSelectors: Record<string, string> = {
full: 'body',
sidebar: '.sidebar, .part.sidebar',
editor: '.editor-container, .part.editor',
panel: '.panel, .part.panel',
activitybar: '.activitybar, .part.activitybar',
statusbar: '.statusbar, .part.statusbar',
titlebar: '.titlebar, .part.titlebar',
menubar: '.menubar',
};
const region = input.region || 'full';
const depth = input.depth || 5;
const selector = regionSelectors[region] || 'body';
try {
const By = (await import('selenium-webdriver')).By;
// Try each selector variant (some may not exist depending on VSCode version)
const selectors = selector.split(', ');
let element = null;
let usedSelector = '';
for (const sel of selectors) {
try {
element = await webDriver.findElement(By.css(sel));
usedSelector = sel;
break;
} catch {
continue;
}
}
if (!element) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: `Could not find UI region: ${region}`,
triedSelectors: selectors,
}, null, 2),
}],
};
}
// Get a clean, focused structure of the region
const structure = await webDriver.executeScript<string>(
`
function getUIStructure(el, currentDepth, maxDepth) {
if (currentDepth > maxDepth) return { truncated: true };
const style = window.getComputedStyle(el);
if (style.display === 'none') return null;
const rect = el.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) return null;
const node = {
element: el.tagName.toLowerCase(),
id: el.id || undefined,
classes: el.className && typeof el.className === 'string'
? el.className.split(' ').filter(c => c && !c.startsWith('monaco-'))
: undefined,
role: el.getAttribute('role') || undefined,
label: el.getAttribute('aria-label') || el.getAttribute('title') || undefined,
text: undefined,
interactive: ['BUTTON', 'INPUT', 'A', 'SELECT', 'TEXTAREA'].includes(el.tagName)
|| el.getAttribute('role') === 'button'
|| el.getAttribute('tabindex') !== null,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
children: []
};
// Clean up undefined/empty fields
if (!node.id) delete node.id;
if (!node.classes || node.classes.length === 0) delete node.classes;
if (!node.role) delete node.role;
if (!node.label) delete node.label;
if (!node.interactive) delete node.interactive;
// Get direct text
const directText = Array.from(el.childNodes)
.filter(n => n.nodeType === Node.TEXT_NODE)
.map(n => n.textContent.trim())
.filter(t => t)
.join(' ');
if (directText) node.text = directText.slice(0, 100);
// Process children
if (el.children.length > 0 && currentDepth < maxDepth) {
for (const child of el.children) {
const childNode = getUIStructure(child, currentDepth + 1, maxDepth);
if (childNode) node.children.push(childNode);
}
}
if (node.children.length === 0) delete node.children;
return node;
}
return JSON.stringify(getUIStructure(arguments[0], 0, arguments[1]), null, 2);
`,
element,
depth
);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
region: region,
selector: usedSelector,
depth: depth,
structure: JSON.parse(structure),
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
region: region,
}, null, 2),
}],
};
}
}
/**
* Query multiple elements matching a selector
*
* This tool finds all elements matching a CSS selector and returns
* information about each one. Useful for finding interactive elements
* or understanding repeated UI patterns.
*
* @example
* // Find all buttons
* await queryElements({ selector: 'button' });
*
* @example
* // Find all elements with a specific role
* await queryElements({ selector: '[role="tab"]', limit: 10 });
*/
export async function queryElements(input: {
selector: string;
limit?: number;
attributes?: string[];
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const limit = input.limit || 20;
const requestedAttrs = input.attributes || ['id', 'class', 'role', 'aria-label', 'title', 'name', 'type'];
try {
const elements = await webDriver.executeScript<string>(
`
const elements = Array.from(document.querySelectorAll(arguments[0])).slice(0, arguments[1]);
const attrs = arguments[2];
return JSON.stringify(elements.map((el, index) => {
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
const result = {
index: index,
tag: el.tagName.toLowerCase(),
visible: style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
attributes: {},
text: el.textContent ? el.textContent.trim().slice(0, 100) : undefined
};
for (const attr of attrs) {
const value = el.getAttribute(attr);
if (value) result.attributes[attr] = value.slice(0, 200);
}
// Generate a unique selector for this element
let selector = el.tagName.toLowerCase();
if (el.id) selector += '#' + el.id;
else if (el.className && typeof el.className === 'string') {
const classes = el.className.split(' ').filter(c => c && !c.startsWith('monaco-')).slice(0, 2);
if (classes.length) selector += '.' + classes.join('.');
}
result.selector = selector;
return result;
}));
`,
input.selector,
limit,
requestedAttrs
);
const parsed = JSON.parse(elements);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
query: input.selector,
count: parsed.length,
limit: limit,
elements: parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
query: input.selector,
}, null, 2),
}],
};
}
}
/**
* Get a complete accessibility tree of the VSCode window
*
* This provides an accessibility-focused view of the UI,
* which is often cleaner and easier to work with than raw DOM.
*/
export async function getAccessibilityTree(input: {
selector?: string;
depth?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const selector = input.selector || 'body';
const depth = input.depth || 8;
try {
const By = (await import('selenium-webdriver')).By;
const element = await webDriver.findElement(By.css(selector));
const tree = await webDriver.executeScript<string>(
`
function getA11yTree(el, currentDepth, maxDepth) {
if (currentDepth > maxDepth) return null;
const style = window.getComputedStyle(el);
if (style.display === 'none' || el.getAttribute('aria-hidden') === 'true') return null;
const role = el.getAttribute('role') || getImplicitRole(el);
const label = el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('alt');
const name = getAccessibleName(el);
// Skip elements that don't contribute to accessibility
if (!role && !label && !name && el.children.length === 0) return null;
const node = {
role: role || 'generic',
name: name || undefined,
label: label || undefined,
focused: document.activeElement === el,
disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',
expanded: el.getAttribute('aria-expanded'),
selected: el.getAttribute('aria-selected'),
checked: el.checked !== undefined ? el.checked : el.getAttribute('aria-checked'),
children: []
};
// Clean up
if (!node.name) delete node.name;
if (!node.label) delete node.label;
if (!node.focused) delete node.focused;
if (!node.disabled) delete node.disabled;
if (node.expanded === null) delete node.expanded;
if (node.selected === null) delete node.selected;
if (node.checked === null || node.checked === undefined) delete node.checked;
// Get children
if (el.children.length > 0 && currentDepth < maxDepth) {
for (const child of el.children) {
const childNode = getA11yTree(child, currentDepth + 1, maxDepth);
if (childNode) node.children.push(childNode);
}
}
if (node.children.length === 0) delete node.children;
return node;
}
function getImplicitRole(el) {
const tagRoles = {
'BUTTON': 'button',
'A': 'link',
'INPUT': el.type === 'checkbox' ? 'checkbox' : el.type === 'radio' ? 'radio' : 'textbox',
'SELECT': 'combobox',
'TEXTAREA': 'textbox',
'IMG': 'img',
'NAV': 'navigation',
'MAIN': 'main',
'HEADER': 'banner',
'FOOTER': 'contentinfo',
'ASIDE': 'complementary',
'ARTICLE': 'article',
'SECTION': 'region',
'UL': 'list',
'OL': 'list',
'LI': 'listitem',
'TABLE': 'table',
'TR': 'row',
'TH': 'columnheader',
'TD': 'cell'
};
return tagRoles[el.tagName];
}
function getAccessibleName(el) {
// Check aria-labelledby
const labelledBy = el.getAttribute('aria-labelledby');
if (labelledBy) {
const labels = labelledBy.split(' ').map(id => document.getElementById(id)?.textContent).filter(Boolean);
if (labels.length) return labels.join(' ').slice(0, 100);
}
// Check aria-label
const ariaLabel = el.getAttribute('aria-label');
if (ariaLabel) return ariaLabel.slice(0, 100);
// Check for label element
if (el.id) {
const label = document.querySelector('label[for="' + el.id + '"]');
if (label) return label.textContent.trim().slice(0, 100);
}
// Check title
const title = el.getAttribute('title');
if (title) return title.slice(0, 100);
// For buttons/links, use text content
if (['BUTTON', 'A'].includes(el.tagName)) {
return el.textContent.trim().slice(0, 100);
}
return null;
}
return JSON.stringify(getA11yTree(arguments[0], 0, arguments[1]), null, 2);
`,
element,
depth
);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: selector,
depth: depth,
accessibilityTree: JSON.parse(tree),
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
selector: selector,
}, null, 2),
}],
};
}
}
/**
* Get the children of a specific element
*
* This tool returns the direct children or descendants of an element,
* allowing incremental exploration of the DOM tree.
*
* @example
* // Get direct children of the sidebar
* await getElementChildren({ selector: '.sidebar' });
*
* @example
* // Get all descendants up to 3 levels deep
* await getElementChildren({ selector: '.sidebar', directOnly: false, depth: 3 });
*/
export async function getElementChildren(input: {
selector: string;
directOnly?: boolean;
depth?: number;
includeText?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const directOnly = input.directOnly !== false;
const depth = input.depth || 1;
const includeText = input.includeText !== false;
try {
const By = (await import('selenium-webdriver')).By;
const element = await webDriver.findElement(By.css(input.selector));
const children = await webDriver.executeScript<string>(
`
function getChildren(el, currentDepth, maxDepth, directOnly, includeText) {
const results = [];
const childElements = directOnly ? el.children : el.querySelectorAll('*');
for (let i = 0; i < childElements.length; i++) {
const child = directOnly ? childElements[i] : childElements[i];
if (!directOnly && child.parentElement !== el && currentDepth >= maxDepth) continue;
const rect = child.getBoundingClientRect();
const style = window.getComputedStyle(child);
const node = {
index: i,
tag: child.tagName.toLowerCase(),
id: child.id || undefined,
classes: child.className && typeof child.className === 'string'
? child.className.split(' ').filter(c => c).slice(0, 5)
: undefined,
role: child.getAttribute('role') || undefined,
label: child.getAttribute('aria-label') || child.getAttribute('title') || undefined,
visible: style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
childCount: child.children.length,
selector: generateSelector(child)
};
if (includeText) {
const directText = Array.from(child.childNodes)
.filter(n => n.nodeType === Node.TEXT_NODE)
.map(n => n.textContent.trim())
.filter(t => t)
.join(' ');
if (directText) node.text = directText.slice(0, 100);
}
// Clean up undefined fields
Object.keys(node).forEach(k => node[k] === undefined && delete node[k]);
if (node.classes && node.classes.length === 0) delete node.classes;
results.push(node);
}
return results;
}
function generateSelector(el) {
if (el.id) return '#' + el.id;
let selector = el.tagName.toLowerCase();
if (el.className && typeof el.className === 'string') {
const classes = el.className.split(' ').filter(c => c && !c.startsWith('monaco-')).slice(0, 2);
if (classes.length) selector += '.' + classes.join('.');
}
return selector;
}
return JSON.stringify(getChildren(arguments[0], 0, arguments[1], arguments[2], arguments[3]));
`,
element,
depth,
directOnly,
includeText
);
const parsed = JSON.parse(children);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: input.selector,
directOnly: directOnly,
childCount: parsed.length,
children: parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
selector: input.selector,
}, null, 2),
}],
};
}
}
/**
* Get the parent chain of an element
*
* This tool returns the ancestors of an element up to the body,
* useful for understanding element context and building selectors.
*
* @example
* // Get 5 levels of parents
* await getElementParents({ selector: '.my-button' });
*/
export async function getElementParents(input: {
selector: string;
levels?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const levels = input.levels || 5;
try {
const By = (await import('selenium-webdriver')).By;
const element = await webDriver.findElement(By.css(input.selector));
const parents = await webDriver.executeScript<string>(
`
function getParents(el, maxLevels) {
const results = [];
let current = el.parentElement;
let level = 0;
while (current && current.tagName !== 'HTML' && level < maxLevels) {
const rect = current.getBoundingClientRect();
const node = {
level: level,
tag: current.tagName.toLowerCase(),
id: current.id || undefined,
classes: current.className && typeof current.className === 'string'
? current.className.split(' ').filter(c => c).slice(0, 5)
: undefined,
role: current.getAttribute('role') || undefined,
label: current.getAttribute('aria-label') || undefined,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
childCount: current.children.length,
selector: generateSelector(current)
};
Object.keys(node).forEach(k => node[k] === undefined && delete node[k]);
if (node.classes && node.classes.length === 0) delete node.classes;
results.push(node);
current = current.parentElement;
level++;
}
return results;
}
function generateSelector(el) {
if (el.id) return '#' + el.id;
let selector = el.tagName.toLowerCase();
if (el.className && typeof el.className === 'string') {
const classes = el.className.split(' ').filter(c => c && !c.startsWith('monaco-')).slice(0, 2);
if (classes.length) selector += '.' + classes.join('.');
}
return selector;
}
return JSON.stringify(getParents(arguments[0], arguments[1]));
`,
element,
levels
);
const parsed = JSON.parse(parents);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: input.selector,
levels: parsed.length,
parents: parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
selector: input.selector,
}, null, 2),
}],
};
}
}
/**
* Get the siblings of an element
*
* This tool returns elements at the same level (same parent) as the target element.
*
* @example
* // Get all siblings
* await getElementSiblings({ selector: '.tab-item' });
*
* @example
* // Get only next siblings
* await getElementSiblings({ selector: '.tab-item', direction: 'next' });
*/
export async function getElementSiblings(input: {
selector: string;
direction?: 'all' | 'previous' | 'next';
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const direction = input.direction || 'all';
try {
const By = (await import('selenium-webdriver')).By;
const element = await webDriver.findElement(By.css(input.selector));
const siblings = await webDriver.executeScript<string>(
`
function getSiblings(el, direction) {
const parent = el.parentElement;
if (!parent) return [];
const allChildren = Array.from(parent.children);
const currentIndex = allChildren.indexOf(el);
let siblings = [];
if (direction === 'all') {
siblings = allChildren.filter((_, i) => i !== currentIndex);
} else if (direction === 'previous') {
siblings = allChildren.slice(0, currentIndex);
} else if (direction === 'next') {
siblings = allChildren.slice(currentIndex + 1);
}
return siblings.map((sib, i) => {
const rect = sib.getBoundingClientRect();
const style = window.getComputedStyle(sib);
const node = {
index: direction === 'previous' ? i : (direction === 'next' ? currentIndex + 1 + i : (i < currentIndex ? i : i + 1)),
tag: sib.tagName.toLowerCase(),
id: sib.id || undefined,
classes: sib.className && typeof sib.className === 'string'
? sib.className.split(' ').filter(c => c).slice(0, 5)
: undefined,
role: sib.getAttribute('role') || undefined,
label: sib.getAttribute('aria-label') || sib.getAttribute('title') || undefined,
text: sib.textContent ? sib.textContent.trim().slice(0, 50) : undefined,
visible: style.display !== 'none' && rect.width > 0,
selector: generateSelector(sib)
};
Object.keys(node).forEach(k => node[k] === undefined && delete node[k]);
if (node.classes && node.classes.length === 0) delete node.classes;
return node;
});
}
function generateSelector(el) {
if (el.id) return '#' + el.id;
let selector = el.tagName.toLowerCase();
if (el.className && typeof el.className === 'string') {
const classes = el.className.split(' ').filter(c => c && !c.startsWith('monaco-')).slice(0, 2);
if (classes.length) selector += '.' + classes.join('.');
}
return selector;
}
return JSON.stringify(getSiblings(arguments[0], arguments[1]));
`,
element,
direction
);
const parsed = JSON.parse(siblings);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: input.selector,
direction: direction,
siblingCount: parsed.length,
siblings: parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
selector: input.selector,
}, null, 2),
}],
};
}
}
/**
* Find all interactive elements within a container
*
* This tool finds clickable/focusable elements like buttons, inputs, links, etc.
* Very useful for discovering what actions can be performed in a UI area.
*
* @example
* // Find all interactive elements in the sidebar
* await findInteractiveElements({ selector: '.sidebar' });
*
* @example
* // Find only buttons and inputs
* await findInteractiveElements({ types: ['button', 'input'] });
*/
export async function findInteractiveElements(input: {
selector?: string;
types?: Array<'button' | 'input' | 'link' | 'select' | 'checkbox' | 'radio' | 'tab' | 'menu' | 'treeitem'>;
visibleOnly?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const selector = input.selector || 'body';
const types = input.types || ['button', 'input', 'link', 'select', 'checkbox', 'radio', 'tab', 'menu', 'treeitem'];
const visibleOnly = input.visibleOnly !== false;
try {
const By = (await import('selenium-webdriver')).By;
const container = await webDriver.findElement(By.css(selector));
const elements = await webDriver.executeScript<string>(
`
function findInteractive(container, types, visibleOnly) {
const typeSelectors = {
button: 'button, [role="button"], input[type="button"], input[type="submit"]',
input: 'input:not([type="button"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"]), textarea, [contenteditable="true"]',
link: 'a[href], [role="link"]',
select: 'select, [role="combobox"], [role="listbox"]',
checkbox: 'input[type="checkbox"], [role="checkbox"]',
radio: 'input[type="radio"], [role="radio"]',
tab: '[role="tab"]',
menu: '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]',
treeitem: '[role="treeitem"]'
};
const selectors = types.map(t => typeSelectors[t]).filter(Boolean).join(', ');
const elements = container.querySelectorAll(selectors);
const results = [];
elements.forEach((el, i) => {
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
const isVisible = style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
if (visibleOnly && !isVisible) return;
const node = {
index: results.length,
type: getInteractiveType(el, types),
tag: el.tagName.toLowerCase(),
id: el.id || undefined,
name: el.getAttribute('name') || undefined,
role: el.getAttribute('role') || undefined,
label: el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('placeholder') || undefined,
text: el.textContent ? el.textContent.trim().slice(0, 50) : undefined,
value: el.value ? el.value.slice(0, 50) : undefined,
disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',
visible: isVisible,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
selector: generateUniqueSelector(el)
};
Object.keys(node).forEach(k => (node[k] === undefined || node[k] === false) && delete node[k]);
results.push(node);
});
return results;
}
function getInteractiveType(el, types) {
const tag = el.tagName.toLowerCase();
const role = el.getAttribute('role');
const type = el.getAttribute('type');
if (types.includes('button') && (tag === 'button' || role === 'button' || type === 'button' || type === 'submit')) return 'button';
if (types.includes('link') && (tag === 'a' || role === 'link')) return 'link';
if (types.includes('checkbox') && (type === 'checkbox' || role === 'checkbox')) return 'checkbox';
if (types.includes('radio') && (type === 'radio' || role === 'radio')) return 'radio';
if (types.includes('select') && (tag === 'select' || role === 'combobox' || role === 'listbox')) return 'select';
if (types.includes('tab') && role === 'tab') return 'tab';
if (types.includes('menu') && role?.startsWith('menuitem')) return 'menu';
if (types.includes('treeitem') && role === 'treeitem') return 'treeitem';
if (types.includes('input') && (tag === 'input' || tag === 'textarea' || el.getAttribute('contenteditable'))) return 'input';
return 'interactive';
}
function generateUniqueSelector(el) {
if (el.id) return '#' + el.id;
let selector = el.tagName.toLowerCase();
// Add role if present
const role = el.getAttribute('role');
if (role) selector += '[role="' + role + '"]';
// Add aria-label if present
const label = el.getAttribute('aria-label');
if (label) selector += '[aria-label="' + label.slice(0, 30) + '"]';
// Add name if present
const name = el.getAttribute('name');
if (name) selector += '[name="' + name + '"]';
// Add useful classes
if (el.className && typeof el.className === 'string') {
const classes = el.className.split(' ').filter(c => c && !c.startsWith('monaco-')).slice(0, 2);
if (classes.length) selector += '.' + classes.join('.');
}
return selector;
}
return JSON.stringify(findInteractive(arguments[0], arguments[1], arguments[2]));
`,
container,
types,
visibleOnly
);
const parsed = JSON.parse(elements);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
container: selector,
types: types,
visibleOnly: visibleOnly,
count: parsed.length,
elements: parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
container: selector,
}, null, 2),
}],
};
}
}
/**
* Dump the full DOM to a file for later exploration
*
* This tool saves the complete DOM structure to a file, allowing
* the AI to read and search through it incrementally without
* overwhelming the context window.
*
* @example
* // Save full DOM to a JSON file
* await dumpDomToFile({ filePath: '/tmp/vscode-dom.json' });
*
* @example
* // Save just the editor as HTML
* await dumpDomToFile({
* filePath: '/tmp/editor.html',
* selector: '.editor-container',
* format: 'html'
* });
*/
export async function dumpDomToFile(input: {
filePath: string;
selector?: string;
format?: 'tree' | 'html' | 'json';
depth?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const selector = input.selector || 'body';
const format = input.format || 'json';
const depth = input.depth || 20;
try {
const By = (await import('selenium-webdriver')).By;
const fs = await import('fs/promises');
const path = await import('path');
const element = await webDriver.findElement(By.css(selector));
let content: string;
if (format === 'html') {
content = await webDriver.executeScript<string>(
`return arguments[0].outerHTML;`,
element
);
} else if (format === 'json') {
content = await webDriver.executeScript<string>(
`
function domToJson(el, currentDepth, maxDepth) {
if (currentDepth > maxDepth) return { truncated: true };
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
if (style.display === 'none') return null;
const node = {
tag: el.tagName.toLowerCase(),
id: el.id || undefined,
classes: el.className && typeof el.className === 'string'
? el.className.split(' ').filter(c => c)
: undefined,
role: el.getAttribute('role') || undefined,
ariaLabel: el.getAttribute('aria-label') || undefined,
title: el.getAttribute('title') || undefined,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
interactive: isInteractive(el),
text: undefined,
children: []
};
// Get direct text
const directText = Array.from(el.childNodes)
.filter(n => n.nodeType === Node.TEXT_NODE)
.map(n => n.textContent.trim())
.filter(t => t)
.join(' ');
if (directText) node.text = directText.slice(0, 200);
// Process children
if (el.children.length > 0 && currentDepth < maxDepth) {
for (const child of el.children) {
const childNode = domToJson(child, currentDepth + 1, maxDepth);
if (childNode) node.children.push(childNode);
}
}
// Clean up
Object.keys(node).forEach(k => node[k] === undefined && delete node[k]);
if (node.classes && node.classes.length === 0) delete node.classes;
if (node.children && node.children.length === 0) delete node.children;
if (!node.interactive) delete node.interactive;
return node;
}
function isInteractive(el) {
const tag = el.tagName.toLowerCase();
const role = el.getAttribute('role');
return ['button', 'a', 'input', 'select', 'textarea'].includes(tag) ||
['button', 'link', 'tab', 'menuitem', 'checkbox', 'radio', 'textbox', 'combobox'].includes(role) ||
el.getAttribute('tabindex') !== null;
}
return JSON.stringify(domToJson(arguments[0], 0, arguments[1]), null, 2);
`,
element,
depth
);
} else {
// Tree format
content = await webDriver.executeScript<string>(
`
function domToTree(el, currentDepth, maxDepth, indent) {
const style = window.getComputedStyle(el);
if (style.display === 'none') return '';
if (currentDepth > maxDepth) return indent + '... (truncated)\\n';
const tag = el.tagName.toLowerCase();
const id = el.id ? '#' + el.id : '';
const classes = el.className && typeof el.className === 'string'
? '.' + el.className.split(' ').filter(c => c).join('.')
: '';
const role = el.getAttribute('role');
const ariaLabel = el.getAttribute('aria-label');
const attrs = [];
if (role) attrs.push('role="' + role + '"');
if (ariaLabel) attrs.push('aria-label="' + ariaLabel.slice(0, 50) + '"');
const directText = Array.from(el.childNodes)
.filter(n => n.nodeType === Node.TEXT_NODE)
.map(n => n.textContent.trim())
.filter(t => t)
.join(' ')
.slice(0, 50);
let line = indent + '<' + tag + id + classes;
if (attrs.length > 0) line += ' ' + attrs.join(' ');
if (directText) line += '> "' + directText + '"';
else if (el.children.length === 0) line += ' />';
else line += '>';
let result = line + '\\n';
if (el.children.length > 0 && currentDepth < maxDepth) {
for (const child of el.children) {
result += domToTree(child, currentDepth + 1, maxDepth, indent + ' ');
}
}
return result;
}
return domToTree(arguments[0], 0, arguments[1], '');
`,
element,
depth
);
}
// Ensure directory exists
const dir = path.dirname(input.filePath);
await fs.mkdir(dir, { recursive: true });
// Write the file
await fs.writeFile(input.filePath, content, 'utf-8');
const stats = await fs.stat(input.filePath);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
filePath: input.filePath,
selector: selector,
format: format,
depth: depth,
fileSize: stats.size,
fileSizeHuman: stats.size > 1024 * 1024
? (stats.size / 1024 / 1024).toFixed(2) + ' MB'
: (stats.size / 1024).toFixed(2) + ' KB',
message: `DOM dumped to ${input.filePath}. You can now read sections of this file to explore the DOM.`,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
filePath: input.filePath,
}, null, 2),
}],
};
}
}
/**
* Search the DOM for elements matching a text query
*
* This tool searches through element text, IDs, classes, ARIA labels,
* and other attributes to find matching elements.
*
* @example
* // Search for elements with "Save" in their text
* await searchDom({ query: 'Save' });
*
* @example
* // Search in aria-labels and titles only
* await searchDom({ query: 'Explorer', searchIn: ['aria-label', 'title'] });
*/
export async function searchDom(input: {
query: string;
searchIn?: Array<'text' | 'id' | 'class' | 'aria-label' | 'title' | 'role' | 'placeholder'>;
limit?: number;
caseSensitive?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const searchIn = input.searchIn || ['text', 'aria-label', 'title'];
const limit = input.limit || 20;
const caseSensitive = input.caseSensitive || false;
try {
const elements = await webDriver.executeScript<string>(
`
function searchDom(query, searchIn, limit, caseSensitive) {
const results = [];
const searchQuery = caseSensitive ? query : query.toLowerCase();
function matches(text) {
if (!text) return false;
const compareText = caseSensitive ? text : text.toLowerCase();
return compareText.includes(searchQuery);
}
function checkElement(el) {
if (results.length >= limit) return;
const style = window.getComputedStyle(el);
if (style.display === 'none') return;
let matched = false;
const matchedIn = [];
if (searchIn.includes('text')) {
const directText = Array.from(el.childNodes)
.filter(n => n.nodeType === Node.TEXT_NODE)
.map(n => n.textContent)
.join(' ');
if (matches(directText)) {
matched = true;
matchedIn.push('text');
}
}
if (searchIn.includes('id') && matches(el.id)) {
matched = true;
matchedIn.push('id');
}
if (searchIn.includes('class') && el.className && typeof el.className === 'string' && matches(el.className)) {
matched = true;
matchedIn.push('class');
}
if (searchIn.includes('aria-label') && matches(el.getAttribute('aria-label'))) {
matched = true;
matchedIn.push('aria-label');
}
if (searchIn.includes('title') && matches(el.getAttribute('title'))) {
matched = true;
matchedIn.push('title');
}
if (searchIn.includes('role') && matches(el.getAttribute('role'))) {
matched = true;
matchedIn.push('role');
}
if (searchIn.includes('placeholder') && matches(el.getAttribute('placeholder'))) {
matched = true;
matchedIn.push('placeholder');
}
if (matched) {
const rect = el.getBoundingClientRect();
results.push({
index: results.length,
matchedIn: matchedIn,
tag: el.tagName.toLowerCase(),
id: el.id || undefined,
classes: el.className && typeof el.className === 'string'
? el.className.split(' ').filter(c => c).slice(0, 5)
: undefined,
role: el.getAttribute('role') || undefined,
ariaLabel: el.getAttribute('aria-label') || undefined,
title: el.getAttribute('title') || undefined,
text: el.textContent ? el.textContent.trim().slice(0, 100) : undefined,
visible: rect.width > 0 && rect.height > 0,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
selector: generateSelector(el)
});
}
// Search children
for (const child of el.children) {
checkElement(child);
if (results.length >= limit) break;
}
}
function generateSelector(el) {
if (el.id) return '#' + el.id;
let selector = el.tagName.toLowerCase();
const ariaLabel = el.getAttribute('aria-label');
if (ariaLabel) selector += '[aria-label="' + ariaLabel.slice(0, 30) + '"]';
else if (el.className && typeof el.className === 'string') {
const classes = el.className.split(' ').filter(c => c && !c.startsWith('monaco-')).slice(0, 2);
if (classes.length) selector += '.' + classes.join('.');
}
return selector;
}
checkElement(document.body);
// Clean up undefined fields
results.forEach(r => {
Object.keys(r).forEach(k => r[k] === undefined && delete r[k]);
if (r.classes && r.classes.length === 0) delete r.classes;
});
return results;
}
return JSON.stringify(searchDom(arguments[0], arguments[1], arguments[2], arguments[3]));
`,
input.query,
searchIn,
limit,
caseSensitive
);
const parsed = JSON.parse(elements);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
query: input.query,
searchIn: searchIn,
caseSensitive: caseSensitive,
count: parsed.length,
limit: limit,
results: parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
query: input.query,
}, null, 2),
}],
};
}
}
/**
* Execute arbitrary JavaScript in the VSCode window context
*
* This tool allows running any JavaScript code in the VSCode window,
* just like using the DevTools console. The script has full access to
* the DOM, window object, and any globals.
*
* WARNING: This is a powerful tool. Use responsibly.
*
* @example
* // Get the document title
* await executeScript({ script: "return document.title" });
*
* @example
* // Get all Monaco editor instances
* await executeScript({
* script: "return window.monaco?.editor?.getModels()?.map(m => m.uri.toString())"
* });
*
* @example
* // Click a button programmatically
* await executeScript({
* script: "document.querySelector('.my-button')?.click(); return 'clicked'",
* returnType: "string"
* });
*/
export async function executeScript(input: {
script: string;
returnType?: 'json' | 'string' | 'none';
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const returnType = input.returnType || 'json';
try {
// Wrap the script to handle different return types
let wrappedScript: string;
if (returnType === 'none') {
wrappedScript = `
(function() {
${input.script}
return { executed: true };
})()
`;
} else if (returnType === 'string') {
wrappedScript = `
(function() {
const result = (function() { ${input.script} })();
return { result: String(result) };
})()
`;
} else {
// JSON - attempt to serialize
wrappedScript = `
(function() {
try {
const result = (function() { ${input.script} })();
// Handle various types
if (result === undefined) return { result: null, type: 'undefined' };
if (result === null) return { result: null, type: 'null' };
if (typeof result === 'function') return { result: result.toString(), type: 'function' };
if (result instanceof HTMLElement) {
return {
result: {
tagName: result.tagName.toLowerCase(),
id: result.id || undefined,
className: result.className || undefined,
textContent: result.textContent?.slice(0, 500),
innerHTML: result.innerHTML?.slice(0, 1000),
outerHTML: result.outerHTML?.slice(0, 1000)
},
type: 'element'
};
}
if (result instanceof NodeList || result instanceof HTMLCollection) {
return {
result: Array.from(result).slice(0, 100).map(el => ({
tagName: el.tagName?.toLowerCase(),
id: el.id || undefined,
className: el.className || undefined,
textContent: el.textContent?.slice(0, 100)
})),
type: 'nodelist',
length: result.length
};
}
if (Array.isArray(result)) {
return { result: result.slice(0, 100), type: 'array', length: result.length };
}
return { result: result, type: typeof result };
} catch (e) {
return { error: e.message, type: 'error' };
}
})()
`;
}
const result = await webDriver.executeScript<unknown>(wrappedScript);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
returnType: returnType,
...(result as object),
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
script: input.script.slice(0, 200) + (input.script.length > 200 ? '...' : ''),
}, null, 2),
}],
};
}
}
/**
* Simple querySelector / querySelectorAll wrapper
*
* This tool provides a direct interface to document.querySelector()
* and document.querySelectorAll() - the most common DOM query methods.
*
* @example
* // Find a single element
* await querySelector({ selector: ".my-button" });
*
* @example
* // Find all matching elements
* await querySelector({ selector: "button", all: true });
*
* @example
* // Extract specific properties
* await querySelector({
* selector: "input",
* all: true,
* properties: ["value", "placeholder", "type"]
* });
*/
export async function querySelector(input: {
selector: string;
all?: boolean;
properties?: string[];
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const all = input.all || false;
const defaultProps = ['tagName', 'id', 'className', 'textContent', 'value', 'href', 'src', 'type', 'name', 'placeholder'];
const properties = input.properties || defaultProps;
try {
const result = await webDriver.executeScript<string>(
`
function extractProps(el, props) {
if (!el) return null;
const rect = el.getBoundingClientRect();
const obj = {
exists: true,
tag: el.tagName.toLowerCase(),
visible: rect.width > 0 && rect.height > 0,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) }
};
for (const prop of props) {
let value;
// Handle special cases
if (prop === 'textContent') {
value = el.textContent?.trim()?.slice(0, 200);
} else if (prop === 'innerHTML') {
value = el.innerHTML?.slice(0, 500);
} else if (prop === 'outerHTML') {
value = el.outerHTML?.slice(0, 500);
} else if (prop === 'className') {
value = el.className && typeof el.className === 'string' ? el.className : undefined;
} else if (prop === 'tagName') {
value = el.tagName?.toLowerCase();
} else if (prop.startsWith('data-')) {
value = el.getAttribute(prop);
} else if (prop.startsWith('aria-')) {
value = el.getAttribute(prop);
} else {
// Try as property first, then as attribute
value = el[prop] !== undefined ? el[prop] : el.getAttribute(prop);
}
if (value !== null && value !== undefined && value !== '') {
obj[prop] = typeof value === 'string' ? value.slice(0, 200) : value;
}
}
return obj;
}
const selector = arguments[0];
const all = arguments[1];
const props = arguments[2];
if (all) {
const elements = document.querySelectorAll(selector);
return JSON.stringify({
count: elements.length,
elements: Array.from(elements).slice(0, 100).map((el, i) => ({
index: i,
...extractProps(el, props)
}))
});
} else {
const element = document.querySelector(selector);
if (!element) {
return JSON.stringify({ exists: false, element: null });
}
return JSON.stringify({ exists: true, element: extractProps(element, props) });
}
`,
input.selector,
all,
properties
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: input.selector,
all: all,
...parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
selector: input.selector,
}, null, 2),
}],
};
}
}
/**
* Get element by ID (document.getElementById wrapper)
*
* @example
* await getElementById({ id: "workbench.parts.editor" });
*/
export async function getElementById(input: {
id: string;
properties?: string[];
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const defaultProps = ['tagName', 'className', 'textContent', 'value', 'innerHTML'];
const properties = input.properties || defaultProps;
try {
const result = await webDriver.executeScript<string>(
`
const el = document.getElementById(arguments[0]);
if (!el) return JSON.stringify({ exists: false, element: null });
const props = arguments[1];
const rect = el.getBoundingClientRect();
const obj = {
exists: true,
id: el.id,
tag: el.tagName.toLowerCase(),
visible: rect.width > 0 && rect.height > 0,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
childCount: el.children.length
};
for (const prop of props) {
let value = el[prop] !== undefined ? el[prop] : el.getAttribute(prop);
if (value !== null && value !== undefined && value !== '') {
if (prop === 'textContent' || prop === 'innerHTML') {
obj[prop] = value.slice(0, 500);
} else if (prop === 'className') {
obj[prop] = typeof value === 'string' ? value : undefined;
} else {
obj[prop] = typeof value === 'string' ? value.slice(0, 200) : value;
}
}
}
return JSON.stringify({ exists: true, element: obj });
`,
input.id,
properties
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
id: input.id,
...parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
id: input.id,
}, null, 2),
}],
};
}
}
/**
* Get elements by class name (document.getElementsByClassName wrapper)
*
* @example
* await getElementsByClass({ className: "editor-container" });
*/
export async function getElementsByClass(input: {
className: string;
limit?: number;
properties?: string[];
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const limit = input.limit || 50;
const defaultProps = ['tagName', 'id', 'textContent'];
const properties = input.properties || defaultProps;
try {
const result = await webDriver.executeScript<string>(
`
const elements = document.getElementsByClassName(arguments[0]);
const limit = arguments[1];
const props = arguments[2];
const results = [];
for (let i = 0; i < Math.min(elements.length, limit); i++) {
const el = elements[i];
const rect = el.getBoundingClientRect();
const obj = {
index: i,
tag: el.tagName.toLowerCase(),
id: el.id || undefined,
visible: rect.width > 0 && rect.height > 0,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) }
};
for (const prop of props) {
let value = el[prop] !== undefined ? el[prop] : el.getAttribute(prop);
if (value !== null && value !== undefined && value !== '') {
if (prop === 'textContent') {
obj[prop] = value.trim().slice(0, 100);
} else if (prop === 'className') {
continue; // Skip, we already know the class
} else {
obj[prop] = typeof value === 'string' ? value.slice(0, 100) : value;
}
}
}
// Clean undefined
Object.keys(obj).forEach(k => obj[k] === undefined && delete obj[k]);
results.push(obj);
}
return JSON.stringify({ total: elements.length, returned: results.length, elements: results });
`,
input.className,
limit,
properties
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
className: input.className,
...parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
className: input.className,
}, null, 2),
}],
};
}
}
/**
* Get elements by tag name (document.getElementsByTagName wrapper)
*
* @example
* await getElementsByTag({ tagName: "button" });
*
* @example
* await getElementsByTag({ tagName: "input", properties: ["type", "value", "placeholder"] });
*/
export async function getElementsByTag(input: {
tagName: string;
limit?: number;
properties?: string[];
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const limit = input.limit || 50;
const defaultProps = ['id', 'className', 'textContent', 'type', 'value', 'name'];
const properties = input.properties || defaultProps;
try {
const result = await webDriver.executeScript<string>(
`
const elements = document.getElementsByTagName(arguments[0]);
const limit = arguments[1];
const props = arguments[2];
const results = [];
for (let i = 0; i < Math.min(elements.length, limit); i++) {
const el = elements[i];
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
const isVisible = style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
const obj = {
index: i,
tag: el.tagName.toLowerCase(),
visible: isVisible,
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) }
};
for (const prop of props) {
let value = el[prop] !== undefined ? el[prop] : el.getAttribute(prop);
if (value !== null && value !== undefined && value !== '') {
if (prop === 'textContent') {
const text = value.trim().slice(0, 100);
if (text) obj[prop] = text;
} else if (prop === 'className' && typeof value === 'string') {
const classes = value.split(' ').filter(c => c).slice(0, 5);
if (classes.length) obj.classes = classes;
} else if (prop === 'id' && value) {
obj[prop] = value;
} else if (typeof value === 'string') {
obj[prop] = value.slice(0, 100);
} else {
obj[prop] = value;
}
}
}
results.push(obj);
}
return JSON.stringify({ total: elements.length, returned: results.length, elements: results });
`,
input.tagName,
limit,
properties
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
tagName: input.tagName,
...parsed,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
tagName: input.tagName,
}, null, 2),
}],
};
}
}