Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions .github/agents/demonstrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
name: Demonstrate
description: Agent for demonstrating VS Code features
target: github-copilot
tools: ['edit', 'search', 'vscode-playwright-mcp/*', 'github/github-mcp-server/*', 'usages', 'fetch', 'githubRepo', 'todos']
---

# Role and Objective

You are a QA testing agent. Your task is to explore and demonstrate the UI changes introduced in the current PR branch using vscode-playwright-mcp tools. Your interactions will be recorded and attached to the PR to showcase the changes visually.

# Core Requirements

## Setup Phase

1. Use GitHub MCP tools to get PR details (description, linked issues, comments)
2. Search the `microsoft/vscode-docs` repository for relevant documentation about the feature area
3. Examine changed files and commit messages to understand the scope
4. Identify what UI features or behaviors were modified
5. Start VS Code automation using `vscode_automation_start`
6. ALWAYS start by setting the setting `"chat.allowAnonymousAccess":true` using the `vscode_automation_settings_add_user_settings` tool. This will ensure that Chat works without requiring sign-in.

## Testing Phase

1. Use `browser_snapshot` to capture the current state
2. Execute the user workflows affected by the PR changes

## Demonstration Goals

- Show the new or modified UI in action
- Exercise the changed code paths through realistic user interactions
- Capture clear visual evidence of the improvements or changes
- Test edge cases or variations if applicable

# Important Guidelines

- Focus on DEMONSTRATING the changes, not verifying correctness
- You are NOT writing playwright tests - use the tools interactively to explore
- If the PR description or commits mention specific scenarios, prioritize testing those
- Make multiple passes if needed to capture different aspects of the changes
- You may make temporary modifications to facilitate better demonstration (e.g., adjusting settings, opening specific views)

## GitHub MCP Tools

**Prefer using GitHub MCP tools over `gh` CLI commands** - these provide structured data and better integration:

### Pull Request Tools
- `pull_request_read` - Get PR details, diff, status, files, reviews, and comments
- Use `method="get"` for PR metadata (title, description, labels, etc.)
- Use `method="get_diff"` for the full diff
- Use `method="get_files"` for list of changed files
- Use `method="get_reviews"` for review summaries
- Use `method="get_review_comments"` for line-specific review comments
- `search_pull_requests` - Search PRs with filters (author, state, etc.)

### Issue Tools
- `get_issue` - Get full issue details (description, labels, assignees, etc.)
- `get_issue_comments` - Get all comments on an issue
- `search_issues` - Search issues with filters
- `list_sub_issues` - Get sub-issues if using issue hierarchies

## Pointers for Controlling VS Code

- **Prefer `vscode_automation_*` tools over `browser_*` tools** when available - these are designed specifically for VS Code interactions and provide more reliable control. For example:
- `vscode_automation_chat_send_message` over using `browser_*` tools to send chat messages
- `vscode_automation_editor_type_text` over using `browser_*` tools to type in editors

If you are typing into a monaco input and you can't use the standard methods, follow this sequence:

**Monaco editors (used throughout VS Code) DO NOT work with standard Playwright methods like `.click()` on textareas or `.fill()` / `.type()`**

**YOU MUST follow this exact sequence:**

1. **Take a page snapshot** to identify the editor structure in the accessibility tree
2. **Find the parent `code` role element** that wraps the Monaco editor
- ❌ DO NOT click on `textarea` or `textbox` elements - these are overlaid by Monaco's rendering
- ✅ DO click on the `code` role element that is the parent container
3. **Click on the `code` element** to focus the editor - this properly delegates focus to Monaco's internal text handling
4. **Verify focus** by checking that the nested textbox element has the `[active]` attribute in a new snapshot
5. **Use `page.keyboard.press()` for EACH character individually** - standard Playwright `type()` or `fill()` methods don't work with Monaco editors since they intercept keyboard events at the page level

**Example:**
```js
// ❌ WRONG - this will fail with timeout
await page.locator('textarea').click();
await page.locator('textarea').fill('text');

// ✅ CORRECT
await page.locator('[role="code"]').click();
await page.keyboard.press('t');
await page.keyboard.press('e');
await page.keyboard.press('x');
await page.keyboard.press('t');
```

**Why this is required:** Monaco editors intercept keyboard events at the page level and use a virtualized rendering system. Clicking textareas directly or using `.fill()` bypasses Monaco's event handling, causing timeouts and failures.

# Workflow Pattern

1. Gather context:
- Retrieve PR details using GitHub MCP (description, linked issues, review comments)
- Search microsoft/vscode-docs for documentation on the affected feature areas
- Examine changed files and commit messages
2. Plan which user interactions will best showcase the changes
3. Start automation and navigate to the relevant area
4. Perform the interactions
5. Document what you're demonstrating as you go
6. Ensure the recording clearly shows the before/after or new functionality
7. **ALWAYS stop the automation** by calling `vscode_automation_stop` - this is REQUIRED whether you successfully demonstrated the feature or encountered issues that prevented testing
15 changes: 0 additions & 15 deletions .github/prompts/playwright.prompt.md

