#
tokens: 30392/50000 45/45 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .github
│   └── npm-publish.yml
├── .gitignore
├── components.json
├── eslint.config.js
├── index.html
├── LICENSE
├── package-lock.json
├── package.json
├── public
│   └── vite.svg
├── README.md
├── src
│   ├── App
│   │   ├── context.tsx
│   │   ├── execute.tsx
│   │   └── index.tsx
│   ├── assets
│   │   └── react.svg
│   ├── components
│   │   └── ui
│   │       ├── button.tsx
│   │       ├── card.tsx
│   │       ├── click-to-edit.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── scroll-area.tsx
│   │       ├── tabs.tsx
│   │       └── textarea.tsx
│   ├── hooks
│   │   └── use-global-stage.ts
│   ├── index.css
│   ├── lib
│   │   └── utils.ts
│   ├── main.tsx
│   ├── mcp
│   │   ├── eval.ts
│   │   ├── handle-browser-event.ts
│   │   ├── index.ts
│   │   ├── logger.ts
│   │   ├── recording
│   │   │   ├── events.ts
│   │   │   ├── index.ts
│   │   │   ├── init-recording.ts
│   │   │   ├── selector-engine.ts
│   │   │   ├── snowflake.ts
│   │   │   └── utils.ts
│   │   ├── state.ts
│   │   └── toolbox.ts
│   ├── server.ts
│   ├── types.ts
│   ├── vite-env.d.ts
│   └── web-server.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.lib.json
├── tsconfig.node.json
├── tsup.config.ts
└── vite.config.ts
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules
build
.npmrc
dist
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# How to Use playwright-mcp?

