/**
* VSCode Automation MCP Server - Wait/Sync Tools
*
* Tools for waiting on conditions, elements, and VSCode state.
*
* @author Sukarth Acharya
* @license MIT
*/
import { z } from 'zod';
import { getVSCodeDriver } from '../vscode-driver.js';
/**
* Input schema for vscode_wait_for_element tool
*/
export const waitForElementInputSchema = {
selector: z.string().describe('CSS selector of the element to wait for'),
state: z.enum(['present', 'visible', 'hidden', 'removed', 'enabled', 'disabled'])
.optional().default('visible').describe('State to wait for'),
timeout: z.number().optional().default(10000).describe('Maximum wait time in milliseconds'),
pollInterval: z.number().optional().default(100).describe('How often to check in milliseconds'),
};
/**
* Input schema for vscode_wait_for_text tool
*/
export const waitForTextInputSchema = {
text: z.string().describe('Text content to wait for'),
selector: z.string().optional().describe('CSS selector to search within (default: entire page)'),
exact: z.boolean().optional().default(false).describe('Require exact match vs contains'),
timeout: z.number().optional().default(10000).describe('Maximum wait time in milliseconds'),
};
/**
* Input schema for vscode_wait_for_idle tool
*/
export const waitForIdleInputSchema = {
timeout: z.number().optional().default(30000).describe('Maximum wait time in milliseconds'),
minIdleTime: z.number().optional().default(500).describe('Minimum idle time to consider VSCode idle'),
};
/**
* Input schema for vscode_wait tool (generic wait)
*/
export const waitInputSchema = {
milliseconds: z.number().describe('Time to wait in milliseconds'),
};
/**
* Input schema for vscode_wait_for_condition tool
*/
export const waitForConditionInputSchema = {
script: z.string().describe('JavaScript that returns true when condition is met'),
timeout: z.number().optional().default(10000).describe('Maximum wait time in milliseconds'),
pollInterval: z.number().optional().default(100).describe('How often to check in milliseconds'),
description: z.string().optional().describe('Description of what we are waiting for'),
};
/**
* Wait for an element to reach a specific state
*/
export async function waitForElement(input: {
selector: string;
state?: 'present' | 'visible' | 'hidden' | 'removed' | 'enabled' | 'disabled';
timeout?: number;
pollInterval?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const state = input.state || 'visible';
const timeout = input.timeout || 10000;
const pollInterval = input.pollInterval || 100;
const startTime = Date.now();
try {
while (Date.now() - startTime < timeout) {
const result = await webDriver.executeScript<string>(
`
const el = document.querySelector(arguments[0]);
const state = arguments[1];
if (!el) {
return JSON.stringify({ found: false, matches: state === 'removed' });
}
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
const isVisible = style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
const isEnabled = !el.disabled && el.getAttribute('aria-disabled') !== 'true';
let matches = false;
switch (state) {
case 'present': matches = true; break;
case 'visible': matches = isVisible; break;
case 'hidden': matches = !isVisible; break;
case 'removed': matches = false; break;
case 'enabled': matches = isEnabled; break;
case 'disabled': matches = !isEnabled; break;
}
return JSON.stringify({ found: true, matches, isVisible, isEnabled });
`,
input.selector,
state
);
const parsed = JSON.parse(result);
if (parsed.matches) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
selector: input.selector,
state: state,
waitedMs: Date.now() - startTime,
elementState: parsed,
}, null, 2),
}],
};
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
// Timeout
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: `Timeout waiting for element "${input.selector}" to be ${state}`,
selector: input.selector,
state: state,
timeout: timeout,
}, 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),
}],
};
}
}
/**
* Wait for specific text to appear
*/
export async function waitForText(input: {
text: string;
selector?: string;
exact?: boolean;
timeout?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const timeout = input.timeout || 10000;
const exact = input.exact || false;
const pollInterval = 100;
const startTime = Date.now();
try {
while (Date.now() - startTime < timeout) {
const result = await webDriver.executeScript<string>(
`
const searchText = arguments[0];
const selector = arguments[1];
const exact = arguments[2];
const container = selector ? document.querySelector(selector) : document.body;
if (!container) return JSON.stringify({ found: false, error: 'Container not found' });
const text = container.textContent || '';
const found = exact ? text === searchText : text.includes(searchText);
return JSON.stringify({ found, containerText: text.slice(0, 200) });
`,
input.text,
input.selector || null,
exact
);
const parsed = JSON.parse(result);
if (parsed.found) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
text: input.text,
selector: input.selector || 'body',
waitedMs: Date.now() - startTime,
}, null, 2),
}],
};
}
if (parsed.error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: parsed.error,
}, null, 2),
}],
};
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: `Timeout waiting for text "${input.text}"`,
text: input.text,
timeout: timeout,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Wait for VSCode to become idle (no loading indicators, etc.)
*/
export async function waitForIdle(input: {
timeout?: number;
minIdleTime?: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const timeout = input.timeout || 30000;
const minIdleTime = input.minIdleTime || 500;
const startTime = Date.now();
let lastBusyTime = Date.now();
try {
while (Date.now() - startTime < timeout) {
const result = await webDriver.executeScript<string>(
`
// Check for various loading indicators (only visible/active ones)
const loadingIndicators = [
'.monaco-progress-container.active',
'.codicon-loading',
'.codicon-sync.codicon-modifier-spin',
'.part.statusbar .statusbar-item.busy',
];
const isBusy = loadingIndicators.some(sel => {
const el = document.querySelector(sel);
return el && el.offsetParent !== null; // Only count visible elements
});
// Animations check disabled: VSCode always has animations running
const hasAnimations = false;
return JSON.stringify({
isBusy,
hasAnimations,
animationCount: 0,
indicators: loadingIndicators.filter(sel => document.querySelector(sel))
});
`
);
const parsed = JSON.parse(result);
if (parsed.isBusy || parsed.hasAnimations) {
lastBusyTime = Date.now();
} else if (Date.now() - lastBusyTime >= minIdleTime) {
// Been idle for minIdleTime
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
idleForMs: Date.now() - lastBusyTime,
totalWaitMs: Date.now() - startTime,
}, null, 2),
}],
};
}
await new Promise(resolve => setTimeout(resolve, 100));
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: 'Timeout waiting for VSCode to become idle',
timeout: timeout,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}
/**
* Simple wait for a specified duration
*/
export async function wait(input: {
milliseconds: number;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
await new Promise(resolve => setTimeout(resolve, input.milliseconds));
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
waitedMs: input.milliseconds,
}, null, 2),
}],
};
}
/**
* Wait for a custom JavaScript condition to be true
*/
export async function waitForCondition(input: {
script: string;
timeout?: number;
pollInterval?: number;
description?: string;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
const timeout = input.timeout || 10000;
const pollInterval = input.pollInterval || 100;
const startTime = Date.now();
try {
while (Date.now() - startTime < timeout) {
const result = await webDriver.executeScript<boolean>(
`
try {
return Boolean((function() { ${input.script} })());
} catch (e) {
return false;
}
`
);
if (result) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
description: input.description || 'Condition met',
waitedMs: Date.now() - startTime,
}, null, 2),
}],
};
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: `Timeout waiting for condition: ${input.description || input.script.slice(0, 100)}`,
timeout: timeout,
}, null, 2),
}],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
}],
};
}
}