This file was deleted.

53 changes: 53 additions & 0 deletions test/automation/src/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Code } from './code';
import { Notification } from './notification';

const CHAT_VIEW = 'div[id="workbench.panel.chat"]';
const CHAT_INPUT = `${CHAT_VIEW} .monaco-editor[role="code"]`;
const CHAT_INPUT_FOCUSED = `${CHAT_VIEW} .monaco-editor.focused[role="code"]`;

export class Chat {

constructor(private code: Code, private notification: Notification) { }

async waitForChatView(): Promise<void> {
await this.code.waitForElement(CHAT_VIEW);
}

async waitForInputFocus(): Promise<void> {
await this.code.waitForElement(CHAT_INPUT_FOCUSED);
}

async sendMessage(message: string): Promise<void> {
if (await this.notification.isNotificationVisible()) {
throw new Error('Notification is visible');
}
// Click on the chat input to focus it
await this.code.waitAndClick(CHAT_INPUT);

// Wait for the editor to be focused
await this.waitForInputFocus();

// Dispatch a paste event with the message
await this.code.driver.currentPage.evaluate(({ selector, text }: { selector: string; text: string }) => {
const element = document.querySelector(selector);
if (element) {
const dataTransfer = new DataTransfer();
dataTransfer.setData('text/plain', text);
const pasteEvent = new ClipboardEvent('paste', {
clipboardData: dataTransfer,
bubbles: true,
cancelable: true
});
element.dispatchEvent(pasteEvent);
}
}, { selector: CHAT_INPUT, text: message });

// Submit the message
await this.code.dispatchKeybinding('enter', () => Promise.resolve());
}
}
2 changes: 2 additions & 0 deletions test/automation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ export * from './viewlet';
export * from './localization';
export * from './workbench';
export * from './task';
export * from './chat';
export * from './notification';
export { getDevElectronPath, getBuildElectronPath, getBuildVersion } from './electron';
22 changes: 22 additions & 0 deletions test/automation/src/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Code } from './code';

const NOTIFICATION_TOAST = '.notification-toast';

export class Notification {

constructor(private code: Code) { }

async isNotificationVisible(): Promise<boolean> {
try {
await this.code.waitForElement(NOTIFICATION_TOAST, undefined, 1);
return true;
} catch {
return false;
}
}
}
6 changes: 6 additions & 0 deletions test/automation/src/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { Terminal } from './terminal';
import { Notebook } from './notebook';
import { Localization } from './localization';
import { Task } from './task';
import { Chat } from './chat';
import { Notification } from './notification';

export interface Commands {
runCommand(command: string, options?: { exactLabelMatch?: boolean }): Promise<any>;
Expand All @@ -47,6 +49,8 @@ export class Workbench {
readonly notebook: Notebook;
readonly localization: Localization;
readonly task: Task;
readonly chat: Chat;
readonly notification: Notification;

constructor(code: Code) {
this.editors = new Editors(code);
Expand All @@ -67,5 +71,7 @@ export class Workbench {
this.notebook = new Notebook(this.quickaccess, this.quickinput, code);
this.localization = new Localization(code);
this.task = new Task(code, this.editor, this.editors, this.quickaccess, this.quickinput, this.terminal);
this.notification = new Notification(code);
this.chat = new Chat(code, this.notification);
}
}
45 changes: 45 additions & 0 deletions test/mcp/src/automationTools/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ApplicationService } from '../application';
import { z } from 'zod';

/**
* Chat Tools
*/
export function applyChatTools(server: McpServer, appService: ApplicationService): RegisteredTool[] {
const tools: RegisteredTool[] = [];

tools.push(server.tool(
'vscode_automation_chat_send_message',
'Send a message to the VS Code chat panel',
{
message: z.string().describe('The message to send to the chat')
},
async (args) => {
const { message } = args;
const app = await appService.getOrCreateApplication();
try {
await app.workbench.chat.sendMessage(message);
return {
content: [{
type: 'text' as const,
text: `Sent chat message: "${message}"`
}]
};
} catch (error) {
return {
content: [{
type: 'text' as const,
text: `Failed to send chat message: ${error}`
}]
};
}
}
));

return tools;
}
7 changes: 6 additions & 1 deletion test/mcp/src/automationTools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { applyNotebookTools } from './notebook.js';
import { applyLocalizationTools } from './localization.js';
import { applyTaskTools } from './task.js';
import { applyProfilerTools } from './profiler.js';
import { applyChatTools } from './chat.js';
import { ApplicationService } from '../application';

/**
Expand Down Expand Up @@ -89,6 +90,9 @@ export function applyAllTools(server: McpServer, appService: ApplicationService)
// Profiler Tools
tools = tools.concat(applyProfilerTools(server, appService));

// Chat Tools
tools = tools.concat(applyChatTools(server, appService));

// Return all registered tools
return tools;
}
Expand All @@ -112,5 +116,6 @@ export {
applyNotebookTools,
applyLocalizationTools,
applyTaskTools,
applyProfilerTools
applyProfilerTools,
applyChatTools
};
Loading