[![npm version](https://img.shields.io/npm/v/playwright-mcp)](https://www.npmjs.com/package/playwright-mcp) [![Docs](https://img.shields.io/badge/docs-playwright--mcp-blue)](https://ashish-bansal.github.io/playwright-mcp/)

## Introduction

Ever tried using Cursor or Claude to write Playwright tests? Yeah, it's kind of a mess.

Why? Because your AI assistant has no clue what's on your web page. It's like trying to describe a painting to someone who's blindfolded. The result?

- **Flaky tests** → The AI is guessing selectors, and it gets them wrong.
- **Broken scripts** → You spend more time fixing tests than writing them.
- **Debugging nightmares** → The AI can't "see" what's happening, so you end up doing all the heavy lifting.

Wouldn't it be nice if your AI could actually understand your web page instead of just making educated guesses?

### Enter playwright-mcp !

`playwright-mcp` gives your AI assistant superpowers by making the browser fully visible to it. Instead of working in the dark, your AI assistant now has real-time access to the page DOM, elements, and interactions.

### How playwright-mcp Works (AKA How We Fix This Mess)

Once installed, playwright-mcp spins up a Playwright-controlled browser and gives your AI assistant full access to it. This unlocks:

1. **Recording interactions** → Click, type, scroll—let playwright-mcp turn your actions into fully functional Playwright test cases.
2. **Taking screenshots** → Capture elements or full pages so your AI gets actual visual context (no more guessing).
3. **Extracting the DOM** → Grab the entire HTML structure so the AI can generate rock-solid selectors.
4. **Executing JavaScript** → Run custom JS inside the browser for debugging, automation, or just for fun.

### Why You'll Love playwright-mcp

🚀 **AI-generated tests that actually work** → No more flaky selectors, broken tests, or guesswork.

⏳ **Massive time savings** → Write and debug Playwright tests 5x faster.

🛠️ **Fewer headaches** → Your AI assistant gets live page context, so it can generate real test cases.

🔌 **Works out of the box** → Plug it into Cursor, Claude, WindSurf, or whatever you use—it just works.

#### **Give Your AI the Context It Deserves**

Tired of fixing AI-generated tests? Stop debugging AI's bad guesses—start writing flawless Playwright tests. Use the guide below to install playwright-mcp and let your AI assistant actually help you for once. 

---

### Installation: Get Up and Running in No Time

### Prerequisites (a.k.a. What You Need Before You Start)

Before you install `playwright-mcp`, make sure you have:

✅ Node.js installed on your system (because, well… it's an npm package)

✅ Playwright and its Chromium browser installed

✅ An IDE that supports MCP, like Cursor

✅ A brain that's ready to automate Playwright tests like a pro

### Setting Up playwright-mcp (It's Easy, I Promise)

#### Connect It to Your IDE (Cursor Setup)

If you're using Cursor, follow these steps to connect `playwright-mcp` like a boss:

1. Open Cursor IDE
2. Navigate to Settings (⚙️)
3. Select Cursor Settings → Go to the MCP tab
4. Click "Add new MCP server"
5. Fill in the following details:
    
    ![Connect playwright-mcp to cursor](https://github.com/Ashish-Bansal/playwright-mcp/blob/docs/static/img/cursor-add-mcp.png?raw=true)


    - Name → `playwright-mcp`
    - Command → `npx -y playwright-mcp`
6. Click "Add", and boom—you're connected! 🚀

Note: If clicking on "Add new MCP server", opens a file(.cursor/mcp.json), Paste the following code and save:

```jsx
{
  "mcpServers": {
    "playwright-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "playwright-mcp"
      ]
    }
  }
}
```

Now Cursor actually understands your web pages. No more random test suggestions based on zero context! Head to the [Claude tutorial](https://ashish-bansal.github.io/playwright-mcp/tutorials/claude-desktop-tutorial) or [Cursor tutorial](https://ashish-bansal.github.io/playwright-mcp/tutorials/cursor-tutorial) to understand it in details. 

---

### **Connect It to Claude desktop**

Wait… Does It Work with Other AI Assistants? Yes! While `playwright-mcp` is a match made in heaven for IDEs, you can use it with Claude desktop to write tests as well. 

1. Install `playwright-mcp` (The Easy Part)
    1. First things first, fire up your terminal and run:
    2. `npm install -g playwright-mcp`
2. Hook It Up to Claude Desktop
    1. Find the Configuration File
    2. On windows 
        1. `%APPDATA%\Claude\claude_desktop_config.json`
    3. On macOS: 
        1. `~/Library/Application Support/Claude/claude_desktop_config.json`
    4. Update the config file
    
    ```jsx
    {
      "mcpServers": {
        "playwright": {
          "command": "npx",
          "args": ["-y", "playwright-mcp"]
        }
      }
    }
    ```
    
3. Restart Claude Desktop (Because It's a New Day)
    1. Close and reopen Claude Desktop to apply the changes.
4. Verify That It's Working 
    1. Once everything is set up, let's test if Claude can actually talk to Playwright now.
    2. Open Claude and ask: "List all available MCP tools."
    3. If `playwright-mcp` is installed correctly, it should list tools like:
        1. `get-context`
        2. `get-full-dom`
        3. `get-screenshot`
        4. `execute-code`
        5. `init-browser`
        6. `validate-selectors`
    4. Ask Claude to init browser and a browser should open up after your approval! 

Now that the Calude has access to the web pages. You can ask it write highly contextual tests! Head to the [Claude tutorial](https://ashish-bansal.github.io/playwright-mcp/tutorials/claude-desktop-tutorial) or [Cursor tutorial](https://ashish-bansal.github.io/playwright-mcp/tutorials/cursor-tutorial) to understand it in details. 


[📖 **View Documentation**](https://ashish-bansal.github.io/playwright-mcp/)

```

--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------

```typescript
/// <reference types="vite/client" />

```

--------------------------------------------------------------------------------
/src/mcp/recording/index.ts:
--------------------------------------------------------------------------------

```typescript
import { initRecording } from "./init-recording.js";

export { initRecording };
```

--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

```

--------------------------------------------------------------------------------
/src/mcp/recording/snowflake.ts:
--------------------------------------------------------------------------------

```typescript
import { Snowflake } from "@skorotkiewicz/snowflake-id";

const snowflake = new Snowflake(42 * 10);

export const getSnowflakeId = async () => {
  return await snowflake.generate();
}

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

```

--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------

```typescript
import { defineConfig } from 'tsup'

export default defineConfig({
  env: {
    NODE_ENV: process.env.NODE_ENV || 'development',
  },
  entry: ['src/server.ts', 'src/mcp/interceptor.ts', 'src/mcp/toolbox.ts'],
  tsconfig: 'tsconfig.lib.json',
  splitting: true,
  format: ['cjs', 'esm'],
  clean: true,
})

```

--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script>window.triggerSyncToReact()</script>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/index.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "iconLibrary": "lucide"
}
```

--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------

```typescript
// vite.ui.config.js
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"


export default defineConfig({
  plugins: [react(), tailwindcss()],
  server: {
    port: 5174
  },
  build: {
    outDir: 'dist/ui',
    // Generate assets that will be embedded in your Node.js package
    emptyOutDir: true
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})
```

--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "ES2022",
    "lib": ["ES2023"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["vite.config.ts"]
}

```

--------------------------------------------------------------------------------
/tsconfig.lib.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "ES2022",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["vite.config.ts"]
}

```

--------------------------------------------------------------------------------
/src/hooks/use-global-stage.ts:
--------------------------------------------------------------------------------

```typescript
import { useCallback, useEffect, useState } from "react";

export const useGlobalState = () => {
  const [state, setState] = useState(window.globalState);

  useEffect(() => {
    // Subscribe to changes from other components/Node
    const subscriptionId = window.stateSubscribers.length;
    window.stateSubscribers.push(setState);

    return () => {
      window.stateSubscribers = window.stateSubscribers.filter(
        (_, index) => index !== subscriptionId
      );
    };
  }, []);

  // Create an update function that syncs with Node
  const updateState = useCallback((update: any) => {
    window.updateGlobalState(update);
  }, []);

  return [state, updateState];
}

```

--------------------------------------------------------------------------------
/.github/npm-publish.yml:
--------------------------------------------------------------------------------

```yaml
name: Publish to npm

on:
  release:
    types: [created]
  workflow_dispatch:

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Set up npm authentication
        run: |
          echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc

      - name: Publish to npm
        run: npm publish

```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
    },
  },
)

```

--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true,

    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }

  },
  "include": ["src"]
}

```

--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------

```typescript
import * as React from "react"

import { cn } from "@/lib/utils"

function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
  return (
    <textarea
      data-slot="textarea"
      className={cn(
        "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
        className
      )}
      {...props}
    />
  )
}

export { Textarea }

```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { server } from "./mcp/index";
import { webServer, isPortInUse } from "./web-server";

async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("MCP Server started");

    if (process.env.NODE_ENV !== 'development') {
      const portInUse = await isPortInUse(5174);
      if (!portInUse) {
        webServer.listen(5174, () => {
          console.error("Web server started");
        });
      } else {
        console.error("Port 5174 is in use, skipping web server");
      }
    }
}

main().catch((error) => {
    console.error("Fatal error in main", error);
    process.exit(1);
});

```

--------------------------------------------------------------------------------
/src/mcp/logger.ts:
--------------------------------------------------------------------------------

```typescript
type LogLevel = 'debug' | 'info' | 'warn' | 'error'

class Logger {
  private level: LogLevel

  constructor(level: LogLevel = 'info') {
    this.level = level
  }

  private shouldLog(messageLevel: LogLevel): boolean {
    const levels: LogLevel[] = ['debug', 'info', 'warn', 'error']
    return levels.indexOf(messageLevel) >= levels.indexOf(this.level)
  }

  private log(level: LogLevel, ...args: any[]): void {
    if (this.shouldLog(level)) {
      console[level](...args)
    }
  }

  debug(...args: any[]): void {
    this.log('debug', ...args)
  }

  info(...args: any[]): void {
    this.log('info', ...args)
  }

  warn(...args: any[]): void {
    this.log('warn', ...args)
  }

  error(...args: any[]): void {
    this.log('error', ...args)
  }
}

const logger = new Logger()

export { logger, Logger }

```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
declare global {
  interface Message {
    type: 'DOM' | 'Image' | 'Text' | 'Interaction';
    content: string;
    windowUrl: string;
  }

  interface Window {
    // for the user page
    mcpStartPicking: (pickingType: 'DOM' | 'Image') => void;
    mcpStopPicking: () => void;
    onElementPicked: (message: Message) => void;
    // for the iframe
    triggerMcpStartPicking: (pickingType: 'DOM' | 'Image') => void;
    triggerMcpStopPicking: () => void;
    // for the react page
    globalState: any;
    stateSubscribers: ((state: any) => void)[];
    notifyStateSubscribers: () => void;
    updateGlobalState: (state: any) => void;
    triggerSyncToReact: () => void;
    // for recording
    recordDOM: (dom: string, elementUUID: string) => Promise<void>;
    recordInput: (dom: string, elementUUID: string, value: string) => Promise<void>;
    recordKeyPress: (dom: string, keys: string[]) => Promise<void>;
  }
}



```

--------------------------------------------------------------------------------
/src/mcp/recording/events.ts:
--------------------------------------------------------------------------------

```typescript
export enum BrowserEventType {
    Click = 'click',
    Input = 'input',
    KeyPress = 'key-press',
    OpenPage = 'open-page',
  }

  export interface BaseBrowserEvent {
    eventId: string
    dom: string
    windowUrl: string
  }

  export interface ClickBrowserEvent extends BaseBrowserEvent {
    type: BrowserEventType.Click
    elementUUID: string
    selectors: string[]
    elementName?: string
    elementType?: string
  }

  export interface InputBrowserEvent extends BaseBrowserEvent {
    type: BrowserEventType.Input
    elementUUID: string
    typedText: string
    selectors: string[]
    elementName?: string
    elementType?: string
  }

  export interface KeyPressBrowserEvent extends BaseBrowserEvent {
    type: BrowserEventType.KeyPress
    keys: string[]
  }

  export interface OpenPageBrowserEvent extends BaseBrowserEvent {
    type: BrowserEventType.OpenPage
    title: string
  }

  export type BrowserEvent =
    | ClickBrowserEvent
    | InputBrowserEvent
    | KeyPressBrowserEvent
    | OpenPageBrowserEvent

```

--------------------------------------------------------------------------------
/src/App/index.tsx:
--------------------------------------------------------------------------------

```typescript
import React from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import Context from './context';
import Execute from './execute';

const App: React.FC = () => {
  return (
    <div className="fixed top-0 right-0 w-full h-screen bg-gray-100 border-l border-zinc-200 z-[999999] flex flex-col overflow-hidden">
      <div className="p-4 bg-white border-b border-zinc-200 flex items-center justify-center">
        <h3 className="m-0 text-base font-medium text-gray-900">
          Playwright MCP
        </h3>
      </div>

      <Tabs defaultValue="context" className="flex-1 flex flex-col">
        <div className="p-4 bg-white border-b border-zinc-200">
          <TabsList>
            <TabsTrigger value="context">Context</TabsTrigger>
            <TabsTrigger value="execute">Execute</TabsTrigger>
          </TabsList>
        </div>

        <TabsContent value="context" className="flex-1">
          <Context />
        </TabsContent>

        <TabsContent value="execute" className="flex-1">
          <Execute />
        </TabsContent>
      </Tabs>
    </div>
  );
};

export default App;

```

--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------

```
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
```

--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------

```typescript
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"

import { cn } from "@/lib/utils"

function ScrollArea({
  className,
  children,
  ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
  return (
    <ScrollAreaPrimitive.Root
      data-slot="scroll-area"
      className={cn("relative", className)}
      {...props}
    >
      <ScrollAreaPrimitive.Viewport
        data-slot="scroll-area-viewport"
        className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
      >
        {children}
      </ScrollAreaPrimitive.Viewport>
      <ScrollBar />
      <ScrollAreaPrimitive.Corner />
    </ScrollAreaPrimitive.Root>
  )
}

function ScrollBar({
  className,
  orientation = "vertical",
  ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
  return (
    <ScrollAreaPrimitive.ScrollAreaScrollbar
      data-slot="scroll-area-scrollbar"
      orientation={orientation}
      className={cn(
        "flex touch-none p-px transition-colors select-none",
        orientation === "vertical" &&
          "h-full w-2.5 border-l border-l-transparent",
        orientation === "horizontal" &&
          "h-2.5 flex-col border-t border-t-transparent",
        className
      )}
      {...props}
    >
      <ScrollAreaPrimitive.ScrollAreaThumb
        data-slot="scroll-area-thumb"
        className="bg-border relative flex-1 rounded-full"
      />
    </ScrollAreaPrimitive.ScrollAreaScrollbar>
  )
}

export { ScrollArea, ScrollBar }

```

--------------------------------------------------------------------------------
/src/mcp/state.ts:
--------------------------------------------------------------------------------

```typescript
import type { Page } from "playwright";
type PickingType = 'DOM' | 'Image';

let globalState = {
  messages: [] as Message[],
  pickingType: null as PickingType | null,
  recordingInteractions: false as boolean,
  code: `async function run(page) {
    let title = await page.title();
    return title
}` as string,
}

async function initState(page: Page) {
  // function to notify Node.js from React
  await page.exposeFunction('updateGlobalState', (state: any) => {
    updateState(page, state);
  });

  await page.exposeFunction('triggerSyncToReact', () => {
    updateState(page, getState());
  });

  await page.addInitScript((state) => {
    if (window.globalState) {
      return
    }

    window.globalState = state;
    window.stateSubscribers = [];

    // function to notify other components
    window.notifyStateSubscribers = () => {
      window.stateSubscribers.forEach(cb => cb(window.globalState));
    };
  }, globalState);
}

async function syncToReact(page: Page, state: typeof globalState) {
  const allFrames = await page.frames();
  const toolboxFrame = allFrames.find(f => f.name() === 'toolbox-frame');
  if (!toolboxFrame) {
    console.error('Toolbox frame not found');
    return;
  }

  try {
    await toolboxFrame.evaluate((state) => {
      window.globalState = state;
      window.notifyStateSubscribers();
    }, state);
  } catch (error) {
    console.debug('Error syncing to React:', error);
  }
}

const getState = () => {
  return structuredClone(globalState);
}

const updateState = (page: Page, state: typeof globalState) => {
  globalState = structuredClone(state);
  syncToReact(page, state);
}

export { initState, getState, updateState, type Message };

```

--------------------------------------------------------------------------------
/src/components/ui/click-to-edit.tsx:
--------------------------------------------------------------------------------

```typescript
import type * as React from 'react'

import { cn } from '@/lib/utils'
import { useEffect, useRef } from 'react'
import ContentEditable from 'react-contenteditable'
import useStateRef from 'react-usestateref'

const ClickToEdit = ({
  text,
  textClassName,
  className,
  placeholder,
  onSave,
  ...props
}: {
  text: string
  textClassName?: string
  className?: string
  placeholder?: string
  onSave: (text: string) => void
} & React.HTMLAttributes<HTMLDivElement>) => {
  const [value, setValue, valueRef] = useStateRef(text)
  const contentEditable = useRef<HTMLElement>(null)

  const handleChange = (e: { target: { value: string } }) => {
    setValue(contentEditable.current?.textContent || '')
  }

  const handleBlur = () => {
    const currentValue = valueRef.current
    if (currentValue && currentValue !== text) {
      onSave(currentValue)
    }
  }

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      e.preventDefault()
      contentEditable.current?.blur()
    }
  }

  useEffect(() => {
    setValue(text)
  }, [text])

  return (
    <div className={cn('flex items-center gap-2 w-auto', className)}>
      <ContentEditable
        innerRef={contentEditable}
        html={value}
        onChange={handleChange}
        className={cn(
          'flex-1 flex items-center gap-1 group outline-none',
          placeholder &&
            !value &&
            'before:content-[attr(data-placeholder)] before:text-muted-foreground',
          textClassName,
        )}
        onBlur={handleBlur}
        onKeyDown={handleKeyDown}
        data-placeholder={placeholder}
        {...props}
      />
    </div>
  )
}

ClickToEdit.displayName = 'ClickToEdit'

export { ClickToEdit }

```

--------------------------------------------------------------------------------
/src/mcp/handle-browser-event.ts:
--------------------------------------------------------------------------------

```typescript
import { updateState } from "./state";
import { getState } from "./state";
import { preprocessBrowserEvent } from "./recording/utils";
import { Page } from "playwright";
import _ from "lodash";

export const handleBrowserEvent = (page: Page) => {
  const eventQueue: any[] = [];

  const processEvents = _.debounce(() => {
    if (eventQueue.length === 0) {
      return;
    }

    // Skip events for same element and type
    while (eventQueue.length > 1) {
      const currentEvent = eventQueue[0];
      const nextEvent = eventQueue[1];
      if (currentEvent.type === nextEvent.type && currentEvent.elementUUID === nextEvent.elementUUID) {
        eventQueue.shift();
      } else {
        break;
      }
    }

    const event = eventQueue.shift();
    const state = getState();

    preprocessBrowserEvent(event);

    if (state.messages.length > 0) {
      const lastMessage = state.messages[state.messages.length - 1];
      if (lastMessage.type === 'Interaction') {
        const lastInteraction = JSON.parse(lastMessage.content);
        if (lastInteraction.type === "input" && lastInteraction.elementUUID === event.elementUUID) {
          lastInteraction.typedText = event.typedText;
          state.messages[state.messages.length - 1] = {
            type: 'Interaction',
            content: JSON.stringify(lastInteraction),
            windowUrl: event.windowUrl,
          };
          updateState(page, state);
          return;
        }
      }
    }

    state.messages.push({
      type: 'Interaction',
      content: JSON.stringify(event),
      windowUrl: event.windowUrl,
    });
    updateState(page, state);
  }, 100, { maxWait: 500 });

  return (event: any) => {
    const state = getState();
    if (!state.recordingInteractions || state.pickingType) {
      return;
    }

    eventQueue.push(event);
    processEvents();
  }
}

```

--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------

```typescript
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"

import { cn } from "@/lib/utils"

function Tabs({
  className,
  ...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
  return (
    <TabsPrimitive.Root
      data-slot="tabs"
      className={cn("flex flex-col gap-2", className)}
      {...props}
    />
  )
}

function TabsList({
  className,
  ...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
  return (
    <TabsPrimitive.List
      data-slot="tabs-list"
      className={cn(
        "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
        className
      )}
      {...props}
    />
  )
}

function TabsTrigger({
  className,
  ...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
  return (
    <TabsPrimitive.Trigger
      data-slot="tabs-trigger"
      className={cn(
        "data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/50 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function TabsContent({
  className,
  ...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
  return (
    <TabsPrimitive.Content
      data-slot="tabs-content"
      className={cn("flex-1 outline-none", className)}
      {...props}
    />
  )
}

export { Tabs, TabsList, TabsTrigger, TabsContent }

```

--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------

```typescript
import * as React from "react"

import { cn } from "@/lib/utils"

function Card({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card"
      className={cn(
        "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
        className
      )}
      {...props}
    />
  )
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-header"
      className={cn(
        "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-[data-slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
        className
      )}
      {...props}
    />
  )
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-title"
      className={cn("leading-none font-semibold", className)}
      {...props}
    />
  )
}

function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-description"
      className={cn("text-muted-foreground text-sm", className)}
      {...props}
    />
  )
}

function CardAction({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-action"
      className={cn(
        "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
        className
      )}
      {...props}
    />
  )
}

function CardContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-content"
      className={cn("px-6", className)}
      {...props}
    />
  )
}

function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-footer"
      className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
      {...props}
    />
  )
}

export {
  Card,
  CardHeader,
  CardFooter,
  CardTitle,
  CardAction,
  CardDescription,
  CardContent,
}

```

--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------

```typescript
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
        destructive:
          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
        outline:
          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
        ghost:
          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2 has-[>svg]:px-3",
        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
        icon: "size-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function Button({
  className,
  variant,
  size,
  asChild = false,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean
  }) {
  const Comp = asChild ? Slot : "button"

  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "playwright-mcp",
  "version": "0.0.11",
  "description": "Playwright integration for ModelContext",
  "type": "module",
  "main": "dist/server.js",
  "bin": {
    "playwright-mcp": "dist/server.js"
  },
  "scripts": {
    "dev": "concurrently \"npm run dev:ui\" \"npm run dev:lib\"",
    "dev:ui": "vite --config vite.config.js",
    "dev:lib": "tsup --watch",
    "build": "NODE_ENV=production tsup && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.7.0",
    "@radix-ui/react-dropdown-menu": "^2.1.6",
    "@radix-ui/react-scroll-area": "^1.2.3",
    "@radix-ui/react-slot": "^1.1.2",
    "@radix-ui/react-tabs": "^1.1.3",
    "@skorotkiewicz/snowflake-id": "^1.0.1",
    "@tailwindcss/vite": "^4.0.14",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "happy-dom": "^17.4.4",
    "lodash": "^4.17.21",
    "lucide-react": "^0.482.0",
    "playwright": "^1.51.0",
    "prismjs": "^1.30.0",
    "react": "^19.0.0",
    "react-contenteditable": "^3.3.7",
    "react-dom": "^19.0.0",
    "react-simple-code-editor": "^0.14.1",
    "react-usestateref": "^1.0.9",
    "tailwind-merge": "^3.0.2",
    "tailwindcss": "^4.0.14",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@eslint/js": "^9.21.0",
    "@types/jsdom": "^21.1.7",
    "@types/lodash": "^4.17.16",
    "@types/node": "^22.13.10",
    "@types/prismjs": "^1.26.5",
    "@types/react": "^19.0.10",
    "@types/react-dom": "^19.0.4",
    "@vitejs/plugin-react": "^4.3.4",
    "concurrently": "^9.1.2",
    "eslint": "^9.21.0",
    "eslint-plugin-react-hooks": "^5.1.0",
    "eslint-plugin-react-refresh": "^0.4.19",
    "globals": "^15.15.0",
    "tsup": "^8.4.0",
    "typescript": "~5.7.2",
    "typescript-eslint": "^8.24.1",
    "vite": "^6.2.0"
  },
  "keywords": [
    "playwright",
    "mcp",
    "modelcontextprotocol",
    "test automation",
    "playwright-mcp"
  ],
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Ashish-Bansal/playwright-mcp.git"
  },
  "author": "Ashish Bansal",
  "publishConfig": {
    "access": "public"
  }
}

