/**
* VSCode Automation MCP Server - Notification & Dialog Tools
*
* Tools for interacting with notifications, dialogs, and popups.
*
* @author Sukarth Acharya
* @license MIT
*/
import { z } from 'zod';
import { getVSCodeDriver } from '../vscode-driver.js';
/**
* Input schema for vscode_get_notifications tool
*/
export const getNotificationsInputSchema = {
includeHidden: z.boolean().optional().default(false).describe('Include dismissed/hidden notifications'),
};
/**
* Input schema for vscode_dismiss_notification tool
*/
export const dismissNotificationInputSchema = {
index: z.number().optional().describe('Index of notification to dismiss (from get_notifications)'),
text: z.string().optional().describe('Dismiss notification containing this text'),
all: z.boolean().optional().default(false).describe('Dismiss all notifications'),
};
/**
* Input schema for vscode_click_notification_button tool
*/
export const clickNotificationButtonInputSchema = {
notificationIndex: z.number().optional().default(0).describe('Index of the notification'),
buttonText: z.string().describe('Text of the button to click'),
};
/**
* Input schema for vscode_get_dialogs tool
*/
export const getDialogsInputSchema = {};
/**
* Input schema for vscode_handle_dialog tool
*/
export const handleDialogInputSchema = {
action: z.enum(['accept', 'dismiss', 'input']).describe('How to handle the dialog'),
inputText: z.string().optional().describe('Text to enter for input dialogs'),
buttonText: z.string().optional().describe('Specific button to click'),
};
/**
* Get all visible notifications
*/
export async function getNotifications(input: {
includeHidden?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
const result = await webDriver.executeScript<string>(
`
const notifications = [];
// VSCode notification center
const notificationElements = document.querySelectorAll('.notification-toast, .notification-list-item, .monaco-notification');
notificationElements.forEach((el, index) => {
const text = el.textContent?.trim() || '';
const severity = el.classList.contains('error') || el.querySelector('.codicon-error') ? 'error' :
el.classList.contains('warning') || el.querySelector('.codicon-warning') ? 'warning' :
'info';
const buttons = Array.from(el.querySelectorAll('a.action-label, button.action-label, .notification-list-item-buttons-container a'))
.map(btn => btn.textContent?.trim())
.filter(Boolean);
const source = el.querySelector('.notification-list-item-source')?.textContent?.trim() ||
el.querySelector('.notification-source')?.textContent?.trim() || '';
const isVisible = el.offsetParent !== null;
notifications.push({
index,
severity,
text: text.slice(0, 500),
source,
buttons,
visible: isVisible,
selector: generateSelector(el)
});
});
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(notifications);
`
);
let notifications = JSON.parse(result);
if (!input.includeHidden) {
notifications = notifications.filter((n: { visible: boolean }) => n.visible);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
count: notifications.length,
notifications,
}, 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),
}],
};
}
}
/**
* Dismiss notifications
*/
export async function dismissNotification(input: {
index?: number;
text?: string;
all?: boolean;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
const result = await webDriver.executeScript<string>(
`
const notifications = document.querySelectorAll('.notification-toast, .notification-list-item, .monaco-notification');
const index = arguments[0];
const text = arguments[1];
const all = arguments[2];
let dismissed = 0;
notifications.forEach((el, i) => {
let shouldDismiss = false;
if (all) {
shouldDismiss = true;
} else if (index !== null && index !== undefined && i === index) {
shouldDismiss = true;
} else if (text && el.textContent?.includes(text)) {
shouldDismiss = true;
}
if (shouldDismiss) {
const closeBtn = el.querySelector('.codicon-close, .clear-notification-action, [aria-label="Close"], .action-label.codicon-notifications-clear');
if (closeBtn) {
closeBtn.click();
dismissed++;
}
}
});
return JSON.stringify({ dismissed });
`,
input.index ?? null,
input.text ?? null,
input.all || false
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
dismissed: parsed.dismissed,
}, 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),
}],
};
}
}
/**
* Click a button in a notification
*/
export async function clickNotificationButton(input: {
notificationIndex?: number;
buttonText: string;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
const result = await webDriver.executeScript<string>(
`
const notifications = document.querySelectorAll('.notification-toast, .notification-list-item, .monaco-notification');
const index = arguments[0] ?? 0;
const buttonText = arguments[1];
const notification = notifications[index];
if (!notification) {
return JSON.stringify({ success: false, error: 'Notification not found at index ' + index });
}
const buttons = notification.querySelectorAll('a.action-label, button.action-label, .notification-list-item-buttons-container a');
for (const btn of buttons) {
if (btn.textContent?.trim() === buttonText) {
btn.click();
return JSON.stringify({ success: true, clicked: buttonText });
}
}
return JSON.stringify({
success: false,
error: 'Button not found',
availableButtons: Array.from(buttons).map(b => b.textContent?.trim())
});
`,
input.notificationIndex ?? 0,
input.buttonText
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify(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,
}, null, 2),
}],
};
}
}
/**
* Get current dialogs/modals
*/
export async function getDialogs(_input: Record<string, never>): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
const result = await webDriver.executeScript<string>(
`
const dialogs = [];
// VSCode dialog selectors
const dialogSelectors = [
'.monaco-dialog-box',
'.dialog-shadow',
'.quick-input-widget:not(.hidden)',
'.context-view',
'[role="dialog"]',
'[role="alertdialog"]',
'.modal',
];
dialogSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach((el, index) => {
if (el.offsetParent === null) return; // Hidden
const title = el.querySelector('.dialog-title, .quick-input-title, h2, .modal-title')?.textContent?.trim() || '';
const message = el.querySelector('.dialog-message, .dialog-message-text, .quick-input-message, .modal-body')?.textContent?.trim() || '';
const buttons = Array.from(el.querySelectorAll('button, .dialog-button, a.action-label'))
.map(btn => btn.textContent?.trim())
.filter(Boolean);
const inputs = Array.from(el.querySelectorAll('input, textarea'))
.map(inp => ({
type: inp.type || 'text',
placeholder: inp.placeholder,
value: inp.value,
selector: inp.id ? '#' + inp.id : 'input'
}));
dialogs.push({
index,
type: selector,
title: title.slice(0, 200),
message: message.slice(0, 500),
buttons,
inputs,
selector: el.id ? '#' + el.id : selector
});
});
});
return JSON.stringify(dialogs);
`
);
const dialogs = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
hasDialog: dialogs.length > 0,
count: dialogs.length,
dialogs,
}, 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),
}],
};
}
}
/**
* Handle a dialog (accept, dismiss, or input)
*/
export async function handleDialog(input: {
action: 'accept' | 'dismiss' | 'input';
inputText?: string;
buttonText?: string;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
const result = await webDriver.executeScript<string>(
`
const action = arguments[0];
const inputText = arguments[1];
const buttonText = arguments[2];
// Find the dialog
const dialogSelectors = [
'.monaco-dialog-box',
'.quick-input-widget:not(.hidden)',
'[role="dialog"]',
'[role="alertdialog"]',
];
let dialog = null;
for (const selector of dialogSelectors) {
dialog = document.querySelector(selector);
if (dialog && dialog.offsetParent !== null) break;
dialog = null;
}
if (!dialog) {
return JSON.stringify({ success: false, error: 'No dialog found' });
}
// Handle input
if (action === 'input' && inputText !== undefined) {
const input = dialog.querySelector('input, textarea');
if (input) {
input.value = inputText;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}
// Find and click button
let buttonToClick = null;
const buttons = dialog.querySelectorAll('button, .dialog-button, a.action-label');
if (buttonText) {
buttonToClick = Array.from(buttons).find(b => b.textContent?.trim() === buttonText);
} else if (action === 'accept') {
// Look for primary/OK button
buttonToClick = dialog.querySelector('.primary, [data-default], button:first-of-type');
if (!buttonToClick) buttonToClick = Array.from(buttons).find(b =>
/^(ok|yes|accept|confirm|save|continue|submit)$/i.test(b.textContent?.trim() || '')
);
} else if (action === 'dismiss') {
// Look for cancel/close button
buttonToClick = dialog.querySelector('.secondary, [aria-label="Close"]');
if (!buttonToClick) buttonToClick = Array.from(buttons).find(b =>
/^(cancel|no|close|dismiss)$/i.test(b.textContent?.trim() || '')
);
// Or press Escape
if (!buttonToClick) {
document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
return JSON.stringify({ success: true, action: 'escape' });
}
}
if (buttonToClick) {
buttonToClick.click();
return JSON.stringify({ success: true, clicked: buttonToClick.textContent?.trim() });
}
return JSON.stringify({
success: false,
error: 'Could not find appropriate button',
availableButtons: Array.from(buttons).map(b => b.textContent?.trim())
});
`,
input.action,
input.inputText ?? null,
input.buttonText ?? null
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify(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,
}, null, 2),
}],
};
}
}
/**
* Get quick pick items (command palette, file picker, etc.)
*/
export async function getQuickPickItems(_input: Record<string, never>): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
const result = await webDriver.executeScript<string>(
`
const quickInput = document.querySelector('.quick-input-widget:not(.hidden)');
if (!quickInput) {
return JSON.stringify({ visible: false, items: [] });
}
const title = quickInput.querySelector('.quick-input-title')?.textContent?.trim() || '';
const inputValue = quickInput.querySelector('input')?.value || '';
const items = [];
const listItems = quickInput.querySelectorAll('.quick-input-list .monaco-list-row, .quick-pick-list .monaco-list-row');
listItems.forEach((item, index) => {
const label = item.querySelector('.label-name, .quick-input-list-label')?.textContent?.trim() || '';
const description = item.querySelector('.label-description, .quick-input-list-description')?.textContent?.trim() || '';
const detail = item.querySelector('.quick-input-list-detail')?.textContent?.trim() || '';
const isFocused = item.classList.contains('focused');
const isSelected = item.querySelector('.codicon-check') !== null;
items.push({
index,
label,
description,
detail,
focused: isFocused,
selected: isSelected
});
});
return JSON.stringify({
visible: true,
title,
inputValue,
itemCount: items.length,
items
});
`
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
...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,
}, null, 2),
}],
};
}
}
/**
* Select an item from the quick pick
*/
export async function selectQuickPickItem(input: {
index?: number;
label?: string;
}): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
const driver = getVSCodeDriver();
const webDriver = await driver.getDriver();
try {
const result = await webDriver.executeScript<string>(
`
const quickInput = document.querySelector('.quick-input-widget:not(.hidden)');
if (!quickInput) {
return JSON.stringify({ success: false, error: 'No quick pick visible' });
}
const index = arguments[0];
const label = arguments[1];
const items = quickInput.querySelectorAll('.quick-input-list .monaco-list-row, .quick-pick-list .monaco-list-row');
let targetItem = null;
if (index !== null && index !== undefined) {
targetItem = items[index];
} else if (label) {
targetItem = Array.from(items).find(item => {
const itemLabel = item.querySelector('.label-name, .quick-input-list-label')?.textContent?.trim();
return itemLabel === label || itemLabel?.includes(label);
});
}
if (!targetItem) {
return JSON.stringify({
success: false,
error: 'Item not found',
availableItems: Array.from(items).slice(0, 10).map(i =>
i.querySelector('.label-name, .quick-input-list-label')?.textContent?.trim()
)
});
}
targetItem.click();
return JSON.stringify({
success: true,
selected: targetItem.querySelector('.label-name, .quick-input-list-label')?.textContent?.trim()
});
`,
input.index ?? null,
input.label ?? null
);
const parsed = JSON.parse(result);
return {
content: [{
type: 'text',
text: JSON.stringify(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,
}, null, 2),
}],
};
}
}
// Export input schema for quick pick
export const getQuickPickItemsInputSchema = {};
export const selectQuickPickItemInputSchema = {
index: z.number().optional().describe('Index of the item to select'),
label: z.string().optional().describe('Label text of the item to select (partial match)'),
};