```

--------------------------------------------------------------------------------
/src/mcp/eval.ts:
--------------------------------------------------------------------------------

```typescript
import { Page } from 'playwright';
import vm from 'vm';


// Not super secure, but it's ok for now
export const secureEvalAsync = async (page: Page, code: string, context = {}) => {
  // Set default options
  const timeout = 20000;
  const filename = 'eval.js';

  let logs: string[] = [];
  let errors: string[] = [];

  // Code should already be a function declaration
  // Just need to execute it with page argument
  const wrappedCode = `
    ${code}
    run(page);
  `;

  // Create restricted sandbox with provided context
  const sandbox = {
    // Core async essentials
    Promise,
    setTimeout,
    clearTimeout,
    setImmediate,
    clearImmediate,

    // Pass page object to sandbox
    page,

    // Capture all console methods
    console: {
      log: (...args: any[]) => {
        const msg = args.map(arg => String(arg)).join(' ');
        logs.push(`[log] ${msg}`);
      },
      error: (...args: any[]) => {
        const msg = args.map(arg => String(arg)).join(' ');
        errors.push(`[error] ${msg}`);
      },
      warn: (...args: any[]) => {
        const msg = args.map(arg => String(arg)).join(' ');
        logs.push(`[warn] ${msg}`);
      },
      info: (...args: any[]) => {
        const msg = args.map(arg => String(arg)).join(' ');
        logs.push(`[info] ${msg}`);
      },
      debug: (...args: any[]) => {
        const msg = args.map(arg => String(arg)).join(' ');
        logs.push(`[debug] ${msg}`);
      },
      trace: (...args: any[]) => {
        const msg = args.map(arg => String(arg)).join(' ');
        logs.push(`[trace] ${msg}`);
      }
    },

    // User-provided context
    ...context,

    // Explicitly block access to sensitive globals
    process: undefined,
    global: undefined,
    require: undefined,
    __dirname: undefined,
    __filename: undefined,
    Buffer: undefined
  };

  try {
    // Create context and script
    const vmContext = vm.createContext(sandbox);
    const script = new vm.Script(wrappedCode, { filename });

    // Execute and await result
    const result = script.runInContext(vmContext);
    const awaitedResult = await result;

    return {
      result: awaitedResult,
      logs,
      errors
    };

  } catch (error: any) {
    return {
      error: true,
      message: error.message,
      stack: error.stack,
      logs,
      errors
    };
  }
}

```

--------------------------------------------------------------------------------
/src/mcp/recording/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { BrowserEvent, BrowserEventType } from "./events.js";
import { getSelectors } from "./selector-engine.js";
import { Window } from "happy-dom";

const parseDom = (html: string) => {
  const window = new Window({
    settings: {
      disableJavaScriptEvaluation: true
    }
  });
  window.document.write(html);
  return window.document as unknown as Document;
}

export const preprocessBrowserEvent = (event: BrowserEvent) => {
  if (
    event.type === BrowserEventType.Click ||
    event.type === BrowserEventType.Input
  ) {
    const dom = parseDom(event.dom)
    event.selectors = getSelectors(dom, event.elementUUID);

    const element = dom.querySelector(`[uuid="${event.elementUUID}"]`)
    event.elementName = element ? getElementName(element) : "unknown"
    event.elementType = element ? getElementType(element) : "unknown"
    // for efficiency, we don't need to preserve it for now
    event.dom = ''
  }
}

const extractText = (element: Element): string => {
  if (element.childNodes.length === 0) {
    return element.textContent?.trim() || ''
  }

  const texts = Array.from(element.childNodes).map((node) =>
    extractText(node as unknown as Element),
  )
  return texts
    .filter((text) => text.trim().length > 0)
    .map((text) => text.trim())
    .join('\n')
}

const extractTextsFromSiblings = (element: Element): string[] => {
  const siblings = Array.from(element.parentElement?.childNodes || [])
  return siblings
    .map((sibling) => extractText(sibling as unknown as Element))
    .map((text) => text.trim())
    .filter((text) => text.length > 0)
}

const getElementName = (element: Element) => {
  let text = ''
  const priorityAttrs = ['aria-label', 'title', 'placeholder', 'name', 'alt']
  for (const attr of priorityAttrs) {
    if (!text) {
      text = element?.getAttribute(attr) || ''
    }
  }
  if (!text) {
    text = extractText(element)
  }
  if (!text) {
    text = extractTextsFromSiblings(element).join('\n')
  }
  if (!text) {
    text = "unknown"
  }
  return text
}

const getElementType = (element: Element) => {
  const tagName = element?.tagName.toLowerCase()
  let elementType: 'button' | 'link' | 'input' | 'textarea' | 'element' =
    'element'
  if (tagName === 'a') {
    elementType = 'link'
  } else if (tagName === 'button') {
    elementType = 'button'
  } else if (tagName === 'textarea') {
    elementType = 'textarea'
  } else if (tagName === 'input') {
    elementType = 'input'
  }
  return elementType
}

```

--------------------------------------------------------------------------------
/src/App/execute.tsx:
--------------------------------------------------------------------------------

```typescript
import React, { useState } from 'react';
import { Play } from 'lucide-react';
import Editor from 'react-simple-code-editor';
import { highlight, languages } from 'prismjs';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import 'prismjs/themes/prism.css';
import { Button } from '@/components/ui/button';
import { useGlobalState } from '@/hooks/use-global-stage';

const Execute: React.FC = () => {
  const [state, updateState] = useGlobalState();
  const [result, setResult] = useState<string>('');
  const [error, setError] = useState<string>('');
  const [logs, setLogs] = useState<string[]>([]);

  const executeCode = async () => {
    try {
      const response = await (window as any).executeCode(state.code);
      if (response.error) {
        setError(response.message);
        setResult('');
      } else {
        setResult(JSON.stringify(response.result));
        setLogs(response.logs);
        setError('');
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
      setResult('');
      setLogs([]);
    }
  };

  return (
    <div className="flex-1 flex flex-col">
      <div className="p-4 bg-white border-b border-zinc-200">
        <Editor
          value={state.code}
          onValueChange={code => updateState({ ...state, code })}
          highlight={code => highlight(code, languages.js, 'javascript')}
          padding={10}
          className="font-mono mb-4 min-h-[200px] border rounded-md"
          style={{
            fontFamily: '"Fira code", "Fira Mono", monospace',
            fontSize: 14,
          }}
        />
        <Button
          onClick={executeCode}
          variant="outline"
          className="gap-2"
        >
          <Play size={20} />
          <span>Execute</span>
        </Button>
      </div>
      <div className="flex-1 p-4 space-y-4">
        {error && (
          <div className="bg-red-50 border border-red-200 rounded-md p-4">
            <div className="font-medium text-red-800 mb-1">Error</div>
            <div className="text-red-600 font-mono text-sm whitespace-pre-wrap">
              {error}
            </div>
          </div>
        )}
        {result && (
          <div className="bg-green-50 border border-green-200 rounded-md p-4">
            <div className="font-medium text-green-800 mb-1">Result</div>
            <div className="text-green-700 font-mono text-sm whitespace-pre-wrap">
              {result}
            </div>
          </div>
        )}
        {logs.length > 0 && (
          <div className="bg-gray-50 border border-gray-200 rounded-md p-4">
            <div className="font-medium text-gray-800 mb-2">Logs</div>
            <div className="space-y-1">
              {logs.map((log, i) => (
                <div key={i} className="font-mono text-sm text-gray-600 flex items-start">
                  <span className="text-gray-400 mr-2">{`>`}</span>
                  <span className="whitespace-pre-wrap">{log}</span>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default Execute;

```

--------------------------------------------------------------------------------
/src/web-server.ts:
--------------------------------------------------------------------------------

```typescript
import http from 'http';
import fs from 'fs';
import path from 'path';
import url, { fileURLToPath } from 'url';
import { dirname } from 'path';
import net from 'net';

// Get the current file's path
const __filename = fileURLToPath(import.meta.url);
// Get the current directory
const __dirname = dirname(__filename);

// Define the directory from which to serve files
const SERVE_DIR = path.join(__dirname, 'ui'); // Change 'public' to your desired directory name

// Helper function to check if port is in use
async function isPortInUse(port: number): Promise<boolean> {
  return new Promise((resolve) => {
    const tester = net.createServer()
      .once('error', () => resolve(true))
      .once('listening', () => {
        tester.once('close', () => resolve(false));
        tester.close();
      })
      .listen(port);
  });
}

// Create HTTP server
const server = http.createServer((req, res) => {
  // Parse URL to get the file path
  const parsedUrl = url.parse(req.url || '');
  const pathname = parsedUrl.pathname || '/';

  // Resolve to absolute path within SERVE_DIR only
  let filePath = path.join(SERVE_DIR, pathname);

  // Security check: ensure the file path is within SERVE_DIR
  if (!filePath.startsWith(SERVE_DIR)) {
    res.writeHead(403, { 'Content-Type': 'text/plain' });
    res.end('403 Forbidden: Access denied');
    return;
  }

  // If path ends with '/', serve index.html from that directory
  if (pathname.endsWith('/')) {
    filePath = path.join(filePath, 'index.html');
  }

  // Check if file exists
  fs.stat(filePath, (err, stats) => {
    if (err) {
      // File not found
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('404 Not Found');
      return;
    }

    // If it's a directory, attempt to serve index.html
    if (stats.isDirectory()) {
      filePath = path.join(filePath, 'index.html');
      fs.stat(filePath, (err, stats) => {
        if (err) {
          res.writeHead(404, { 'Content-Type': 'text/plain' });
          res.end('404 Not Found');
          return;
        }
        serveFile(filePath, res);
      });
    } else {
      // It's a file, serve it
      serveFile(filePath, res);
    }
  });
});

// Helper function to serve a file
function serveFile(filePath: string, res: http.ServerResponse): void {
  // Get file extension to set correct content type
  const ext = path.extname(filePath);
  let contentType = 'text/plain';

  switch (ext) {
    case '.html':
      contentType = 'text/html';
      break;
    case '.css':
      contentType = 'text/css';
      break;
    case '.js':
      contentType = 'application/javascript';
      break;
    case '.json':
      contentType = 'application/json';
      break;
    case '.png':
      contentType = 'image/png';
      break;
    case '.jpg':
    case '.jpeg':
      contentType = 'image/jpeg';
      break;
    case '.gif':
      contentType = 'image/gif';
      break;
    case '.svg':
      contentType = 'image/svg+xml';
      break;
    case '.pdf':
      contentType = 'application/pdf';
      break;
  }

  // Read and serve the file
  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.writeHead(500, { 'Content-Type': 'text/plain' });
      res.end('Internal Server Error');
      return;
    }

    res.writeHead(200, { 'Content-Type': contentType });
    res.end(data);
  });
}

export { server as webServer, isPortInUse };

```

--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------

```
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
```

--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------

```css
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap");

@import "tailwindcss";

@plugin "tailwindcss-animate";

@custom-variant dark (&:is(.dark *));


body {
  font-family: "Plus Jakarta Sans", sans-serif;
}

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
}

@theme inline {
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);
  --color-sidebar: var(--sidebar);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-ring: var(--sidebar-ring);
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }
}

```

--------------------------------------------------------------------------------
/src/mcp/index.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { chromium, BrowserContext, Browser, Page } from "playwright";
import { injectToolbox } from "./toolbox.js";
import { secureEvalAsync } from "./eval.js";
import { initState, getState, updateState, type Message } from "./state.js";
import { initRecording } from "./recording";
import { handleBrowserEvent } from "./handle-browser-event.js";

let browser: Browser;
let context: BrowserContext;
let page: Page;


const server = new McpServer({
  name: "playwright",
  version: "1.0.0",
});

server.prompt(
  "server-flow",
  "Get prompt on how to use this MCP server",
  () => {
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `# DON'T ASSUME ANYTHING. Whatever you write in code, it must be found in the context. Otherwise leave comments.

## Goal
Help me write playwright code with following functionalities:
- [[add semi-high level functionality you want here]]
- [[more]]
- [[more]]
- [[more]]

## Reference
- Use @x, @y files if you want to take reference on how I write POM code

## Steps
- First fetch the context from 'get-context' tool, until it returns no elements remaining
- Based on context and user functionality, write code in POM format, encapsulating high level functionality into reusable functions
- Try executing code using 'execute-code' tool. You could be on any page, so make sure to navigate to the correct page
- Write spec file using those reusable functions, covering multiple scenarios
`
          }
        }
      ]
    };
  }
);


server.tool(
  'init-browser',
  'Initialize a browser with a URL',
  {
    url: z.string().url().describe('The URL to navigate to')
  },
  async ({ url }) => {
    if (context) {
      await context.close();
    }
    if (browser) {
      await browser.close();
    }

    browser = await chromium.launch({
      headless: false,
    });
    context = await browser.newContext({
      viewport: null,
      userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
      bypassCSP: true,
    });
    page = await context.newPage();

    await page.exposeFunction('triggerMcpStartPicking', (pickingType: 'DOM' | 'Image') => {
      page.evaluate((pickingType: 'DOM' | 'Image') => {
        window.mcpStartPicking(pickingType);
      }, pickingType);
    });

    await page.exposeFunction('triggerMcpStopPicking', () => {
      page.evaluate(() => {
        window.mcpStopPicking();
      });
    });

    await page.exposeFunction('onElementPicked', (message: Message) => {
      const state = getState();
      state.messages.push(message);
      state.pickingType = null;
      updateState(page, state);
    });

    await page.exposeFunction('takeScreenshot', async (selector: string) => {
      try {
        const screenshot = await page.locator(selector).screenshot({
          timeout: 5000
        });
        return screenshot.toString('base64');
      } catch (error) {
        console.error('Error taking screenshot', error);
        return null;
      }
    });

    await page.exposeFunction('executeCode', async (code: string) => {
      const result = await secureEvalAsync(page, code);
      return result;
    });

    await initState(page);
    await initRecording(page, handleBrowserEvent(page));

    await page.addInitScript(injectToolbox);
    await page.goto(url);

    return {
      content: [
        {
          type: "text",
          text: `Browser has been initialized and navigated to ${url}`,
        },
      ],
    };
  }
)

server.tool(
  "get-full-dom",
  "Get the full DOM of the current page. (Deprecated, use get-context instead)",
  {},
  async () => {
    const html = await page.content();
    return {
      content: [
        {
          type: "text",
          text: html,
        },
      ],
    };
  }
);

server.tool(
  'get-screenshot',
  'Get a screenshot of the current page',
  {},
  async () => {
    const screenshot = await page.screenshot({
      type: "png",
    });
    return {
      content: [
        {
          type: "image",
          data: screenshot.toString('base64'),
          mimeType: "image/png",
        },
      ],
    };
  }
)

server.tool(
  'execute-code',
  'Execute custom Playwright JS code against the current page',
  {
    code: z.string().describe(`The Playwright code to execute. Must be an async function declaration that takes a page parameter.

Example:
async function run(page) {
  console.log(await page.title());
  return await page.title();
}

Returns an object with:
- result: The return value from your function
- logs: Array of console logs from execution
- errors: Array of any errors encountered

Example response:
{"result": "Google", "logs": ["[log] Google"], "errors": []}`)
  },
  async ({ code }) => {
    const result = await secureEvalAsync(page, code);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(result, null, 2) // Pretty print the JSON
        }
      ]
    };
  }
)

server.tool(
  "get-context",
  "Get the website context which would be used to write the testcase",
  {},
  async () => {
    const state = getState();

    if (state.messages.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: `No messages available`
          }
        ]
      };
    }

    const content: any = [];

    let totalLength = 0;
    let messagesProcessed = 0;

    while (messagesProcessed < state.messages.length && totalLength < 20000) {
      const message = state.messages[messagesProcessed];
      let currentContent = message.content
      if (message.type === 'DOM') {
        currentContent = `DOM: ${message.content}`;
      } else if (message.type === 'Text') {
        currentContent = `Text: ${message.content}`;
      } else if (message.type === 'Interaction') {
        const interaction = JSON.parse(message.content);
        delete interaction.eventId;
        delete interaction.dom;
        delete interaction.elementUUID;
        if (interaction.selectors) {
          interaction.selectors = interaction.selectors.slice(0, 10);
        }

        currentContent = JSON.stringify(interaction);
      } else if (message.type === 'Image') {
        currentContent = message.content;
      }

      totalLength += currentContent.length;

      const item: any = {}
      const isImage = message.type === 'Image';
      if (isImage) {
        item.type = "image";
        item.data = message.content;
        item.mimeType = "image/png";
      } else {
        item.type = "text";
        item.text = currentContent;
      }
      content.push(item);
      messagesProcessed++;
    }

    // Remove processed messages
    state.messages.splice(0, messagesProcessed);
    updateState(page, state);

    const remainingCount = state.messages.length;
    if (remainingCount > 0) {
      content.push({
        type: "text",
        text: `Remaining ${remainingCount} messages, please fetch those in next requests.\n`
      });
    }

    return {
      content
    };
  }
);

export { server }

```

--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------

```typescript
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"

import { cn } from "@/lib/utils"

function DropdownMenu({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
  return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}

function DropdownMenuPortal({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
  return (
    <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
  )
}

function DropdownMenuTrigger({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
  return (
    <DropdownMenuPrimitive.Trigger
      data-slot="dropdown-menu-trigger"
      {...props}
    />
  )
}

function DropdownMenuContent({
  className,
  sideOffset = 4,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
  return (
    <DropdownMenuPrimitive.Portal>
      <DropdownMenuPrimitive.Content
        data-slot="dropdown-menu-content"
        sideOffset={sideOffset}
        className={cn(
          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
          className
        )}
        {...props}
      />
    </DropdownMenuPrimitive.Portal>
  )
}

function DropdownMenuGroup({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
  return (
    <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
  )
}

function DropdownMenuItem({
  className,
  inset,
  variant = "default",
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
  inset?: boolean
  variant?: "default" | "destructive"
}) {
  return (
    <DropdownMenuPrimitive.Item
      data-slot="dropdown-menu-item"
      data-inset={inset}
      data-variant={variant}
      className={cn(
        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function DropdownMenuCheckboxItem({
  className,
  children,
  checked,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
  return (
    <DropdownMenuPrimitive.CheckboxItem
      data-slot="dropdown-menu-checkbox-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      checked={checked}
      {...props}
    >
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
        <DropdownMenuPrimitive.ItemIndicator>
          <CheckIcon className="size-4" />
        </DropdownMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </DropdownMenuPrimitive.CheckboxItem>
  )
}

function DropdownMenuRadioGroup({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
  return (
    <DropdownMenuPrimitive.RadioGroup
      data-slot="dropdown-menu-radio-group"
      {...props}
    />
  )
}

function DropdownMenuRadioItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
  return (
    <DropdownMenuPrimitive.RadioItem
      data-slot="dropdown-menu-radio-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
        <DropdownMenuPrimitive.ItemIndicator>
          <CircleIcon className="size-2 fill-current" />
        </DropdownMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </DropdownMenuPrimitive.RadioItem>
  )
}

function DropdownMenuLabel({
  className,
  inset,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
  inset?: boolean
}) {
  return (
    <DropdownMenuPrimitive.Label
      data-slot="dropdown-menu-label"
      data-inset={inset}
      className={cn(
        "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
        className
      )}
      {...props}
    />
  )
}

function DropdownMenuSeparator({
  className,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
  return (
    <DropdownMenuPrimitive.Separator
      data-slot="dropdown-menu-separator"
      className={cn("bg-border -mx-1 my-1 h-px", className)}
      {...props}
    />
  )
}

function DropdownMenuShortcut({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="dropdown-menu-shortcut"
      className={cn(
        "text-muted-foreground ml-auto text-xs tracking-widest",
        className
      )}
      {...props}
    />
  )
}

function DropdownMenuSub({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
  return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}

function DropdownMenuSubTrigger({
  className,
  inset,
  children,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
  inset?: boolean
}) {
  return (
    <DropdownMenuPrimitive.SubTrigger
      data-slot="dropdown-menu-sub-trigger"
      data-inset={inset}
      className={cn(
        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
        className
      )}
      {...props}
    >
      {children}
      <ChevronRightIcon className="ml-auto size-4" />
    </DropdownMenuPrimitive.SubTrigger>
  )
}

function DropdownMenuSubContent({
  className,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
  return (
    <DropdownMenuPrimitive.SubContent
      data-slot="dropdown-menu-sub-content"
      className={cn(
        "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
        className
      )}
      {...props}
    />
  )
}

export {
  DropdownMenu,
  DropdownMenuPortal,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuLabel,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubTrigger,
  DropdownMenuSubContent,
}

```

--------------------------------------------------------------------------------
/src/mcp/recording/selector-engine.ts:
--------------------------------------------------------------------------------

```typescript
import { logger } from '../logger'

const ATTR_PRIORITIES: Record<string, number> = {
  id: 1,
  'data-testid': 2,
  'data-test-id': 2,
  'data-pw': 2,
  'data-cy': 2,
  'data-id': 2,
  'data-name': 3,
  name: 3,
  'aria-label': 3,
  title: 3,
  placeholder: 4,
  href: 4,
  alt: 4,
  'data-index': 5,
  'data-role': 5,
  role: 5,
}

const IMPORTANT_ATTRS = Object.keys(ATTR_PRIORITIES)

const _escapeSpecialCharacters = (str: string): string => {
  // Only escape double quotes for CSS selectors
  return str.replace(/"/g, '\\"')
}

const getNodeSimpleSelectors = (element: Element): string[] => {
  const selectors: string[] = []
  const tag = element.tagName.toLowerCase()

  const attrSelectors = IMPORTANT_ATTRS.map((attr) => {
    const value = element.getAttribute(attr)
    if (!value) return null
    return {
      priority: ATTR_PRIORITIES[attr] || 999,
      selector:
        attr === 'id'
          ? `#${_escapeSpecialCharacters(value)}`
          : `${tag}[${attr}="${_escapeSpecialCharacters(value)}"]`,
    }
  }).filter((item) => item !== null)

  const otherSelectors = []

  // Locate by class
  const classList = element.classList
  if (classList.length > 0) {
    otherSelectors.push({
      priority: 100,
      selector: `${tag}.${Array.from(classList).join('.')}`,
    })
  }

  const availableSelectors = [...attrSelectors, ...otherSelectors]
  availableSelectors.sort((a, b) => a!.priority - b!.priority)

  // Take top 5 selectors based on priority
  const topSelectors = availableSelectors.slice(0, 5)
  topSelectors.push({
    priority: 999,
    selector: tag,
  })

  // Add selectors in priority order
  for (const item of topSelectors) {
    selectors.push(item!.selector)
  }

  return selectors
}

const _getSiblingRelationshipSelectors = (dom: Document, element: Element): string[] => {
  const selectors: string[] = []
  const parent = element.parentElement
  if (!parent || parent.tagName === 'BODY') {
    return selectors
  }

  const siblings = Array.from(parent.children)
  const elementIndex = siblings.indexOf(element)
  const tagName = element.tagName.toLowerCase()

  const selectorPrefixes: string[] = []
  for (let i = 0; i < siblings.length; i++) {
    if (i === elementIndex) continue

    const sibling = siblings[i]
    const siblingSimpleSelectors = getNodeSimpleSelectors(sibling)
    siblingSimpleSelectors.forEach((siblingSelector) => {
      selectorPrefixes.push(`${siblingSelector} ~ `)
    })
  }

  const selectorSuffixes = [tagName, ...getNodeSimpleSelectors(element)]
  selectorSuffixes.forEach((selectorSuffix) => {
    selectorPrefixes.forEach((selectorPrefix) => {
      selectors.push(`${selectorPrefix}${selectorSuffix}`)
    })
  })

  return selectors
}

const _getChildRelationshipSelectors = (dom: Document, element: Element) => {
  // BFS to get all children and their depth upto level 3
  const children = []
  const currentQueue = Array.from(element.children).map((child) => ({
    child,
    depth: 0,
  }))
  while (currentQueue.length > 0) {
    const item = currentQueue.shift()
    if (!item) continue

    const { child, depth } = item
    if (depth > 3) {
      continue
    }

    children.push({ child, depth })
    currentQueue.push(
      ...Array.from(child.children).map((child) => ({
        child,
        depth: depth + 1,
      })),
    )
  }

  const selectorSuffixes: string[] = []
  children.forEach(({ child, depth }) => {
    const childSelectors = getNodeSimpleSelectors(child)
    const childIndex = Array.from(element.children).indexOf(child) + 1

    childSelectors.forEach((childSelector) => {
      if (depth === 0) {
        // For now, disable `>` immediate child selector, since that doesn't work properly.
        // In happy-dom, it's not supported - https://github.com/capricorn86/happy-dom/issues/1642
        // In jsdom, it's giving DOM exception
        // Example - Failed to validate selector
        //     div:has(> [data-testid="adult_count"])
        //     DOMException {}
        //     message = 'div.`makeFlex >[data-testid="adult_count"]' is not a valid selector
        //     code = 12
        // Selector for parent element, using :has() to indicate parent contains this specific child
        selectorSuffixes.push(`:has(${childSelector})`)
        // Also add nth-child variant for more specificity if needed
        selectorSuffixes.push(`:has(${childSelector}:nth-child(${childIndex}))`)
      } else {
        // Depth != 0, means it's a descendant, not direct child.
        selectorSuffixes.push(`:has(${childSelector})`)
      }
    })
  })

  const selectorPrefixes = [
    element.tagName.toLowerCase(),
    ...getNodeSimpleSelectors(element),
  ]

  const selectors: string[] = []
  selectorPrefixes.forEach((selectorPrefix) => {
    selectorSuffixes.forEach((selectorSuffix) => {
      selectors.push(`${selectorPrefix}${selectorSuffix}`)
    })
  })
  return selectors
}

const getMatchCount = (dom: Document, selector: string): number => {
  try {
    return dom.querySelectorAll(selector).length
  } catch {
    return Number.POSITIVE_INFINITY // Invalid selector
  }
}

const _getParentPathSelectors = (dom: Document, element: Element): string[] => {
  // Build path from target to root
  const path: Element[] = []
  let current: Element | null = element
  while (current && current.tagName !== 'HTML') {
    path.push(current)
    current = current.parentElement
  }

  logger.debug(
    'Path',
    path.map((node) => node.tagName),
  )

  // Pre-compute selectors for each node
  const nodeSelectors: {
    node: Element
    selectors: string[]
  }[] = path.map((node) => ({
    node,
    selectors: getNodeSimpleSelectors(node),
  }))
  if (!nodeSelectors.length) {
    return []
  }

  const result: string[] = []
  const targetNode = nodeSelectors[0].node
  const targetSelectors = nodeSelectors[0].selectors
  const targetSelectorsWithNthChild = targetSelectors.map((selector) => {
    const index =
      targetNode.parentElement
        ? Array.from(targetNode.parentElement.children).indexOf(targetNode) + 1
        : 1
    return `${selector}:nth-child(${index})`
  })
  const allTargetSelectors = [
    ...targetSelectors,
    ...targetSelectorsWithNthChild,
  ]
  logger.debug('Target Selectors', allTargetSelectors)

  for (const targetSelector of allTargetSelectors) {
    const matches = getMatchCount(dom, targetSelector)

    // Skip invalid selectors
    if (matches === 0) continue

    // If unique, add to results
    if (matches === 1) {
      result.push(targetSelector)
    }

    // Try combinations with ancestors
    let currentSelector = targetSelector
    let currentMatches = matches
    let lastAddedNode = targetNode

    for (let i = 1; i < nodeSelectors.length; i++) {
      const ancestor = nodeSelectors[i].node
      const ancestorSelectors = nodeSelectors[i].selectors
      let bestSelector: string | null = null
      let bestMatches = currentMatches

      for (const ancestorSelector of ancestorSelectors) {
        const descendantOperator =
          Array.from(ancestor.children).indexOf(lastAddedNode) !== -1
            ? ' > '
            : ' '
        const possibleCombinedSelectors = [
          `${ancestorSelector} ${descendantOperator} ${currentSelector}`,
        ]
        if (ancestor.tagName != 'BODY' && ancestor.parentElement) {
          const elementIndex =
            Array.from(ancestor.parentElement.children).indexOf(ancestor) + 1
          possibleCombinedSelectors.push(
            `${ancestorSelector}:nth-child(${elementIndex}) ${descendantOperator} ${currentSelector}`,
          )
        }

        logger.debug('Possible Combined Selectors', possibleCombinedSelectors)

        for (const combinedSelector of possibleCombinedSelectors) {
          const newMatches = getMatchCount(dom, combinedSelector)

          // Skip invalid combinations
          if (newMatches === 0) continue
          else if (newMatches === 1) {
            // If unique, add to results immediately
            result.push(combinedSelector)
            bestSelector = null // Skip updating current selector
          } else if (newMatches < bestMatches) {
            // Update best if it reduces matches
            bestSelector = combinedSelector
            bestMatches = newMatches
          }
        }
      }

      // Update current if we found a better (but not unique) selector
      if (bestSelector && bestMatches < currentMatches) {
        currentSelector = bestSelector
        currentMatches = bestMatches
        lastAddedNode = ancestor
      }
    }
  }

  return result
}


const validateSelector = (document: Document, element: Element, selector: string) => {
  try {
    const selectedElements = document.querySelectorAll(selector)
    return selectedElements.length === 1 && selectedElements[0] === element
  } catch (e) {
    return false
  }
}

const getSelectors = (document: Document, elementUUID: string): string[] => {
  const element = document.querySelector(`[uuid="${elementUUID}"]`)
  if (!element) {
    throw new Error(`Element with UUID ${elementUUID} not found`)
  }

  const validSelectors: string[] = []
  const selectorGenerators = [
    () => _getParentPathSelectors(document, element),
    () => _getChildRelationshipSelectors(document, element),
    () => _getSiblingRelationshipSelectors(document, element)
  ]

  for (const generator of selectorGenerators) {
    const selectors = generator()
    for (const selector of selectors) {
      if (validateSelector(document, element, selector)) {
        validSelectors.push(selector)
        if (validSelectors.length >= 10) {
          return validSelectors
        }
      }
    }
  }

  return validSelectors
}

export { getSelectors }

```

--------------------------------------------------------------------------------
/src/mcp/recording/init-recording.ts:
--------------------------------------------------------------------------------

```typescript
import {
  type BaseBrowserEvent,
  BrowserEventType,
  type ClickBrowserEvent,
  type InputBrowserEvent,
  type KeyPressBrowserEvent,
  type OpenPageBrowserEvent,
} from './events'
import type { Page } from 'playwright'
import { getSnowflakeId } from './snowflake'

export const initRecording = async (
  page: Page,
  onBrowserEvent: (event: BaseBrowserEvent) => void,
) => {
  page.addInitScript(() => {
    if (window.self !== window.top) {
      return;
    }

    function getDom(): string {
      const snapshot = document.documentElement.cloneNode(true) as HTMLElement

      // Handle all elements that need state preservation
      const originalElements = document.querySelectorAll<HTMLElement>('*')
      const clonedElements = snapshot.querySelectorAll<HTMLElement>('*')

      // Restore scroll positions in the clone
      originalElements.forEach((originalElement, index) => {
        const clonedElement = clonedElements[index]
        if (!clonedElement) return

        // Preserve scroll positions as data attributes
        if (originalElement.scrollLeft || originalElement.scrollTop) {
          if (originalElement.scrollLeft) {
            clonedElement.setAttribute(
              'qaby-data-scroll-left',
              originalElement.scrollLeft.toString(),
            )
          }
          if (originalElement.scrollTop) {
            clonedElement.setAttribute(
              'qaby-data-scroll-top',
              originalElement.scrollTop.toString(),
            )
          }
        }

        // Handle form elements
        if (
          originalElement instanceof HTMLInputElement ||
          originalElement instanceof HTMLTextAreaElement ||
          originalElement instanceof HTMLSelectElement ||
          originalElement.hasAttribute('contenteditable')
        ) {
          preserveElementState(originalElement, clonedElement)
        }
      })

      return snapshot.outerHTML
    }

    function preserveElementState(
      original: HTMLElement,
      cloned: HTMLElement,
    ): void {
      // Handle contenteditable elements
      if (original.hasAttribute('contenteditable')) {
        // Use innerHTML instead of textContent to preserve formatting
        // Escape HTML content before storing as attribute
        const escapedHTML = original.innerHTML
          .replace(/&/g, '&amp;')
          .replace(/"/g, '&quot;')
          .replace(/'/g, '&#39;')
          .replace(/</g, '&lt;')
          .replace(/>/g, '&gt;')
        cloned.setAttribute('qaby-data-contenteditable', escapedHTML)
      }

      // Handle form elements
      if (original instanceof HTMLInputElement) {
        preserveInputState(original, cloned as HTMLInputElement)
      } else if (original instanceof HTMLTextAreaElement) {
        preserveTextAreaState(original, cloned as HTMLTextAreaElement)
      } else if (original instanceof HTMLSelectElement) {
        preserveSelectState(original, cloned as HTMLSelectElement)
      }
    }

    function preserveInputState(
      original: HTMLInputElement,
      cloned: HTMLInputElement,
    ): void {
      switch (original.type) {
        case 'checkbox':
        case 'radio':
          if (original.checked) {
            cloned.setAttribute('checked', '')
          } else {
            cloned.removeAttribute('checked')
          }
          if (original.indeterminate) {
            cloned.setAttribute('qaby-data-indeterminate', 'true')
          }
          break
        case 'range':
          cloned.setAttribute('value', original.value)
          break
        case 'date':
        case 'datetime-local':
        case 'month':
        case 'time':
        case 'week':
          if (original.valueAsDate) {
            cloned.setAttribute(
              'qaby-data-value-as-date',
              original.valueAsDate.toISOString(),
            )
            cloned.setAttribute('value', original.value)
          }
          break
        default:
          // For text, email, password, etc.
          cloned.setAttribute('value', original.value)
      }
    }

    function preserveTextAreaState(
      original: HTMLTextAreaElement,
      cloned: HTMLTextAreaElement,
    ): void {
      cloned.innerHTML = original.value
    }

    function preserveSelectState(
      original: HTMLSelectElement,
      cloned: HTMLSelectElement,
    ): void {
      // First remove any existing selected attributes
      cloned.querySelectorAll('option').forEach((option) => {
        option.removeAttribute('selected')
      })

      if (original.multiple) {
        // For multi-select, preserve selected state of each option
        Array.from(original.selectedOptions).forEach((option) => {
          // Find the corresponding option in cloned select by index
          const optionIndex = Array.from(original.options).indexOf(option)
          // Use querySelector instead of options property
          const clonedOption = cloned.querySelector(
            `option:nth-child(${optionIndex + 1})`,
          )
          if (clonedOption) {
            clonedOption.setAttribute('selected', '')
          }
        })
      } else if (original.selectedIndex >= 0) {
        const clonedOption = cloned.querySelector(
          `option:nth-child(${original.selectedIndex + 1})`,
        )
        if (clonedOption) {
          clonedOption.setAttribute('selected', '')
        }
      }
    }

    function generateUUID(): string {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
        const r = (Math.random() * 16) | 0
        const v = c === 'x' ? r : (r & 0x3) | 0x8
        return v.toString(16)
      })
    }

    function addAttributesToNode(node: Node): void {
      if (node.nodeType === window.Node.ELEMENT_NODE) {
        const element = node as unknown as Element
        if (!element.getAttribute('uuid')) {
          element.setAttribute('uuid', generateUUID())
        }
        for (const child of node.childNodes) {
          addAttributesToNode(child)
        }
      }
    }

    function removeAttributesFromNode(node: Node): void {
      if (node.nodeType === window.Node.ELEMENT_NODE) {
        const element = node as unknown as Element
        element.removeAttribute('uuid')
      }
    }

    // Event handlers
    const recordedEvents = new WeakMap<MouseEvent, boolean>();

    function handleClick(e: MouseEvent): void {
      // Check if event was already recorded
      if (recordedEvents.get(e)) {
        return;
      }

      const target = e.target as Element;
      if (!target) return;
      if (target.getAttribute('data-skip-recording')) return;

      e.stopPropagation();
      recordedEvents.set(e, true);

      addAttributesToNode(document.documentElement);
      const elementUUID = target.getAttribute('uuid');
      const dom = getDom();

      window.recordDOM(dom, elementUUID as string).then(() => {
        // Re-dispatch the event after recording
        target.dispatchEvent(e);
      });
      removeAttributesFromNode(document.documentElement);
    }

    function handleKeyDown(event: KeyboardEvent): void {
      const dom = getDom()

      if (['Enter', 'Escape'].includes(event.key)) {
        window.recordKeyPress(dom, [event.key])
        return
      }

      if (document.activeElement?.tagName.toLowerCase() === 'input') {
        if (event.key === 'Tab') {
          window.recordKeyPress(dom, [event.key])
        }
        return
      }

      if (document.activeElement?.tagName.toLowerCase() === 'textarea') {
        return
      }

      const keys: string[] = []
      if (event.ctrlKey) keys.push('Control')
      if (event.shiftKey) keys.push('Shift')
      if (event.altKey) keys.push('Alt')
      if (event.metaKey) keys.push('Meta')

      if (!['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) {
        keys.push(event.key)
      }

      if (keys.includes('Meta') && keys.includes('Tab')) {
        return
      }

      if (keys.length === 1 && keys[0] === 'Meta') {
        return
      }

      if (keys.length > 0) {
        window.recordKeyPress(dom, keys)
      }
    }

    function handleKeyUp(event: KeyboardEvent): void {
      if (['Enter', 'Escape'].includes(event.key)) {
        return;
      }

      if (
        document.activeElement?.tagName.toLowerCase() !== 'input' &&
        document.activeElement?.tagName.toLowerCase() !== 'textarea'
      ) {
        return
      }

      const dom = getDom()
      window.recordInput(
        dom,
        document.activeElement.getAttribute('uuid') as string,
        (document.activeElement as HTMLInputElement).value,
      )
    }

    window.addEventListener('click', handleClick, { capture: true })
    window.addEventListener('keydown', handleKeyDown, { capture: true })
    window.addEventListener('keyup', handleKeyUp, { capture: true })

    console.log('Recording initialized for window:', window.location.href)
  })

  let buttonClicked = false
  await page.exposeFunction(
    'recordDOM',
    async (dom: string, elementUUID: string) => {
      buttonClicked = true
      const event: ClickBrowserEvent = {
        eventId: await getSnowflakeId(),
        type: BrowserEventType.Click,
        dom,
        elementUUID,
        selectors: [`[uuid="${elementUUID}"]`],
        windowUrl: page.url(),
      }
      onBrowserEvent(event)
    },
  )

  await page.exposeFunction(
    'recordInput',
    async (dom: string, elementUUID: string, value: string) => {
      const event: InputBrowserEvent = {
        eventId: await getSnowflakeId(),
        type: BrowserEventType.Input,
        dom,
        elementUUID,
        typedText: value,
        selectors: [`[uuid="${elementUUID}"]`],
        windowUrl: page.url(),
      }
      onBrowserEvent(event)
    },
  )

  await page.exposeFunction(
    'recordKeyPress',
    async (dom: string, keys: string[]) => {
      const event: KeyPressBrowserEvent = {
        eventId: await getSnowflakeId(),
        type: BrowserEventType.KeyPress,
        keys,
        dom,
        windowUrl: page.url(),
      }
      onBrowserEvent(event)
    },
  )

  page.on('load', async () => {
    if (!buttonClicked) {
      const event: OpenPageBrowserEvent = {
        eventId: await getSnowflakeId(),
        type: BrowserEventType.OpenPage,
        windowUrl: page.url(),
        // TODO: Fix navigation handling
        // title: await page.title(),
        title: '',
        // TODO: Fix dom content here
        dom: '',
      }
      onBrowserEvent(event)
    }

    buttonClicked = false
  })
}

```

--------------------------------------------------------------------------------
/src/mcp/toolbox.ts:
--------------------------------------------------------------------------------

```typescript
interface PickingState {
  activePickingType: 'DOM' | 'Image' | null;
  mouseMoveHandler: ((e: MouseEvent) => void) | null;
  clickHandler: ((e: MouseEvent) => void) | null;
}

export const injectToolbox = () => {
  window.addEventListener('DOMContentLoaded', function() {
    const inIframe = window.self !== window.top;
    if (inIframe) {
      return;
    }

    // Create sidebar if it doesn't exist
    if (document.querySelector('#mcp-sidebar')) {
      return;
    }

    const pickingState: PickingState = {
      activePickingType: null,
      mouseMoveHandler: null,
      clickHandler: null
    };

    const toggleSidebar = (expanded: boolean) => {
      const sidebar = document.querySelector('#mcp-sidebar') as HTMLElement;
      const toggleButton = document.querySelector('#mcp-sidebar-toggle-button') as HTMLElement;
      if (sidebar && toggleButton) {
        const width = parseInt(sidebar.style.width);
        sidebar.style.transform = expanded ? 'translateX(0)' : `translateX(${width}px)`;
        toggleButton.style.right = expanded ? `${width}px` : '0';
        toggleButton.textContent = expanded ? '⟩' : '⟨';
        localStorage.setItem('mcp-sidebar-expanded', expanded.toString());
      }
    };

    const mcpStopPicking = () => {
      // Stop picking
      if (pickingState.mouseMoveHandler) {
        document.removeEventListener('mousemove', pickingState.mouseMoveHandler);
      }
      if (pickingState.clickHandler) {
        document.removeEventListener('click', pickingState.clickHandler, true);
      }
      // Remove preview overlay if it exists
      const previewOverlay = document.querySelector('#mcp-highlight-overlay-preview');
      if (previewOverlay) {
        previewOverlay.remove();
      }
      pickingState.activePickingType = null;

      // Restore sidebar state
      toggleSidebar(true);
    };

    const mcpStartPicking = (pickingType: 'DOM' | 'Image') => {
      pickingState.activePickingType = pickingType;

      // Collapse sidebar when picking starts
      toggleSidebar(false);

      pickingState.mouseMoveHandler = (e: MouseEvent) => {
        const element = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement;
        const sidebar = document.querySelector('#mcp-sidebar');
        const expandButton = document.querySelector('#mcp-sidebar-toggle-button');
        if (!element ||
          (sidebar && sidebar.contains(element)) ||
          (expandButton && expandButton.contains(element)) ||
          element.closest('[id^="mcp-highlight-overlay"]')) return;

        // Create or update highlight overlay
        let overlay: HTMLElement | null = document.querySelector('#mcp-highlight-overlay-preview');
        if (!overlay) {
          overlay = document.createElement('div');
          overlay.id = 'mcp-highlight-overlay-preview';
          overlay.style.cssText = `
            position: fixed;
            border: 1px dashed #4CAF50;
            background: rgba(76, 175, 80, 0.1);
            pointer-events: none;
            z-index: 999998;
            transition: all 0.2s ease;
          `;
          document.body.appendChild(overlay);
        }

        const rect = element.getBoundingClientRect();
        overlay.style.top = rect.top + 'px';
        overlay.style.left = rect.left + 'px';
        overlay.style.width = rect.width + 'px';
        overlay.style.height = rect.height + 'px';
      };

      pickingState.clickHandler = async (event: MouseEvent) => {
        const element = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement;
        const sidebar = document.querySelector('#mcp-sidebar');
        const expandButton = document.querySelector('#mcp-sidebar-toggle-button');
        if (!element ||
          (sidebar && sidebar.contains(element)) ||
          (expandButton && expandButton.contains(element)) ||
          element.closest('[id^="mcp-highlight-overlay"]')) return;

        event.stopPropagation();
        event.preventDefault();

        let message: Message;
        if (pickingState.activePickingType === 'DOM') {
          const html = element.outerHTML;
          message = {
            type: 'DOM',
            content: html,
            windowUrl: window.location.href
          };
        } else {
          const previewOverlay = document.querySelector('#mcp-highlight-overlay-preview') as HTMLElement;
          if (previewOverlay) {
            previewOverlay.style.display = 'none';
          }
          const screenshotId = `screenshot-${Math.random().toString(36).substring(2)}`;
          element.setAttribute('data-screenshot-id', screenshotId);
          const screenshot = await (window as any).takeScreenshot(`[data-screenshot-id="${screenshotId}"]`);
          element.removeAttribute('data-screenshot-id');
          if (previewOverlay) {
            previewOverlay.style.display = 'block';
          }
          message = {
            type: 'Image',
            content: screenshot,
            windowUrl: window.location.href
          };
        }

        mcpStopPicking();
        (window as any).onElementPicked(message);
      };

      document.addEventListener('mousemove', pickingState.mouseMoveHandler);
      document.addEventListener('click', pickingState.clickHandler, true);
    };

    // Expose picking functions to window
    window.mcpStartPicking = mcpStartPicking;
    window.mcpStopPicking = mcpStopPicking;

    const getSidebarWidth = () => {
      const defaultWidth = localStorage.getItem('mcp-sidebar-width') || '500';
      return parseInt(defaultWidth);
    }

    const createSidebar = () => {
      const sidebar = document.createElement('div');
      sidebar.id = 'mcp-sidebar';
      const defaultWidth = getSidebarWidth();
      sidebar.style.cssText = `
        position: fixed;
        top: 0;
        right: 0;
        width: ${defaultWidth}px;
        height: 100vh;
        background: #f5f5f5;
        border-left: 1px solid rgb(228, 228, 231);
        z-index: 999999;
        display: flex;
        flex-direction: column;
        overflow: hidden;
        transition: transform 0.3s ease;
      `;

      const iframe = document.createElement('iframe');
      iframe.name = 'toolbox-frame';
      iframe.src = 'http://localhost:5174/';
      iframe.style.cssText = `
        width: 100%;
        height: 100%;
        border: none;
      `;

      // Add resize handle
      const resizeHandle = document.createElement('div');
      resizeHandle.id = 'mcp-resize-handle';
      resizeHandle.style.cssText = `
        position: absolute;
        left: 0;
        top: 0;
        width: 4px;
        height: 100%;
        cursor: ew-resize;
        background: transparent;
      `;

      let isResizing = false;
      let lastX = 0;
      const originalProperties: Record<string, string> = {}

      // Function to start resize
      const startResize = (e: MouseEvent) => {
        isResizing = true;
        lastX = e.clientX;

        // Add an overlay over the iframe while resizing to prevent mouse events going to the iframe
        const overlay = document.createElement('div');
        overlay.className = 'resize-overlay';
        overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;z-index:1000;';
        sidebar.appendChild(overlay);

        // Add resize event listeners to document
        document.addEventListener('mousemove', resize);
        document.addEventListener('mouseup', stopResize);

        // Disable text selection during resize
        originalProperties.bodyUserSelect = document.body.style.userSelect;
        document.body.style.userSelect = 'none';
        const toggleButton = document.querySelector('#mcp-sidebar-toggle-button') as HTMLElement;
        if (toggleButton) {
          originalProperties.toggleButtonTransition = toggleButton.style.transition;
          toggleButton.style.transition = '';
        }
      };

      // Function to handle resize
      const resize = (e: MouseEvent) => {
        if (!isResizing) return;

        const deltaX = lastX - e.clientX;
        const newWidth = Math.min(
          Math.max(400, sidebar.offsetWidth + deltaX),
          window.innerWidth * 0.8
        );

        sidebar.style.width = `${newWidth}px`;
        const toggleButton = document.querySelector('#mcp-sidebar-toggle-button') as HTMLElement;
        if (toggleButton) {
          toggleButton.style.right = `${newWidth}px`;
        }
        localStorage.setItem('mcp-sidebar-width', newWidth.toString());
        lastX = e.clientX;
      };

      // Function to stop resize
      const stopResize = () => {
        if (!isResizing) return;
        isResizing = false;

        // Remove the overlay
        const overlay = sidebar.querySelector('.resize-overlay');
        if (overlay) sidebar.removeChild(overlay);

        // Remove event listeners
        document.removeEventListener('mousemove', resize);
        document.removeEventListener('mouseup', stopResize);

        // Re-enable text selection
        document.body.style.userSelect = originalProperties.bodyUserSelect;
        const toggleButton = document.querySelector('#mcp-sidebar-toggle-button') as HTMLElement;
        if (toggleButton) {
          toggleButton.style.transition = originalProperties.toggleButtonTransition;
        }
      };

      resizeHandle.addEventListener('mousedown', startResize);

      sidebar.appendChild(resizeHandle);
      sidebar.appendChild(iframe);
      document.body.appendChild(sidebar);
    }

    const createSidebarToggleButton = () => {
      const toggleButton = document.createElement('button');
      toggleButton.id = 'mcp-sidebar-toggle-button';
      toggleButton.textContent = '⟩';
      toggleButton.setAttribute('data-skip-recording', 'true');
      const sidebarWidth = getSidebarWidth();
      toggleButton.style.cssText = `
        position: fixed;
        right: ${sidebarWidth}px;
        top: 50%;
        transform: translateY(-50%);
        background: #f5f5f5;
        border: 1px solid rgb(228, 228, 231);
        border-right: none;
        border-radius: 4px 0 0 4px;
        font-size: 20px;
        cursor: pointer;
        padding: 8px;
        color: rgb(17, 24, 39);
        z-index: 999999;
        transition: right 0.3s ease;
      `;
      document.body.appendChild(toggleButton);

      let isExpanded = localStorage.getItem('mcp-sidebar-expanded') !== 'false';
      if (!isExpanded) {
        toggleSidebar(false);
      }

      toggleButton.addEventListener('click', () => {
        isExpanded = !isExpanded;
        toggleSidebar(isExpanded);
      });
    }

    createSidebar();
    createSidebarToggleButton();
  });
}

```

--------------------------------------------------------------------------------
/src/App/context.tsx:
--------------------------------------------------------------------------------

```typescript
import React, { useEffect, useRef } from 'react';
import { Maximize, StopCircle, Image, CircleXIcon, GlobeIcon, KeyboardIcon, MousePointerClickIcon, TextCursorInputIcon, CodeIcon, PlusIcon } from 'lucide-react';
import { useGlobalState } from '@/hooks/use-global-stage';
import { Button } from '@/components/ui/button';
import { ScrollArea } from "@/components/ui/scroll-area";
import { Card, CardContent } from '@/components/ui/card';
import { ClickToEdit } from '@/components/ui/click-to-edit';
import { BrowserEvent, BrowserEventType } from '@/mcp/recording/events';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';

interface MessageProps {
  message: Message;
  onDelete: (content: string) => void;
}

const truncate = (text: string, maxLength = 25) => {
  return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
}

const MessageCard: React.FC<{
  icon: React.ReactNode,
  title: React.ReactNode,
  content?: React.ReactNode,
  onDelete: () => void
}> = ({ icon, title, content, onDelete }) => {
  return (
    <Card className="group py-4 rounded-sm">
      <CardContent className="px-4 flex gap-2 flex-col">
        <div className="flex gap-2">
          <div className="flex flex-1 gap-2">
            <div className="w-5 h-5 flex items-center justify-center">
              {icon}
            </div>
            <div className="text-sm font-medium text-gray-800">{title}</div>
          </div>
          <div className="">
            <CircleXIcon className="w-4 h-4 transition-opacity duration-200 opacity-0 group-hover:opacity-100 hover:text-destructive cursor-pointer" onClick={onDelete} />
          </div>
        </div>
        {content && (
          <div className="mt-2">
            {content}
          </div>
        )}
      </CardContent>
    </Card>
  );
};

const renderInteraction = (message: Message, deleteMessage: () => void) => {
  const rawInteraction = JSON.parse(message.content);
  const interaction = rawInteraction as BrowserEvent;

  const getIcon = (type: BrowserEventType) => {
    switch (type) {
      case BrowserEventType.Click:
        return <MousePointerClickIcon />;
      case BrowserEventType.Input:
        return <TextCursorInputIcon />;
      case BrowserEventType.KeyPress:
        return <KeyboardIcon />;
      case BrowserEventType.OpenPage:
        return <GlobeIcon />;
      default:
        return <GlobeIcon />;
    }
  };

  const getText = (interaction: BrowserEvent) => {
    switch (interaction.type) {
      case BrowserEventType.Click:
        return <>Click on <span className="font-bold text-gray-600 ">"{truncate(interaction.elementName || '')}"</span> {truncate(interaction.elementType || '')}</>;
      case BrowserEventType.Input:
        return <>Type <span className="font-bold text-gray-600 ">"{truncate(interaction.typedText || '')}"</span> in <span className="font-bold text-gray-600">{truncate(interaction.elementName || '')}</span></>;
      case BrowserEventType.KeyPress:
        return <>Press <span className="font-bold text-gray-600 ">{interaction.keys.join(' + ')}</span> key{interaction.keys.length > 1 ? 's' : ''}</>;
      case BrowserEventType.OpenPage:
        return <>Navigate to <span className="font-bold text-gray-600 ">{truncate(interaction.windowUrl || '')}</span></>;
      default:
        return <>Unknown interaction</>;
    }
  };

  const selector = 'selectors' in interaction ? interaction.selectors?.[0] : undefined;

  return (
    <MessageCard
      icon={getIcon(interaction.type)}
      title={getText(interaction)}
      content={selector && (
        <div className="flex flex-col gap-2">
          <ClickToEdit className="text-xs text-muted-foreground bg-gray-100 rounded-sm p-1" placeholder="CSS selector e.g. [data-testid='button']" text={selector} onSave={() => { }} />
        </div>
      )}
      onDelete={deleteMessage}
    />
  );
};

const renderImage = (message: Message, deleteMessage: () => void) => {
  return (
    <MessageCard
      icon={<Image className="text-gray-600" />}
      title="Screenshot captured"
      content={
        <img
          src={`data:image/png;base64,${message.content}`}
          className="rounded w-full"
          alt="Screenshot"
        />
      }
      onDelete={deleteMessage}
    />
  );
};

const renderDom = (message: Message, deleteMessage: () => void) => {
  const chars = message.content.length;

  return (
    <MessageCard
      icon={<CodeIcon className="text-gray-600" />}
      title="DOM Element captured"
      content={
        <div className="bg-gray-50 p-3 rounded">
          <div className="font-mono text-xs overflow-x-auto break-all">
            {message.content.length > 300 ? message.content.slice(0, 297) + '...' : message.content}
          </div>
          <div className="text-xs text-muted-foreground mt-2">
            {chars} characters
          </div>
        </div>
      }
      onDelete={deleteMessage}
    />
  );
};

const MessageComponent: React.FC<MessageProps> = ({ message, onDelete }) => {
  const deleteMessage = () => onDelete(message.content);

  if (message.type === 'Interaction') {
    return renderInteraction(message, deleteMessage);
  }

  if (message.type === 'Image') {
    return renderImage(message, deleteMessage);
  }

  if (message.type === 'DOM') {
    return renderDom(message, deleteMessage);
  }

  return null;
};

const Context: React.FC = () => {
  const [state, updateState] = useGlobalState();
  const messagesContainerRef = useRef<HTMLDivElement>(null);
  const prevMessagesLength = useRef(state.messages.length);
  const isFirstRender = useRef(true);

  const scrollToBottom = () => {
    if (messagesContainerRef.current) {
      const scrollArea = messagesContainerRef.current.querySelector('[data-radix-scroll-area-viewport]');
      if (scrollArea) {
        scrollArea.scrollTo({
          top: scrollArea.scrollHeight,
          behavior: 'smooth'
        });
      }
    }
  };

  useEffect(() => {
    if (state.messages.length > prevMessagesLength.current) {
      setTimeout(() => {
        scrollToBottom();
      }, 200);
    }
    prevMessagesLength.current = state.messages.length;
  }, [state.messages.length]);

  useEffect(() => {
    if (isFirstRender.current) {
      setTimeout(() => {
        scrollToBottom();
      }, 500);
      isFirstRender.current = false;
    }
  }, []);

  const handleDelete = (content: string) => {
    updateState({
      ...state,
      messages: state.messages.filter((m: Message) => m.content !== content)
    });
  };

  const stopPicking = () => {
    updateState({
      ...state,
      pickingType: null
    });
    window.triggerMcpStopPicking();
  };

  const startPicking = (type: 'DOM' | 'Image') => {
    updateState({
      ...state,
      pickingType: type
    });
    window.triggerMcpStartPicking(type);
  };

  const toggleRecordingInteractions = () => {
    updateState({
      ...state,
      recordingInteractions: !state.recordingInteractions
    });
  };

  const messageGroups: Message[][] = []
  state.messages.forEach((message: Message) => {
    const url = message.windowUrl
    const lastMessageGroup = messageGroups.length > 0 ? messageGroups[messageGroups.length - 1] : null
    if (!lastMessageGroup || lastMessageGroup[0].windowUrl !== url) {
      messageGroups.push([message])
    } else {
      lastMessageGroup.push(message)
    }
  });

  const recordingInteractions = state.recordingInteractions;

  return (
    <div className="flex-1 flex flex-col h-full bg-white">
      <div className="p-4 flex gap-2">
        <Button
          onClick={toggleRecordingInteractions}
          className="w-40"
        >
          <div className="flex items-center gap-2">
            {recordingInteractions ? (
              <div className="w-3 h-3 bg-red-500" />
            ) : (
              <div className="w-4 h-4 rounded-full bg-red-500" />
            )}
            {recordingInteractions ? 'Stop Recording' : 'Start Recording'}
          </div>
        </Button>
      </div>
      <ScrollArea ref={messagesContainerRef} className="flex-1 max-h-[calc(100vh-194px)] overflow-y-auto">
        {(messageGroups.length > 0 || recordingInteractions) ? (
          <div className="flex flex-col gap-8 p-4">
            {messageGroups.map((messageGroup: Message[], index: number) => (
              <div key={index} className="flex flex-col gap-4">
                <div className="text-sm font-medium text-gray-800 px-1">On page <span className="font-bold text-gray-600 break-all">{truncate(messageGroup[0].windowUrl || '', 120)}</span></div>
                <div className="flex flex-col gap-2">
                  {messageGroup.map((message: Message, index: number) => (
                    <MessageComponent
                      key={index}
                      message={message}
                      onDelete={handleDelete}
                    />
                  ))}
                </div>
              </div>
            ))}
            {recordingInteractions && (
              <div className="flex items-center gap-1 mb-8">
                <div className="mr-2">
                  <div className="w-5 h-5 rounded-full border border-dotted border-gray-300 flex items-center justify-center">
                    <div className="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div>
                  </div>
                </div>
                <div className="text-gray-700">
                  Recording interaction with browser
                </div>
                <div className="ml-auto relative">
                  <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                      <Button variant="outline" size="icon">
                        <PlusIcon className="h-4 w-4" />
                      </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent align="end" className="z-[999999]">
                      <DropdownMenuItem onSelect={() => startPicking('DOM')}>
                        <Maximize className="mr-2 h-4 w-4" />
                        <span>Select DOM Element</span>
                      </DropdownMenuItem>
                      <DropdownMenuItem onSelect={() => startPicking('Image')}>
                        <Image className="mr-2 h-4 w-4" />
                        <span>Take Screenshot</span>
                      </DropdownMenuItem>
                    </DropdownMenuContent>
                  </DropdownMenu>
                </div>
              </div>
            )}
          </div>
        ) : (
          <div className="flex flex-col gap-2 p-4">
            <div className="text-sm text-muted-foreground">
              No interactions recorded yet!
              <br />
              <br />
              Click 'Start Recording' to record interactions.
              <br />
              <br />
              Once you are done with it, go to your MCP server (like Claude, Cursor),
              ask it to pull context using `get-context` tool and give it instructions
              on what kind of testcase to write .
            </div>
          </div>
        )}
      </ScrollArea>
    </div>
  );
};

export default Context;

```