From ec02a90812d85d654418937f05178182d73a3264 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Thu, 13 Nov 2025 19:42:44 +0100 Subject: [PATCH] adding examples --- extension-kapture/KaptureDevToolsPanel.webp | Bin 0 -> 54278 bytes extension-kapture/PRIVACY_POLICY.md | 87 + .../ScreenshotWithExtensionPanel.webp | Bin 0 -> 54290 bytes extension-kapture/background.js | 239 +++ extension-kapture/console-listener.js | 88 + extension-kapture/content-script.js | 30 + extension-kapture/devtools.html | 8 + extension-kapture/devtools.js | 6 + extension-kapture/icons/icon128.png | Bin 0 -> 7391 bytes extension-kapture/icons/icon16.png | Bin 0 -> 744 bytes extension-kapture/icons/icon48.png | Bin 0 -> 2791 bytes extension-kapture/manifest.json | 44 + extension-kapture/modules/background-click.js | 122 ++ .../modules/background-commands.js | 88 + .../modules/background-console.js | 21 + .../modules/background-keypress.js | 214 ++ .../modules/background-navigate.js | 82 + .../modules/background-screenshot.js | 54 + extension-kapture/modules/models.js | 33 + extension-kapture/modules/tab-manager.js | 342 +++ extension-kapture/modules/tab-state.js | 177 ++ extension-kapture/page-helpers.js | 683 ++++++ extension-kapture/panel.css | 377 ++++ extension-kapture/panel.html | 56 + extension-kapture/panel.js | 304 +++ extension-kapture/popup.html | 130 ++ extension-kapture/popup.js | 80 + extension-playwright/README.md | 48 + extension-playwright/icons/icon-128.png | Bin 0 -> 6352 bytes extension-playwright/icons/icon-16.png | Bin 0 -> 571 bytes extension-playwright/icons/icon-32.png | Bin 0 -> 1258 bytes extension-playwright/icons/icon-48.png | Bin 0 -> 2043 bytes extension-playwright/manifest.json | 35 + extension-playwright/package-lock.json | 1885 +++++++++++++++++ extension-playwright/package.json | 35 + extension-playwright/playwright.config.ts | 31 + extension-playwright/src/background.ts | 222 ++ extension-playwright/src/relayConnection.ts | 178 ++ extension-playwright/src/ui/authToken.css | 142 ++ extension-playwright/src/ui/authToken.tsx | 118 ++ extension-playwright/src/ui/colors.css | 891 ++++++++ extension-playwright/src/ui/connect.css | 262 +++ extension-playwright/src/ui/connect.html | 29 + extension-playwright/src/ui/connect.tsx | 253 +++ .../src/ui/copyToClipboard.css | 39 + .../src/ui/copyToClipboard.tsx | 54 + extension-playwright/src/ui/icons.css | 32 + extension-playwright/src/ui/icons.tsx | 49 + extension-playwright/src/ui/status.html | 13 + extension-playwright/src/ui/status.tsx | 112 + extension-playwright/src/ui/tabItem.tsx | 67 + extension-playwright/src/ui/tsconfig.json | 4 + extension-playwright/tests/extension.spec.ts | 336 +++ extension-playwright/tsconfig.json | 22 + extension-playwright/tsconfig.ui.json | 19 + extension-playwright/vite.config.mts | 54 + extension-playwright/vite.sw.config.mts | 31 + 57 files changed, 8226 insertions(+) create mode 100644 extension-kapture/KaptureDevToolsPanel.webp create mode 100644 extension-kapture/PRIVACY_POLICY.md create mode 100644 extension-kapture/ScreenshotWithExtensionPanel.webp create mode 100644 extension-kapture/background.js create mode 100644 extension-kapture/console-listener.js create mode 100644 extension-kapture/content-script.js create mode 100644 extension-kapture/devtools.html create mode 100644 extension-kapture/devtools.js create mode 100644 extension-kapture/icons/icon128.png create mode 100644 extension-kapture/icons/icon16.png create mode 100644 extension-kapture/icons/icon48.png create mode 100644 extension-kapture/manifest.json create mode 100644 extension-kapture/modules/background-click.js create mode 100644 extension-kapture/modules/background-commands.js create mode 100644 extension-kapture/modules/background-console.js create mode 100644 extension-kapture/modules/background-keypress.js create mode 100644 extension-kapture/modules/background-navigate.js create mode 100644 extension-kapture/modules/background-screenshot.js create mode 100644 extension-kapture/modules/models.js create mode 100644 extension-kapture/modules/tab-manager.js create mode 100644 extension-kapture/modules/tab-state.js create mode 100644 extension-kapture/page-helpers.js create mode 100644 extension-kapture/panel.css create mode 100644 extension-kapture/panel.html create mode 100644 extension-kapture/panel.js create mode 100644 extension-kapture/popup.html create mode 100644 extension-kapture/popup.js create mode 100644 extension-playwright/README.md create mode 100644 extension-playwright/icons/icon-128.png create mode 100644 extension-playwright/icons/icon-16.png create mode 100644 extension-playwright/icons/icon-32.png create mode 100644 extension-playwright/icons/icon-48.png create mode 100644 extension-playwright/manifest.json create mode 100644 extension-playwright/package-lock.json create mode 100644 extension-playwright/package.json create mode 100644 extension-playwright/playwright.config.ts create mode 100644 extension-playwright/src/background.ts create mode 100644 extension-playwright/src/relayConnection.ts create mode 100644 extension-playwright/src/ui/authToken.css create mode 100644 extension-playwright/src/ui/authToken.tsx create mode 100644 extension-playwright/src/ui/colors.css create mode 100644 extension-playwright/src/ui/connect.css create mode 100644 extension-playwright/src/ui/connect.html create mode 100644 extension-playwright/src/ui/connect.tsx create mode 100644 extension-playwright/src/ui/copyToClipboard.css create mode 100644 extension-playwright/src/ui/copyToClipboard.tsx create mode 100644 extension-playwright/src/ui/icons.css create mode 100644 extension-playwright/src/ui/icons.tsx create mode 100644 extension-playwright/src/ui/status.html create mode 100644 extension-playwright/src/ui/status.tsx create mode 100644 extension-playwright/src/ui/tabItem.tsx create mode 100644 extension-playwright/src/ui/tsconfig.json create mode 100644 extension-playwright/tests/extension.spec.ts create mode 100644 extension-playwright/tsconfig.json create mode 100644 extension-playwright/tsconfig.ui.json create mode 100644 extension-playwright/vite.config.mts create mode 100644 extension-playwright/vite.sw.config.mts diff --git a/extension-kapture/KaptureDevToolsPanel.webp b/extension-kapture/KaptureDevToolsPanel.webp new file mode 100644 index 0000000000000000000000000000000000000000..9075392e409e39ead0db32d333d41b678a4ddbf1 GIT binary patch literal 54278 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 literal 0 HcmV?d00001 diff --git a/extension-kapture/PRIVACY_POLICY.md b/extension-kapture/PRIVACY_POLICY.md new file mode 100644 index 0000000..116dfa5 --- /dev/null +++ b/extension-kapture/PRIVACY_POLICY.md @@ -0,0 +1,87 @@ +# Privacy Policy for Kapture Browser Automation + +**Last Updated: December 2024** + +## Overview + +Kapture Browser Automation ("the Extension") is designed to enable local browser automation through the Model Context Protocol (MCP). This privacy policy explains how the Extension handles user data. + +## Data Collection and Usage + +### What We Access +The Extension may access: +- Current tab URL and title +- Page content (HTML/DOM) when requested +- Browser console logs when requested +- Screenshots of the current tab when requested +- Form field values when automating interactions + +### What We DON'T Do +- We do NOT collect or store any personal information +- We do NOT send any data to external servers +- We do NOT track your browsing history +- We do NOT use analytics or tracking services +- We do NOT sell or share any data + +### Local Operation Only +All Extension operations are strictly local: +- Communication occurs only with localhost (127.0.0.1) on port 61822 +- Data is transmitted only to the MCP server running on your local machine +- No internet connection is required for Extension operation +- No data leaves your computer + +## Data Transmission + +When you use the Extension: +1. Commands are received from the local MCP server via WebSocket +2. The Extension executes the requested action on the current web page +3. Results are sent back to the local MCP server +4. All communication uses localhost networking only + +## Data Storage + +The Extension stores: +- Your connection status (connected/disconnected) +- A temporary session Tab ID for the current browser tab +- Command history for the current session (cleared on disconnect) + +No persistent data is stored between browser sessions. + +## Permissions + +The Extension requires certain Chrome permissions: +- `tabs`: To access tab information and execute scripts +- `debugger`: To capture screenshots +- ``: To interact with any website you choose to automate + +These permissions are used solely for providing automation functionality and are never used for tracking or data collection. + +## Security + +- All communication uses WebSocket protocol over localhost +- No authentication tokens or passwords are stored +- Each tab receives a unique session ID that expires on disconnect + +## User Control + +You have complete control: +- Connect/disconnect at any time via the DevTools panel +- Close the DevTools panel to stop all Extension activity +- Disable or uninstall the Extension at any time via Chrome settings + +## Children's Privacy + +This Extension is not directed at children under 13 and does not knowingly collect information from children. + +## Changes to This Policy + +We may update this privacy policy from time to time. Any changes will be reflected in the "Last Updated" date above. + +## Contact + +For questions about this privacy policy or the Extension, please visit: +https://github.com/williamkapke/kapture + +## Consent + +By using the Kapture Browser Automation Extension, you consent to this privacy policy. diff --git a/extension-kapture/ScreenshotWithExtensionPanel.webp b/extension-kapture/ScreenshotWithExtensionPanel.webp new file mode 100644 index 0000000000000000000000000000000000000000..b73b5a783d6f7aa9c35be416ae5fd77c59e8925f GIT binary patch literal 54290 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 V7$p6i6<|T-*l9GazG6%O0066WDDD6N literal 0 HcmV?d00001 diff --git a/extension-kapture/background.js b/extension-kapture/background.js new file mode 100644 index 0000000..ff567b1 --- /dev/null +++ b/extension-kapture/background.js @@ -0,0 +1,239 @@ +// Background service worker - manages WebSocket connections + +import { TabManager } from './modules/tab-manager.js'; +import {ConsoleLogEntry} from "./modules/models.js"; + +// Single source of truth for all tab state +const tabManager = new TabManager(); + +// Helper function to send connection state to content script +function sendConnectionStateToTab(tabId, connectionState) { + chrome.tabs.sendMessage(tabId, { + command: '_connectionStateChanged', + params: { + status: connectionState.status, + connected: connectionState.connected + } + }).catch(err => { + // Content script might not be injected yet, ignore error + console.debug('Could not send connection state to tab:', err); + }); +} + +// Listen for tab state changes +tabManager.addListener((tabId, event, tabState, data) => { + switch (event) { + case 'stateChanged': + // Broadcast to all ports for this tab + tabState.broadcastToPorts({ + type: 'state', + tabId, + ...tabState.getConnectionState() + }); + + // Send connection state to content script + const connectionState = tabState.getConnectionState(); + sendConnectionStateToTab(tabId, connectionState); + + // Update action badge for active tab + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs[0]?.id === tabId) { + updateActionBadge(tabState.getConnectionState()); + } + }); + break; + + case 'messageReceived': + case 'messageSent': + // Broadcast updated messages to all ports + tabState.broadcastToPorts({ + type: 'messages', + tabId, + messages: tabState.getMessages() + }); + break; + + case 'consoleLogAdded': + // Broadcast updated console count + tabState.broadcastToPorts({ + type: 'consoleCount', + tabId, + count: tabState.getConsoleLogCount() + }); + break; + + case 'messagesCleared': + tabState.broadcastToPorts({ + type: 'messages', + tabId, + messages: [] + }); + break; + + case 'consoleLogsCleared': + tabState.broadcastToPorts({ + type: 'consoleCount', + tabId, + count: 0 + }); + break; + } +}); + +// Update action badge based on connection status +function updateActionBadge(state) { + switch (state.status) { + case 'connected': + chrome.action.setBadgeText({ text: '✓' }); + chrome.action.setBadgeBackgroundColor({ color: '#4caf50' }); + break; + + case 'retrying': + chrome.action.setBadgeText({ text: '↻' }); + chrome.action.setBadgeBackgroundColor({ color: '#ff9800' }); + break; + + case 'disconnected': + default: + chrome.action.setBadgeText({ text: '' }); + break; + } +} + +// Handle UI connections +chrome.runtime.onConnect.addListener((port) => { + port.onMessage.addListener((msg) => { + if (msg.type === 'subscribe' && msg.tabId) { + tabManager.addPort(msg.tabId, port); + } else if (msg.type === 'clearMessages' && msg.tabId) { + tabManager.clearMessages(msg.tabId); + } else if (msg.type === 'clearConsoleLogs' && msg.tabId) { + tabManager.clearConsoleLogs(msg.tabId); + } + }); + + port.onDisconnect.addListener(() => { + // Remove port from all tabs + tabManager.getAllTabs().forEach(tabState => { + tabState.removePort(port); + }); + }); +}); + +// Handle all messages +chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { + // Handle content script messages + if (sender.tab) { + if (request.type === 'contentScriptReady') { + console.log(`Content script ready in tab ${sender.tab.id}`); + + // Send current connection state to the newly ready content script + const tabState = tabManager.getTab(sender.tab.id); + if (tabState) { + const connectionState = tabState.getConnectionState(); + sendConnectionStateToTab(sender.tab.id, connectionState); + } + + sendResponse({ acknowledged: true }); + return false; + } + + if (request.type === 'connect') { + const result = await tabManager.connect(sender.tab.id); + sendResponse(result); + return true; // Keep message channel open for async response + } + + if (request.type === 'disconnect') { + const result = tabManager.disconnect(sender.tab.id); + sendResponse(result); + return false; + } + + if (request.type === 'openPopup') { + chrome.action.openPopup(); + sendResponse({ ok: true }); + return false; + } + + if (request.type === 'mousePosition') { + // Store mouse position for the tab + const tabState = tabManager.getTab(sender.tab.id); + if (tabState) { + tabState.setMousePosition({ x: request.x, y: request.y }); + } + return false; + } + + if (request.type === 'consoleLog') { + // Handle console log from content script + const tabState = tabManager.getTab(sender.tab.id); + if (tabState) { + if (request.level === 'clear') { + // Clear console logs + tabManager.clearConsoleLogs(sender.tab.id); + } else { + // Add console log + tabManager.addConsoleLog(sender.tab.id, new ConsoleLogEntry( + request.level, + request.args, + request.stackTrace + )); + } + } + return false; + } + } + + // Handle messages from popup/panel (not from content scripts) + if (!sender.tab) { + if (request.type === 'connect' && request.tabId) { + const result = await tabManager.connect(request.tabId); + sendResponse(result); + return true; // Keep message channel open for async response + } + + if (request.type === 'disconnect' && request.tabId) { + const result = tabManager.disconnect(request.tabId); + sendResponse(result); + return false; + } + + if (request.type === 'getState' && request.tabId) { + const tabState = tabManager.getTab(request.tabId); + sendResponse(tabState ? tabState.getConnectionState() : { connected: false, status: 'disconnected' }); + return false; + } + } +}); + +// Update badge when active tab changes +chrome.tabs.onActivated.addListener((activeInfo) => { + const tabState = tabManager.getTab(activeInfo.tabId); + if (tabState) { + updateActionBadge(tabState.getConnectionState()); + } else { + updateActionBadge({ status: 'disconnected' }); + } +}); + +// Clean up when tabs are closed +chrome.tabs.onRemoved.addListener((tabId) => { + tabManager.removeTab(tabId); +}); + +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === 'install') { + const destination = { url: 'https://to.kap.co/kapture-welcome' }; + // Navigate the current active tab instead of creating a new one + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs[0]) { + chrome.tabs.update(tabs[0].id, destination); + } + else { + // Fallback: create a new tab if no active tab is found + chrome.tabs.create(destination); + } + }); + } +}); diff --git a/extension-kapture/console-listener.js b/extension-kapture/console-listener.js new file mode 100644 index 0000000..1ffca9c --- /dev/null +++ b/extension-kapture/console-listener.js @@ -0,0 +1,88 @@ +(function() { + const isOverridden = !console.log.toString().includes('[native code]'); + + // Check if already injected + if (isOverridden) { + return; + } + + // Store original console methods + const originalConsole = { + log: console.log, + error: console.error, + warn: console.warn, + info: console.info, + debug: console.debug, + trace: console.trace, + table: console.table, + group: console.group, + groupCollapsed: console.groupCollapsed, + groupEnd: console.groupEnd, + clear: console.clear + }; + + // Helper to serialize arguments + function serializeArgs(args) { + return Array.from(args).map(arg => { + try { + if (arg === undefined) return 'undefined'; + if (arg === null) return 'null'; + if (typeof arg === 'function') return arg.toString(); + if (typeof arg === 'object') { + // Handle circular references + const seen = new WeakSet(); + return JSON.stringify(arg, function(key, value) { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) return '[Circular]'; + seen.add(value); + } + if (typeof value === 'function') return value.toString(); + return value; + }); + } + return String(arg); + } catch (e) { + return String(arg); + } + }); + } + + // Override console methods (all except clear) + ['log', 'error', 'warn', 'info', 'debug', 'trace', 'table', 'group', 'groupCollapsed', 'groupEnd'].forEach(level => { + console[level] = function(...args) { + // Create log entry + const detail = { + level: level, + args: serializeArgs(args), + timestamp: new Date().toISOString() + }; + + // Only include stack trace for errors and warnings + if (level === 'error' || level === 'warn' || level === 'trace') { + detail.stack = new Error().stack; + } + + const event = new CustomEvent('kapture-console', { detail }); + + // Dispatch event for content script to capture + window.dispatchEvent(event); + + // Call original method + originalConsole[level].apply(console, args); + }; + }); + + // Override console.clear + console.clear = function() { + // Dispatch clear event + const event = new CustomEvent('kapture-console', { detail: { level: 'clear' }}); + originalConsole.log('[Kapture] Dispatching console clear event'); + window.dispatchEvent(event); + + // Call original method + originalConsole.clear.apply(console); + }; + + // Log that injection is complete + originalConsole.log('[Kapture] Console listener attached'); +})(); diff --git a/extension-kapture/content-script.js b/extension-kapture/content-script.js new file mode 100644 index 0000000..67d6b86 --- /dev/null +++ b/extension-kapture/content-script.js @@ -0,0 +1,30 @@ +// Proxy messages from webpage to extension +window.addEventListener('kapture-message', (event) => { + chrome.runtime.sendMessage(event.detail); +}); + +// Listen for console log events from the injected code +window.addEventListener('kapture-console', (event) => { + // Send console log to background script + chrome.runtime.sendMessage({ + type: 'consoleLog', + ...event.detail + }); +}); +function ready() { + // Notify background script that content script is ready + chrome.runtime.sendMessage({ type: 'contentScriptReady' }); + + document.body.classList.add('kapture-loaded'); + window.dispatchEvent(new CustomEvent('kapture-loaded')); + + // Check for auto-connect querystring parameter + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('kapture-connect') === 'true') { + chrome.runtime.sendMessage({ type: 'connect' }); + chrome.runtime.sendMessage({ type: 'openPopup' }); + } +} + +// Notify webpage that Kapture is loaded after DOMContentLoaded +document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', ready) : ready(); diff --git a/extension-kapture/devtools.html b/extension-kapture/devtools.html new file mode 100644 index 0000000..5ca8c2f --- /dev/null +++ b/extension-kapture/devtools.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/extension-kapture/devtools.js b/extension-kapture/devtools.js new file mode 100644 index 0000000..153e6a1 --- /dev/null +++ b/extension-kapture/devtools.js @@ -0,0 +1,6 @@ +// Create DevTools panel +chrome.devtools.panels.create( + "Kapture", + null, + "panel.html" +); \ No newline at end of file diff --git a/extension-kapture/icons/icon128.png b/extension-kapture/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..edcbaa64962b6ddd13e1c6f0c5338c69ed226ef0 GIT binary patch literal 7391 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 a&*8sRBfV4cDCf`s0000}5 literal 0 HcmV?d00001 diff --git a/extension-kapture/icons/icon48.png b/extension-kapture/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..cf5876557b19576909df75bc5c42a1a10ac078bc GIT binary patch literal 2791 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 literal 0 HcmV?d00001 diff --git a/extension-kapture/manifest.json b/extension-kapture/manifest.json new file mode 100644 index 0000000..f3107d1 --- /dev/null +++ b/extension-kapture/manifest.json @@ -0,0 +1,44 @@ +{ + "manifest_version": 3, + "name": "Kapture MCP Browser Automation", + "short_name": "Kapture", + "version": "0.14.0", + "description": "Remote browser automation via MCP - DevTools Extension", + "author": "William Kapke", + "homepage_url": "https://github.com/williamkapke/kapture", + "permissions": ["activeTab", "debugger"], + "host_permissions": [""], + "background": { + "service_worker": "background.js", + "type": "module" + }, + "devtools_page": "devtools.html", + "content_scripts": [ + { + "matches": [""], + "js": ["console-listener.js"], + "run_at": "document_start", + "all_frames": false, + "world": "MAIN" + }, + { + "matches": [""], + "js": ["page-helpers.js", "content-script.js"], + "run_at": "document_start", + "all_frames": false + } + ], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } +} diff --git a/extension-kapture/modules/background-click.js b/extension-kapture/modules/background-click.js new file mode 100644 index 0000000..b583c6b --- /dev/null +++ b/extension-kapture/modules/background-click.js @@ -0,0 +1,122 @@ +// Import helper functions from background-commands +import { getFromContentScript, respondWith, respondWithError, attachDebugger, getElement } from './background-commands.js'; + +export async function click({tabId, mousePosition}, { selector, xpath }) { + return await hover({ tabId, mousePosition }, { selector, xpath }, true); +} + +export async function hover(tab, { selector, xpath }, click = false) { + const { tabId, mousePosition } = tab; + // Validate that either selector or xpath is provided + if (!selector && !xpath) { + return respondWithError(tabId, 'SELECTOR_OR_XPATH_REQUIRED', 'Either selector or xpath is required'); + } + + // Get element and validate it exists and is visible + const elementResult = await getElement(tabId, selector, xpath, true); + if (elementResult.error) return elementResult; + + // Get current mouse position + const currentPosition = mousePosition || { x: 0, y: 0 }; + + // Calculate target position (center of element) + const targetX = elementResult.element.bounds.x + elementResult.element.bounds.width / 2; + const targetY = elementResult.element.bounds.y + elementResult.element.bounds.height / 2; + + try { + // Show cursor + await getFromContentScript(tabId, '_cursor', { show: true }); + + // Set initial cursor position + await getFromContentScript(tabId, '_moveMouseSVG', { x: currentPosition.x, y: currentPosition.y }); + + // Animation configuration + const pixelsPerSecond = 1000; // Adjust for desired speed + const frameInterval = 16; // ~60fps + + await attachDebugger(tabId, async () => { + const sendCmd = (cmd, params) => chrome.debugger.sendCommand({ tabId }, cmd, params); + const dispatchMouseEvent = (params) => sendCmd('Input.dispatchMouseEvent', params); + + // Enable Input domain for mouse events + // await sendCmd('Input.enable'); + + // Function to animate to a position + const animateToPosition = async (fromX, fromY, toX, toY) => { + const distance = Math.sqrt(Math.pow(toX - fromX, 2) + Math.pow(toY - fromY, 2)); + const duration = Math.min(1000, (distance / pixelsPerSecond) * 1000); + const animSteps = Math.max(1, Math.ceil(duration / frameInterval)); + const dx = (toX - fromX) / animSteps; + const dy = (toY - fromY) / animSteps; + + for (let i = 1; i <= animSteps; i++) { + const x = fromX + (dx * i); + const y = fromY + (dy * i); + + // Move visual cursor + await getFromContentScript(tabId, '_moveMouseSVG', { x, y }); + + // Send mouse move event + await dispatchMouseEvent({ type: 'mouseMoved', x, y }); + + // Wait for next frame + await new Promise(resolve => setTimeout(resolve, frameInterval)); + } + + return { x: toX, y: toY }; + }; + + // Initial animation to target + let finalPosition = await animateToPosition(currentPosition.x, currentPosition.y, targetX, targetY); + + // Try up to 5 times to ensure we're over the target element + const maxAttempts = 5; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const finalCheck = await getElement(tabId, selector, xpath, true); + if (!finalCheck.error && finalCheck.element) { + const bounds = finalCheck.element.bounds; + + // Check if cursor is actually over the element + const isOverElement = finalPosition.x >= bounds.x && + finalPosition.x <= bounds.x + bounds.width && + finalPosition.y >= bounds.y && + finalPosition.y <= bounds.y + bounds.height; + + if (!isOverElement) { + // Calculate new target center and animate to it + const actualTargetX = bounds.x + bounds.width / 2; + const actualTargetY = bounds.y + bounds.height / 2; + finalPosition = await animateToPosition(finalPosition.x, finalPosition.y, actualTargetX, actualTargetY); + } else { + // Cursor is over the element, we're done + break; + } + } else { + // Element not found, stop trying + break; + } + } + + const bounds = await getFromContentScript(tabId, '_elementPosition', { id: elementResult.element.id }); + const actualTargetX = bounds.x + bounds.width / 2; + const actualTargetY = bounds.y + bounds.height / 2; + await getFromContentScript(tabId, '_moveMouseSVG', { x: actualTargetX, y: actualTargetY }); + + if (click) { + await dispatchMouseEvent({type: 'mousePressed', x: actualTargetX, y: actualTargetY, button: 'left', clickCount: 1}); + await dispatchMouseEvent({type: 'mouseReleased', x: actualTargetX, y: actualTargetY, button: 'left', clickCount: 1}); + } + }); + + // Hide cursor after 1 second + setTimeout(async () => { + await getFromContentScript(tabId, '_cursor', { show: false }); + }, 1000); + + return respondWith(tabId, click ? { clicked: true } : { hovered: true }, selector, xpath); + } catch (error) { + // Make sure cursor is hidden on error + await getFromContentScript(tabId, '_cursor', { show: false }); + return respondWithError(tabId, 'CLICK_FAILED', error.message, selector, xpath); + } +} diff --git a/extension-kapture/modules/background-commands.js b/extension-kapture/modules/background-commands.js new file mode 100644 index 0000000..8739f8b --- /dev/null +++ b/extension-kapture/modules/background-commands.js @@ -0,0 +1,88 @@ +import { keypress } from './background-keypress.js'; +import { click, hover } from './background-click.js'; +import { navigate, back, forward, close, reload, show } from './background-navigate.js'; +import { screenshot } from './background-screenshot.js'; +import { getLogs } from './background-console.js'; + +export const getFromContentScript = async (tabId, command, params, ) => { + return await chrome.tabs.sendMessage(tabId, { command, params }); +} + +// Detect browser type from user agent +export const detectBrowser = () => { + const userAgent = navigator.userAgent.toLowerCase(); + + // Check for Chromium-based browsers that support Chrome extensions + if (userAgent.includes('edg/')) { + return 'edge'; + } else if (userAgent.includes('opr/') || userAgent.includes('opera/')) { + return 'opera'; + } else if (userAgent.includes('vivaldi/')) { + return 'vivaldi'; + } else if (userAgent.includes('brave/')) { + return 'brave'; + } else if (userAgent.includes('chrome/')) { + // Additional check for Brave which doesn't always include 'brave' in UA + if (navigator.brave && navigator.brave.isBrave) { + return 'brave'; + } + return 'chrome'; + } else { + // Default to chromium for any other Chromium-based browser + return 'chromium'; + } +}; + +export const getTabInfo = async(tabId) => await getFromContentScript(tabId, 'getTabInfo'); +export const getElement = async (tabId, selector, xpath, visible) => { + return await getFromContentScript(tabId, 'element', { selector, xpath, visible }); +} + +export const respondWith = async (tabId, obj, selector, xpath) => { + const info = await getTabInfo(tabId); + return { + success: !obj.error, + selector, + xpath: !selector ? xpath : undefined, + ...info, + ...obj + }; +} +export const respondWithError = async (tabId, code, message, selector, xpath) => { + return respondWith(tabId,{ error: { code, message } }, selector, xpath); +} +export async function attachDebugger(tabId, action) { + let debuggerAttached = false; + try { + // Attach debugger to capture screenshot without making tab active + await chrome.debugger.attach({tabId}, '1.3'); + debuggerAttached = true; + + // Enable the Page domain + await chrome.debugger.sendCommand({tabId}, 'Page.enable'); + + return await action(); + } + finally { + // Always detach debugger if attached + try { + if (debuggerAttached) await chrome.debugger.detach({tabId: tabId}); + } + catch (e) { } + } +} + + +export const backgroundCommands = { + navigate, + back, + forward, + close, + reload, + show, + click, + hover, + keypress, + screenshot, + getLogs +} diff --git a/extension-kapture/modules/background-console.js b/extension-kapture/modules/background-console.js new file mode 100644 index 0000000..3afffb4 --- /dev/null +++ b/extension-kapture/modules/background-console.js @@ -0,0 +1,21 @@ +// Import helper functions from background-commands +import { respondWith, respondWithError } from './background-commands.js'; + +export async function getLogs(tabState, { before, limit = 100, level }) { + try { + // Get the logs from the tab state using its built-in method + const logs = tabState.getConsoleLogs(limit, level, before); + + // Check if there are more logs available + const totalFilteredCount = tabState.getConsoleLogs(null, level, before).length; + const hasMore = totalFilteredCount > limit; + + return respondWith(tabState.tabId, { + logs: logs, + hasMore: hasMore, + totalCount: tabState.getConsoleLogCount() + }); + } catch (error) { + return respondWithError(tabState.tabId, 'CONSOLE_LOG_ERROR', error.message); + } +} diff --git a/extension-kapture/modules/background-keypress.js b/extension-kapture/modules/background-keypress.js new file mode 100644 index 0000000..9540d6a --- /dev/null +++ b/extension-kapture/modules/background-keypress.js @@ -0,0 +1,214 @@ +// Import helper functions from background-commands +import { getFromContentScript, respondWith, respondWithError, attachDebugger } from './background-commands.js'; + +// Additional helper functions +const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + +// Send keypress event +export async function keypress({tabId}, params) { + const { key, selector, xpath, delay = 50 } = params; + + if (!key) { + return respondWithError(tabId, 'KEY_REQUIRED', 'Key parameter is required'); + } + + // Validate delay is within reasonable bounds (0-60000ms = 0-60 seconds) + const keypressDelay = Math.max(0, Math.min(60000, delay)); + + try { + // First, focus the target element if selector/xpath provided + if (selector || xpath) { + const focusResult = await getFromContentScript(tabId, 'focus', { selector, xpath }); + if (focusResult.error) { + return focusResult; + } + } + + // Parse key combination to extract key and modifiers + const keyData = parseKeyCombination(key); + + // Use attachDebugger helper to manage debugger lifecycle + return await attachDebugger(tabId, async () => { + // Helper function to send key event + const sendKeyEvent = async (type, autoRepeat = false, text = null) => { + const eventData = { + type, + ...keyData, + windowsVirtualKeyCode: keyData.keyCode, + nativeVirtualKeyCode: keyData.keyCode + }; + + if (type === 'keyDown' || type === 'keyUp') { + eventData.autoRepeat = autoRepeat; + } + + if (text) { + eventData.text = text; + } + + await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchKeyEvent', eventData); + }; + + // Send keydown event + await sendKeyEvent('keyDown', false); + + // If delay > 500ms, simulate key repeat + if (keypressDelay > 500) { + // Calculate number of repeat events based on delay + // Typical repeat rate is ~30ms between events + const repeatInterval = 30; + const repeatCount = Math.floor((keypressDelay - 100) / repeatInterval); + + // Wait a bit before starting repeat (typical OS behavior) + await wait(50); + + // Send repeated keydown events + for (let i = 0; i < repeatCount; i++) { + await sendKeyEvent('keyDown', true); + + // Wait between repeat events + if (i < repeatCount - 1) { + await wait(repeatInterval); + } + } + } else if (keypressDelay > 0) { + // For delays <= 500ms, just wait the specified time + await wait(keypressDelay); + } + + // Send keyup event + await sendKeyEvent('keyUp'); + + // Wait a bit for the page to process the key events + await wait(50); + + // Keys that typically cause scrolling need extra time to settle + const scrollKeys = ['PageUp', 'PageDown', 'Space', ' ', 'ArrowUp', 'ArrowDown', 'Home', 'End']; + if (!selector && !xpath && scrollKeys.includes(keyData.key)) { + // Additional delay for scroll to complete when key is pressed globally (not on a specific element) + await wait(100); + } + + // Get result including element info if selector was provided + const resultData = { + keyPressed: true, + key: key, + delay: keypressDelay + }; + + // If this was an auto-repeat key press, include that info + if (keypressDelay > 500) { + resultData.autoRepeat = true; + resultData.repeatCount = Math.floor((keypressDelay - 100) / 30); + } + + return await respondWith(tabId, resultData, selector, xpath); + }); + } catch (error) { + return respondWithError(tabId, 'KEYPRESS_FAILED', error.message, selector, xpath); + } +} + +// Parse key combination and return CDP-compatible key data +function parseKeyCombination(keyCombination) { + let modifiers = 0; + let key = ''; + + // CDP modifier flags + const CDP_MODIFIERS = { + alt: 1, + ctrl: 2, + meta: 4, + shift: 8 + }; + + // Special key mappings + const KEY_MAPPINGS = { + 'enter': { key: 'Enter', code: 'Enter', keyCode: 13 }, + 'return': { key: 'Enter', code: 'Enter', keyCode: 13 }, + 'tab': { key: 'Tab', code: 'Tab', keyCode: 9 }, + 'delete': { key: 'Delete', code: 'Delete', keyCode: 46 }, + 'backspace': { key: 'Backspace', code: 'Backspace', keyCode: 8 }, + 'escape': { key: 'Escape', code: 'Escape', keyCode: 27 }, + 'esc': { key: 'Escape', code: 'Escape', keyCode: 27 }, + 'space': { key: ' ', code: 'Space', keyCode: 32, text: ' ' }, + ' ': { key: ' ', code: 'Space', keyCode: 32, text: ' ' }, + 'arrowup': { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 }, + 'arrowdown': { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 }, + 'arrowleft': { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 }, + 'arrowright': { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 }, + 'pageup': { key: 'PageUp', code: 'PageUp', keyCode: 33 }, + 'pagedown': { key: 'PageDown', code: 'PageDown', keyCode: 34 }, + 'home': { key: 'Home', code: 'Home', keyCode: 36 }, + 'end': { key: 'End', code: 'End', keyCode: 35 }, + 'insert': { key: 'Insert', code: 'Insert', keyCode: 45 }, + 'f1': { key: 'F1', code: 'F1', keyCode: 112 }, + 'f2': { key: 'F2', code: 'F2', keyCode: 113 }, + 'f3': { key: 'F3', code: 'F3', keyCode: 114 }, + 'f4': { key: 'F4', code: 'F4', keyCode: 115 }, + 'f5': { key: 'F5', code: 'F5', keyCode: 116 }, + 'f6': { key: 'F6', code: 'F6', keyCode: 117 }, + 'f7': { key: 'F7', code: 'F7', keyCode: 118 }, + 'f8': { key: 'F8', code: 'F8', keyCode: 119 }, + 'f9': { key: 'F9', code: 'F9', keyCode: 120 }, + 'f10': { key: 'F10', code: 'F10', keyCode: 121 }, + 'f11': { key: 'F11', code: 'F11', keyCode: 122 }, + 'f12': { key: 'F12', code: 'F12', keyCode: 123 } + }; + + // Parse modifiers from combination + let remaining = keyCombination; + const modifierPatterns = [ + { pattern: /^(Control|Ctrl)\+/i, modifier: 'ctrl' }, + { pattern: /^Shift\+/i, modifier: 'shift' }, + { pattern: /^Alt|Option\+/i, modifier: 'alt' }, + { pattern: /^(Meta|Cmd|Command)\+/i, modifier: 'meta' } + ]; + + // Extract modifiers + let foundModifier = true; + while (foundModifier && remaining) { + foundModifier = false; + for (const { pattern, modifier } of modifierPatterns) { + if (pattern.test(remaining)) { + modifiers |= CDP_MODIFIERS[modifier]; + remaining = remaining.replace(pattern, ''); + foundModifier = true; + break; + } + } + } + + // What's left is the key + key = remaining; + + // Look up special key mapping + const lowerKey = key.toLowerCase(); + if (KEY_MAPPINGS[lowerKey]) { + return { + ...KEY_MAPPINGS[lowerKey], + modifiers, + text: KEY_MAPPINGS[lowerKey].text || undefined + }; + } + + // For single character keys + if (key.length === 1) { + const keyCode = key.toUpperCase().charCodeAt(0); + return { + key: key, + code: 'Key' + key.toUpperCase(), + keyCode: keyCode, + modifiers, + text: modifiers === 0 ? key : undefined // Only send text for unmodified keys + }; + } + + // For other keys, try to generate a reasonable code + return { + key: key, + code: key, + keyCode: 0, // Unknown keycode + modifiers + }; +} diff --git a/extension-kapture/modules/background-navigate.js b/extension-kapture/modules/background-navigate.js new file mode 100644 index 0000000..c24256e --- /dev/null +++ b/extension-kapture/modules/background-navigate.js @@ -0,0 +1,82 @@ +// Import helper functions from background-commands +import { getFromContentScript, respondWith, respondWithError } from './background-commands.js'; + +// Check if URL is allowed for extension +const isAllowedUrl = (url) => !!url && (url.startsWith('http://') || url.startsWith('https://')); + +// Wait for content script to be ready +async function waitForContentScriptReady(tabId, timeout = 5000) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + chrome.runtime.onMessage.removeListener(listener); + reject(new Error('Timeout waiting for content script')); + }, timeout); + + const listener = (request, sender) => { + if (request.type === 'contentScriptReady' && sender.tab?.id === tabId) { + clearTimeout(timer); + chrome.runtime.onMessage.removeListener(listener); + resolve(); + } + }; + + chrome.runtime.onMessage.addListener(listener); + }); +} + +// Execute navigation and wait for content script +async function executeNavigation(tabId, navigationFn) { + try { + await navigationFn(); + await waitForContentScriptReady(tabId); + return await respondWith(tabId, {}); + } catch (error) { + return respondWithError(tabId, 'NAVIGATION_FAILED', error.message); + } +} + +// Navigation commands +export async function navigate({tabId}, { url }) { + if (!isAllowedUrl(url)) { + return respondWithError(tabId, 'NAVIGATION_BLOCKED', `Navigation to ${url} is not allowed`); + } + return executeNavigation(tabId, async () => getFromContentScript(tabId, '_navigate', { url })); +} + +export async function back({tabId}) { + return executeNavigation(tabId, () => chrome.tabs.goBack(tabId)); +} + +export async function forward({tabId}) { + return executeNavigation(tabId, () => chrome.tabs.goForward(tabId)); +} + +export async function close({tabId}) { + try { + await chrome.tabs.remove(tabId); + return { success: true, closed: true }; + } catch (error) { + return { success: false, error: { code: 'CLOSE_FAILED', message: error.message } }; + } +} + +export async function reload({tabId}) { + return executeNavigation(tabId, () => chrome.tabs.reload(tabId)); +} + +export async function show({tabId}) { + try { + // Get the tab to find its window ID + const tab = await chrome.tabs.get(tabId); + + // Bring the window to the front + await chrome.windows.update(tab.windowId, { focused: true }); + + // Make the tab active in the window + await chrome.tabs.update(tabId, { active: true }); + + return await respondWith(tabId, { shown: true }); + } catch (error) { + return respondWithError(tabId, 'SHOW_FAILED', error.message); + } +} \ No newline at end of file diff --git a/extension-kapture/modules/background-screenshot.js b/extension-kapture/modules/background-screenshot.js new file mode 100644 index 0000000..0f543fa --- /dev/null +++ b/extension-kapture/modules/background-screenshot.js @@ -0,0 +1,54 @@ +// Import helper functions from background-commands +import { getElement, getTabInfo, respondWithError, attachDebugger } from './background-commands.js'; + +export async function screenshot({tabId}, { scale = 0.5, quality = 0.5, format = 'webp', selector, xpath }) { + let elementResult; + if (selector || xpath) { + elementResult = await getElement(tabId, selector, xpath, true); + if (elementResult.error) return elementResult; + } + else { + elementResult = await getTabInfo(tabId) + elementResult.element = { + bounds: { + x: elementResult.scrollPosition.x, + y: elementResult.scrollPosition.y, + width: elementResult.viewportDimensions.width, + height: elementResult.viewportDimensions.height + } + }; + } + + const clip = { ...elementResult.element.bounds }; + + // For fixed positioned elements, we need viewport-relative coordinates + // For non-fixed elements, we need document-relative coordinates + if (elementResult.element.position !== 'fixed') { + // Add scroll position to convert from viewport to document coordinates + clip.x += elementResult.scrollPosition.x; + clip.y += elementResult.scrollPosition.y; + } + + if (scale) { + clip.scale = scale; + } + + return attachDebugger(tabId, async () => { + const screenshot = await chrome.debugger.sendCommand({ tabId }, 'Page.captureScreenshot', { + format, + quality: Math.round(quality * 100), // Chrome needs an integer percentage, + clip + }); + + return { + ...elementResult, + element: undefined, + selector: elementResult.element?.selector || undefined, + mimeType: `image/${format}`, + data: screenshot.data, + }; + }) + .catch((err) => { + return respondWithError(tabId,'SCREENSHOT_ERROR', err.message, null, null); + }); +} \ No newline at end of file diff --git a/extension-kapture/modules/models.js b/extension-kapture/modules/models.js new file mode 100644 index 0000000..60f14a5 --- /dev/null +++ b/extension-kapture/modules/models.js @@ -0,0 +1,33 @@ + +// Connection state enum +export const ConnectionStatus = { + DISCONNECTED: 'disconnected', + CONNECTING: 'connecting', + CONNECTED: 'connected', + RETRYING: 'retrying', + ERROR: 'error' +}; + +// Message class +export class Message { + constructor(direction, data) { + this.id = crypto.randomUUID(); + this.direction = direction; // 'incoming' | 'outgoing' + this.data = data; + this.timestamp = new Date(); + } +} + +// Console log entry class +export class ConsoleLogEntry { + constructor(level, args, stackTrace = null) { + this.id = crypto.randomUUID(); + this.level = level; // 'log' | 'info' | 'warn' | 'error' + this.args = args; + if (stackTrace) { + this.stackTrace = stackTrace; + } + this.timestamp = new Date(); + } +} + diff --git a/extension-kapture/modules/tab-manager.js b/extension-kapture/modules/tab-manager.js new file mode 100644 index 0000000..61f9c58 --- /dev/null +++ b/extension-kapture/modules/tab-manager.js @@ -0,0 +1,342 @@ +import { TabState } from './tab-state.js'; +import { backgroundCommands, getTabInfo, detectBrowser } from './background-commands.js'; + +export class TabManager { + constructor() { + this.tabs = new Map(); // tabId -> TabState + this.listeners = new Set(); // State change listeners + } + + // Tab lifecycle + getOrCreateTab(tabId) { + if (!this.tabs.has(tabId)) { + const tabState = new TabState(tabId); + this.tabs.set(tabId, tabState); + this.notifyListeners(tabId, 'created', tabState); + } + return this.tabs.get(tabId); + } + + getTab(tabId) { + return this.tabs.get(tabId); + } + + hasTab(tabId) { + return this.tabs.has(tabId); + } + + removeTab(tabId) { + const tabState = this.tabs.get(tabId); + if (tabState) { + tabState.cleanup(); + this.tabs.delete(tabId); + this.notifyListeners(tabId, 'removed', null); + } + } + + // WebSocket connection management + async connect(tabId) { + const tabState = this.getOrCreateTab(tabId); + + if (tabState.websocket && tabState.websocket.readyState === WebSocket.OPEN) { + return { ok: true, message: 'Already connected' }; + } + + // Get tab info from content script + const tabInfo = await getTabInfo(tabId); + tabState.updatePageMetadata(tabInfo); + + // Set up connection + tabState.connectionInfo.userDisconnected = false; + + await this._createConnection(tabState); + + return { ok: true }; + } + + disconnect(tabId) { + const tabState = this.getTab(tabId); + if (!tabState) { + return { ok: false, error: 'Tab not found' }; + } + + tabState.connectionInfo.setDisconnected(true); + + // Clear keepalive interval + if (tabState.keepaliveInterval) { + clearInterval(tabState.keepaliveInterval); + tabState.keepaliveInterval = null; + } + + if (tabState.websocket) { + tabState.websocket.close(); + tabState.clearWebSocket(); + } + + // Clear any pending reconnect + if (tabState.connectionInfo.reconnectTimer) { + clearTimeout(tabState.connectionInfo.reconnectTimer); + tabState.connectionInfo.reconnectTimer = null; + } + + this.notifyListeners(tabId, 'stateChanged', tabState); + + return { ok: true }; + } + + // Private connection methods + async _createConnection(tabState) { + // First check if tab still exists + try { + await chrome.tabs.get(tabState.tabId); + } catch (error) { + console.log(`Tab ${tabState.tabId} no longer exists, aborting connection`); + this.removeTab(tabState.tabId); + return; + } + + const ws = new WebSocket(tabState.connectionInfo.url); + tabState.setWebSocket(ws); + + // Clear any existing keepalive interval + if (tabState.keepaliveInterval) { + clearInterval(tabState.keepaliveInterval); + tabState.keepaliveInterval = null; + } + + ws.onopen = () => { + tabState.connectionInfo.setConnected(); + this.notifyListeners(tabState.tabId, 'stateChanged', tabState); + + // Send registration message with Chrome tab ID and metadata + const registerMessage = { + type: 'register', + requestedTabId: tabState.tabId.toString(), // Chrome tab ID + browser: detectBrowser(), // Add browser type + ...tabState.pageMetadata + }; + + this.sendMessage(tabState.tabId, registerMessage); + + // Set up keepalive ping every 30 seconds + tabState.keepaliveInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + try { + ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); + } catch (e) { + console.error('Failed to send keepalive ping:', e); + } + } + }, 30000); + }; + + ws.onclose = () => { + // Clear keepalive interval + if (tabState.keepaliveInterval) { + clearInterval(tabState.keepaliveInterval); + tabState.keepaliveInterval = null; + } + + tabState.clearWebSocket(); + + if (!tabState.connectionInfo.userDisconnected) { + // Schedule reconnect + this._scheduleReconnect(tabState); + } else { + tabState.connectionInfo.setDisconnected(true); + } + + this.notifyListeners(tabState.tabId, 'stateChanged', tabState); + }; + + ws.onerror = (error) => { + console.error(`WebSocket error for tab ${tabState.tabId}:`, error); + tabState.connectionInfo.setError(error); + // Don't clear WebSocket here - let onclose handle cleanup and reconnection + this.notifyListeners(tabState.tabId, 'stateChanged', tabState); + }; + + ws.onmessage = async (event) => { + try { + const data = JSON.parse(event.data); + + // Don't track pong messages + if (data.type !== 'pong') { + const message = tabState.addMessage('incoming', data); + this.notifyListeners(tabState.tabId, 'messageReceived', tabState, message); + } + + // Handle commands + if (data.type === 'command' && data.command && data.id) { + await this._handleCommand(tabState, data); + } else if (data.type === 'pong') { + // Pong received - connection is healthy + tabState.lastPongTime = Date.now(); + } + } catch (e) { + console.error('Failed to parse WebSocket message:', e); + } + }; + } + + _scheduleReconnect(tabState) { + // Clear any existing reconnect timer + if (tabState.connectionInfo.reconnectTimer) { + clearTimeout(tabState.connectionInfo.reconnectTimer); + tabState.connectionInfo.reconnectTimer = null; + } + + const attemptNumber = tabState.connectionInfo.reconnectAttempts; + const backoffMs = this._getBackoffMs(attemptNumber); + + console.log(`Scheduling reconnect for tab ${tabState.tabId} in ${backoffMs}ms (attempt ${attemptNumber + 1})`); + + tabState.connectionInfo.setRetrying(attemptNumber + 1, backoffMs); + this.notifyListeners(tabState.tabId, 'stateChanged', tabState); + + tabState.connectionInfo.reconnectTimer = setTimeout(async () => { + if (!tabState.connectionInfo.userDisconnected) { + // Check if tab still exists before attempting reconnect + try { + await chrome.tabs.get(tabState.tabId); + console.log(`Attempting reconnect for tab ${tabState.tabId}`); + this._createConnection(tabState); + } catch (error) { + console.log(`Tab ${tabState.tabId} no longer exists, removing from manager`); + this.removeTab(tabState.tabId); + } + } + }, backoffMs); + } + + _getBackoffMs(attemptNumber) { + // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, then cap at 60s + return Math.min(1000 * Math.pow(2, attemptNumber), 60000); + } + + async _handleCommand(tabState, {command, params, id}) { + try { + let result; + // some need to run with the background context + if (backgroundCommands[command]) { + result = await backgroundCommands[command](tabState, params); + } + // others we execute in the page context + else { + result = await chrome.tabs.sendMessage(tabState.tabId, {command, params}); + } + // `success: true` means we didn't throw an error. TODO: rename or remove it + const response = {id, type: 'response', success: true, result}; + this.sendMessage(tabState.tabId, response); + } + catch (error) { + const errorResponse = { id, type: 'response', success: false, error: { message: error.message, code: 'COMMAND_FAILED' }}; + this.sendMessage(tabState.tabId, errorResponse); + } + } + + // Message sending + sendMessage(tabId, data) { + const tabState = this.getTab(tabId); + if (!tabState || !tabState.websocket || tabState.websocket.readyState !== WebSocket.OPEN) { + return { ok: false, error: 'Not connected' }; + } + + try { + const message = tabState.addMessage('outgoing', data); + tabState.websocket.send(JSON.stringify(data)); + this.notifyListeners(tabId, 'messageSent', tabState, message); + return { ok: true }; + } catch (e) { + return { ok: false, error: e.message }; + } + } + + // Message management + clearMessages(tabId) { + const tabState = this.getTab(tabId); + if (tabState) { + tabState.clearMessages(); + this.notifyListeners(tabId, 'messagesCleared', tabState); + } + } + + // Console log management + addConsoleLog(tabId, log) { + const tabState = this.getTab(tabId); + if (tabState) { + tabState.addConsoleLog(log); + this.notifyListeners(tabId, 'consoleLogAdded', tabState, log); + } + } + + clearConsoleLogs(tabId) { + const tabState = this.getTab(tabId); + if (tabState) { + tabState.clearConsoleLogs(); + this.notifyListeners(tabId, 'consoleLogsCleared', tabState); + } + } + + // Port management + addPort(tabId, port) { + const tabState = this.getOrCreateTab(tabId); + tabState.addPort(port); + + // Send initial state + port.postMessage({ + type: 'state', + tabId, + ...tabState.getConnectionState() + }); + + // Send current messages + port.postMessage({ + type: 'messages', + tabId, + messages: tabState.getMessages() + }); + + // Send console count + port.postMessage({ + type: 'consoleCount', + tabId, + count: tabState.getConsoleLogCount() + }); + } + + removePort(tabId, port) { + const tabState = this.getTab(tabId); + if (tabState) { + tabState.removePort(port); + } + } + + // Event listeners + addListener(callback) { + this.listeners.add(callback); + } + + removeListener(callback) { + this.listeners.delete(callback); + } + + notifyListeners(tabId, event, tabState, data = null) { + this.listeners.forEach(callback => { + try { + callback(tabId, event, tabState, data); + } catch (e) { + console.error('Error in TabManager listener:', e); + } + }); + } + + // Utility + getAllTabs() { + return Array.from(this.tabs.values()); + } + + getActiveTabCount() { + return this.tabs.size; + } +} diff --git a/extension-kapture/modules/tab-state.js b/extension-kapture/modules/tab-state.js new file mode 100644 index 0000000..5ba7a17 --- /dev/null +++ b/extension-kapture/modules/tab-state.js @@ -0,0 +1,177 @@ + +// WebSocket connection info +import { ConnectionStatus, Message } from "./models.js"; + +export class ConnectionInfo { + constructor(url) { + this.url = url; + this.status = ConnectionStatus.DISCONNECTED; + this.connected = false; + this.userDisconnected = false; + this.reconnectAttempts = 0; + this.reconnectTimer = null; + this.nextRetryIn = null; + } + + setConnected() { + this.status = ConnectionStatus.CONNECTED; + this.connected = true; + this.reconnectAttempts = 0; + this.nextRetryIn = null; + } + + setDisconnected(userInitiated = false) { + this.status = ConnectionStatus.DISCONNECTED; + this.connected = false; + this.userDisconnected = userInitiated; + if (userInitiated) { + this.reconnectAttempts = 0; + this.nextRetryIn = null; + } + } + + setRetrying(attemptNumber, nextRetryMs) { + this.status = ConnectionStatus.RETRYING; + this.connected = false; + this.reconnectAttempts = attemptNumber; + this.nextRetryIn = nextRetryMs; + } + + setError(error) { + this.status = ConnectionStatus.ERROR; + this.connected = false; + } +} + +// Main tab state class +export class TabState { + constructor(tabId) { + this.tabId = tabId; + this.websocket = null; + this.connectionInfo = new ConnectionInfo('ws://localhost:61822'); + this.messages = []; + this.consoleLogs = []; + this.ports = new Set(); // Connected DevTools panels/popups + this.pageMetadata = {}; + this.mousePosition = { x: 0, y: 0 }; // Track current mouse position + } + + // WebSocket management + setWebSocket(ws) { + this.websocket = ws; + } + + clearWebSocket() { + this.websocket = null; + } + + // Message management + addMessage(direction, data) { + const message = new Message(direction, data); + this.messages.push(message); + return message; + } + + clearMessages() { + this.messages = []; + } + + getMessages(limit = null) { + if (limit === null) { + return [...this.messages]; + } + return this.messages.slice(-limit); + } + + // Console log management + addConsoleLog(log) { + this.consoleLogs.unshift(log); + } + + clearConsoleLogs() { + this.consoleLogs = []; + } + + getConsoleLogs(limit = null, level = null, before = null) { + let logs = this.consoleLogs; + + if (level) { + logs = logs.filter(log => log.level === level); + } + + if (before) { + const beforeTimestamp = new Date(before).getTime(); + logs = logs.filter(log => + new Date(log.timestamp).getTime() < beforeTimestamp + ); + } + + if (limit === null) { + return [...logs]; + } + return logs.slice(-limit); + } + + getConsoleLogCount() { + return this.consoleLogs.length; + } + + // Port management + addPort(port) { + this.ports.add(port); + } + + removePort(port) { + this.ports.delete(port); + } + + broadcastToPorts(message) { + this.ports.forEach(port => { + try { + port.postMessage(message); + } catch (e) { + // Port might be disconnected + this.ports.delete(port); + } + }); + } + + // State management + getConnectionState() { + return { + connected: this.connectionInfo.connected, + status: this.connectionInfo.status, + reconnectAttempt: this.connectionInfo.reconnectAttempts, + nextRetryIn: this.connectionInfo.nextRetryIn + }; + } + + updatePageMetadata(metadata) { + this.pageMetadata = metadata; + } + + // Mouse position tracking + setMousePosition(position) { + this.mousePosition = { ...position }; + } + + // Cleanup + cleanup() { + // Clear reconnect timer + if (this.connectionInfo.reconnectTimer) { + clearTimeout(this.connectionInfo.reconnectTimer); + this.connectionInfo.reconnectTimer = null; + } + + // Close WebSocket + if (this.websocket) { + this.websocket.close(); + this.websocket = null; + } + + // Clear all data + this.messages = []; + this.consoleLogs = []; + this.ports.clear(); + } +} diff --git a/extension-kapture/page-helpers.js b/extension-kapture/page-helpers.js new file mode 100644 index 0000000..3a5ffc7 --- /dev/null +++ b/extension-kapture/page-helpers.js @@ -0,0 +1,683 @@ +// page-helpers.js - Content script that provides helper functions +let kaptureIdCounter = 0; + +function getUniqueSelector(element) { + if (!element || !(element instanceof Element)) return null; + + // Special handling for html, head, and body elements - their tagName is unique + const tagName = element.tagName.toLowerCase(); + if (tagName === 'html' || tagName === 'head' || tagName === 'body') { + return tagName; + } + + // If element has an ID, use it (unless it's empty or contains special chars) + if (element.id && /^[a-zA-Z][\w-]*$/.test(element.id)) { + // Check if ID is truly unique + if (document.querySelectorAll('#' + CSS.escape(element.id)).length === 1) { + return '#' + CSS.escape(element.id); + } + } + + const uniqueId = 'kapture-' + (++kaptureIdCounter) + + if (!element.id) { + element.id = uniqueId; + return '#' + uniqueId; + } + + element.classList.add(uniqueId); + return '.' + uniqueId +} +function findScrollableParent(element) { + function isScrollable(element) { + const hasScrollableContent = element.scrollHeight > element.clientHeight || + element.scrollWidth > element.clientWidth; + + if (!hasScrollableContent) return false; + + const style = getComputedStyle(element); + return /(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX); + } + + let parent = element.parentElement; + while (parent && parent !== document.body) { + if (isScrollable(parent)) return parent; + parent = parent.parentElement; + } + return document.documentElement; +} +function serializeValue(value, depth = 0, maxDepth = 3, seen = new WeakSet()) { + // Handle primitive types + if (value === null || value === undefined) return value; + if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') { + return value; + } + + if (typeof value === 'function') return '[Function: ' + (value.name || 'anonymous') + ']'; + if (typeof value === 'symbol') return value.toString(); + if (value instanceof Date) return value.toISOString(); + if (value instanceof RegExp) return value.toString(); + + // Handle errors + if (value instanceof Error) { + return { + name: value.name, + message: value.message, + stack: value.stack + }; + } + + // Handle DOM elements + if (value instanceof Element) { + const selector = getUniqueSelector(value); + return { + nodeType: 'ELEMENT_NODE', + selector: selector, + tagName: value.tagName, + id: value.id || undefined, + className: value.className || undefined, + attributes: Array.from(value.attributes).reduce((acc, attr) => { + acc[attr.name] = attr.value; + return acc; + }, {}) + }; + } + + // Prevent infinite recursion + if (depth >= maxDepth) { + return '[Max depth reached]'; + } + + // Handle circular references + if (typeof value === 'object' && seen.has(value)) { + return '[Circular reference]'; + } + + // Mark object as seen + if (typeof value === 'object') { + seen.add(value); + } + + // Handle arrays + if (Array.isArray(value)) { + return value.map(item => serializeValue(item, depth + 1, maxDepth, seen)); + } + + // Handle NodeList and HTMLCollection + if (value instanceof NodeList || value instanceof HTMLCollection) { + return { + nodeType: value instanceof NodeList ? 'NodeList' : 'HTMLCollection', + length: value.length, + items: Array.from(value).map(item => serializeValue(item, depth + 1, maxDepth, seen)) + }; + } + + // Handle typed arrays + if (ArrayBuffer.isView(value)) { + return { + type: value.constructor.name, + length: value.length, + data: '[Binary data]' + }; + } + + // Handle other objects + if (typeof value === 'object') { + const result = {}; + const keys = Object.keys(value); + + // Limit number of keys to prevent huge objects + const maxKeys = 100; + const limitedKeys = keys.slice(0, maxKeys); + + for (const key of limitedKeys) { + try { + const serialized = this.serializeValue(value[key], depth + 1, maxDepth, seen); + if (serialized === undefined || serialized === null) continue; // Skip undefined values + result[key] = serialized; + } catch (e) { + result[key] = '[Error accessing property]'; + } + } + + if (keys.length > maxKeys) { + result['...'] = `${keys.length - maxKeys} more properties`; + } + + return result; + } + + // Fallback for unknown types + return String(value); +} +function getTabInfo() { + const de = document.documentElement; + return { + url: window.location.href, + title: document.title, + domSize: de.outerHTML.length, + fullPageDimensions: { width: de.scrollWidth, height: de.scrollHeight }, + viewportDimensions: { width: window.innerWidth, height: window.innerHeight }, + scrollPosition: { x: window.pageXOffset || de.scrollLeft, y: window.pageYOffset || de.scrollTop }, + pageVisibility: { visible: !document.hidden, visibilityState: document.visibilityState } + }; +} +function findAllElements(selector, xpath) { + if (selector) { + try { + return Array.from(document.querySelectorAll(selector)); + } catch (e) { + throw new Error(`Invalid selector: ${e.message}`); + } + } + try { + const result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + return Array.from({length: result.snapshotLength}, (_, i) => result.snapshotItem(i)); + } + catch (e) { + throw new Error(`Invalid XPath: ${e.message}`); + } +} +function getElementData(element) { + const rect = element.getBoundingClientRect(); + const computedStyle = window.getComputedStyle(element); + + // Get the selector (which may add an ID to the element) + const selector = getUniqueSelector(element); + + // Comprehensive visibility check + const visible = isElementVisible(element, rect, computedStyle); + + const data = { + tagName: element.tagName.toLowerCase(), + id: element.id || undefined, + className: element.className || undefined, + selector: selector, + bounds: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + visible: visible, + focused: element === document.activeElement, + position: computedStyle.position + }; + // Conditionally add attributes + ["href", "src", "value", "name"].forEach(attr => element[attr] && (data[attr] = element[attr])); + + // If it's a select element, add the options + if (data.tagName === 'select') { + data.options = Array.from(element.options).map((option, optionIndex) => ({ + index: optionIndex, + value: option.value, + text: option.text, + selected: option.selected, + disabled: option.disabled + })); + } + // Add scrollable parent if exists + const scrollParent = findScrollableParent(element); + data.scrollParent = getUniqueSelector(scrollParent); + return data; +} +function isElementVisible(element, rect, computedStyle) { + // If rect and computedStyle not provided, calculate them + if (!rect) rect = element.getBoundingClientRect(); + if (!computedStyle) computedStyle = window.getComputedStyle(element); + + // Check if element has dimensions + if (rect.width <= 0 || rect.height <= 0) return false; + + // Check CSS visibility properties + if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden' || computedStyle.opacity === '0') { + return false; + } + + // Check if element is in viewport + const viewportWidth = window.innerWidth || document.documentElement.clientWidth; + const viewportHeight = window.innerHeight || document.documentElement.clientHeight; + + // Check if any part of the element is within the viewport + const inViewport = rect.bottom > 0 && + rect.right > 0 && + rect.top < viewportHeight && + rect.left < viewportWidth; + + if (!inViewport) return false; + + // Get element's center point (used multiple times below) + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + // Helper function to check if element is visible at a point + const isElementAtPoint = (x, y) => { + const elementAtPoint = document.elementFromPoint(x, y); + if (!elementAtPoint) return false; + return elementAtPoint === element || element.contains(elementAtPoint) || elementAtPoint.contains(element); + }; + + // Check if element is visible at center point - this is the most reliable check + if (isElementAtPoint(centerX, centerY)) return true; + + // Check if element is hidden by ancestor's properties + let parent = element.parentElement; + while (parent && parent !== document.body) { + const parentStyle = window.getComputedStyle(parent); + if (parentStyle.display === 'none' || parentStyle.visibility === 'hidden' || parentStyle.opacity === '0') { + return false; + } + + // Check for overflow hidden that might hide the element + if (parentStyle.overflow === 'hidden' || parentStyle.overflowX === 'hidden' || parentStyle.overflowY === 'hidden') { + const parentRect = parent.getBoundingClientRect(); + // Check if element is outside parent's visible area + if (rect.bottom < parentRect.top || rect.top > parentRect.bottom || rect.right < parentRect.left || rect.left > parentRect.right) { + // Before returning false, check if the element is actually visible using elementsFromPoint + const elementsAtPoint = document.elementsFromPoint(centerX, centerY); + + // If the element is in the elements chain at its center point, it's visible + if (elementsAtPoint.includes(element)) { + continue; // Skip this parent check and continue checking other parents + } + + return false; + } + } + parent = parent.parentElement; + } + + // Element might be partially covered, check multiple points + const points = [ + { x: rect.left + rect.width * 0.1, y: rect.top + rect.height * 0.1 }, + { x: rect.right - rect.width * 0.1, y: rect.top + rect.height * 0.1 }, + { x: rect.left + rect.width * 0.1, y: rect.bottom - rect.height * 0.1 }, + { x: rect.right - rect.width * 0.1, y: rect.bottom - rect.height * 0.1 } + ]; + + // Check if any of the points hit our element + return points.some(point => isElementAtPoint(point.x, point.y)); +} + +function respondWith(obj, selector, xpath) { + return { + success: !obj.error, + selector, + xpath: !selector ? xpath : undefined, + ...getTabInfo(), + ...obj + }; +} +function respondWithError(code, message, selector, xpath) { + return respondWith({ error: { code, message } }, selector, xpath); +} +function elementNotFound(selector, xpath) { + return respondWithError('ELEMENT_NOT_FOUND', 'Element not found', selector, xpath); +} +function requireSelectorOrXpath(selector, xpath) { + return respondWithError('SELECTOR_OR_XPATH_REQUIRED', 'Selector or XPath parameter required', selector, xpath); +} + +const helpers = { + //called by the background script + _navigate: ({ url }) => { + window.location.href = url; + }, + _elementPosition: ({ id }) => { + const element = document.getElementById(id); + return element.getBoundingClientRect(); + }, + _connectionStateChanged: ({ status, connected }) => { + // Remove existing connection classes + document.body.classList.remove('kapture-connected', 'kapture-connecting'); + + // Add appropriate class based on status + if (status === 'connected') { + document.body.classList.add('kapture-connected'); + } else if (status === 'retrying') { + document.body.classList.add('kapture-connecting'); + } + // No class for disconnected state + + return { success: true }; + }, + + // tool calls + getTabInfo, + + /** + * Limits the nesting depth of HTML by replacing deeply nested elements with comments + * indicating how many more layers exist. + */ + limitDOMNesting: (htmlString, maxNesting) => { + if (typeof maxNesting !== 'number' || maxNesting < 0) { + return htmlString; + } + + // Parse the HTML string + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlString, 'text/html'); + + // Get the root element (could be body or a specific element) + const root = doc.body.firstElementChild || doc.body; + + /** + * Counts the maximum nesting depth from a given element + */ + function countDepth(element) { + if (!element || !element.children || element.children.length === 0) { + return 0; + } + let maxChildDepth = 0; + for (const child of element.children) { + maxChildDepth = Math.max(maxChildDepth, countDepth(child)); + } + return 1 + maxChildDepth; + } + + /** + * Recursively processes elements and limits nesting depth + */ + function processElement(element, currentDepth) { + if (!element || !element.children) return; + + // If we've reached the max nesting depth + if (currentDepth >= maxNesting) { + // Count how many more layers exist + const remainingDepth = countDepth(element); + + if (remainingDepth > 0) { + // Replace all children with a comment + const comment = doc.createComment(` ... ${remainingDepth} more layer${remainingDepth === 1 ? '' : 's'} of nesting `); + + // Remove all children + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + // Add the comment + element.appendChild(comment); + } + return; + } + + // Process children recursively + const children = Array.from(element.children); + for (const child of children) { + processElement(child, currentDepth + 1); + } + } + + // Start processing from depth 0 + processElement(root, 0); + + // Return the serialized HTML + return root.outerHTML; + }, + + dom: ({selector, xpath, maxNesting}) => { + let html; + + if (!selector && !xpath) { + html = document.body.outerHTML; + } else { + const element = findAllElements(selector, xpath)[0]; + if (!element) return elementNotFound(selector, xpath); + html = element.outerHTML; + } + + // Apply nesting limit (default is 5) + const nestingLimit = typeof maxNesting === 'number' && maxNesting >= 0 ? maxNesting : 5; + html = window.pageHelpers.limitDOMNesting(html, nestingLimit); + + return respondWith({ html }, selector, xpath); + }, + elementsFromPoint: ({x, y}) => { + if (typeof x !== 'number' || typeof y !== 'number') { + return respondWithError('XY_REQUIRED', 'Both x and y coordinates are required'); + } + const elements = document.elementsFromPoint(x, y); + return respondWith({ x, y, elements: elements.map(getElementData) }); + }, + elements: ({selector, xpath, visible = 'all'}) => { + if (!selector && !xpath) return requireSelectorOrXpath(); + + let elements; + try { + elements = findAllElements(selector, xpath).map(getElementData); + } catch (e) { + const errorCode = selector ? 'INVALID_SELECTOR' : 'INVALID_XPATH'; + return respondWithError(errorCode, e.message, selector, xpath); + } + + // Apply visibility filter + if (visible !== 'all') { + const filterVisible = String(visible) === 'true'; + elements = elements.filter(el => el.visible === filterVisible); + } + return respondWith({elements: elements, visible: visible !== 'all' ? visible : undefined}, selector, xpath); + }, + element: ({selector, xpath, visible = 'all'}) => { + const result = helpers.elements({selector, xpath, visible}); + if (result.error) return result; + if (!result.elements.length) return elementNotFound(selector, xpath); + result.element = result.elements[0]; + delete result.elements; + return result; + }, + focus: ({selector, xpath}) => { + if (!selector && !xpath) return requireSelectorOrXpath(); + + let element; + try { + element = findAllElements(selector, xpath)[0]; + } catch (e) { + const errorCode = selector ? 'INVALID_SELECTOR' : 'INVALID_XPATH'; + return respondWithError(errorCode, e.message, selector, xpath); + } + + if (!element) return elementNotFound(selector, xpath); + + // Focus the element + element.focus(); + + // Check if element is actually focusable + const focusableElements = ['input', 'textarea', 'select', 'button', 'a']; + const tagName = element.tagName.toLowerCase(); + const isFocusable = focusableElements.includes(tagName) || + element.hasAttribute('tabindex') || + element.isContentEditable; + + if (!isFocusable) { + // Still return success but with a warning + return respondWith({ + focused: true, + warning: 'Element may not be focusable' + }, selector, xpath); + } + + return respondWith({ focused: true }, selector, xpath); + }, + fill: ({selector, xpath, value}) => { + if (!selector && !xpath) return requireSelectorOrXpath(); + + const element = findAllElements(selector, xpath)[0]; + if (!element) return elementNotFound(selector, xpath); + + // Check if it's an input element + const tagName = element.tagName.toLowerCase(); + const inputTypes = ['input', 'textarea']; + + if (!inputTypes.includes(tagName) && !element.isContentEditable) { + return respondWithError('INVALID_ELEMENT', 'Element is not fillable: ' + tagName, selector, xpath); + } + + // Focus the element + element.focus(); + + // Clear existing value + if (element.value !== undefined) { + element.value = ''; + } else if (element.isContentEditable) { + element.textContent = ''; + } + + // Set new value + if (element.value !== undefined) { + element.value = value; + } else if (element.isContentEditable) { + element.textContent = value; + } + + // Trigger input and change events + element.dispatchEvent(new Event('input', { bubbles: true })); + element.dispatchEvent(new Event('change', { bubbles: true })); + + // Blur to trigger any blur handlers + element.blur(); + + return respondWith({ filled: true }, selector, xpath); + }, + select: ({selector, xpath, value}) => { + if (!selector && !xpath) return requireSelectorOrXpath(); + + const element = findAllElements(selector, xpath)[0]; + if (!element) return elementNotFound(selector, xpath); + + if (element.tagName !== 'SELECT') { + return respondWithError('INVALID_ELEMENT', 'Element is not fillable: ' + element.name, selector, xpath); + } + + // Find option by value + const option = Array.from(element.options).find(opt => opt.value === value); + if (!option) { + return respondWithError('OPTION_NOT_FOUND', 'Option not found with value: ' + value, selector, xpath); + } + + // Select the option + element.value = value; + option.selected = true; + + // Trigger change event + element.dispatchEvent(new Event('change', { bubbles: true })); + + return respondWith({ selected: true }, selector, xpath); + }, + blur: ({selector, xpath}) => { + if (!selector && !xpath) return requireSelectorOrXpath(); + + let element; + try { + element = findAllElements(selector, xpath)[0]; + } catch (e) { + const errorCode = selector ? 'INVALID_SELECTOR' : 'INVALID_XPATH'; + return respondWithError(errorCode, e.message, selector, xpath); + } + + if (!element) return elementNotFound(selector, xpath); + + // Blur the element + element.blur(); + + // Also remove focus from document.activeElement if it's different + if (document.activeElement && document.activeElement !== element) { + document.activeElement.blur(); + } + + return respondWith({ blurred: true }, selector, xpath); + }, + _cursor: ({show}) => { + const cursorId = 'kapture-cursor'; + let cursor = document.getElementById(cursorId); + + try { + if (show === false) { + // Hide cursor + if (cursor) { + cursor.style.display = 'none'; + } + return respondWith({ visible: false }); + } + + // Show cursor - create if doesn't exist + if (!cursor) { + cursor = document.createElement('div'); + cursor.id = cursorId; + + // Create cursor SVG + cursor.innerHTML = ` + + + + `; + + // Style the cursor container + cursor.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 20px; + height: 20px; + z-index: 2147483647; + pointer-events: none; + transform: translate(-2px, -2px); + transition: none; + will-change: transform; + `; + + document.body.appendChild(cursor); + } + + cursor.style.display = 'block'; + return respondWith({ visible: true }); + } catch (e) { + return respondWithError('CURSOR_ERROR', e.message); + } + }, + _moveMouseSVG: ({x, y}) => { + if (typeof x !== 'number' || typeof y !== 'number') { + return respondWithError('XY_REQUIRED', 'Both x and y coordinates are required'); + } + + try { + const cursor = document.getElementById('kapture-cursor'); + if (!cursor) { + return respondWithError('CURSOR_NOT_FOUND', 'Cursor element not found. Call _cursor with show=true first'); + } + + cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`; + return respondWith({ moved: true, x, y }); + } catch (e) { + return respondWithError('MOVE_MOUSE_SVG_ERROR', e.message); + } + } +}; + +// Mouse position tracking with throttling +let lastMouseSendTime = 0; +const MOUSE_THROTTLE_MS = 50; // Throttle to 20 updates per second + +document.addEventListener('mousemove', (event) => { + const now = Date.now(); + if (now - lastMouseSendTime < MOUSE_THROTTLE_MS) return; + + lastMouseSendTime = now; + chrome.runtime.sendMessage({ + type: 'mousePosition', + x: event.clientX, + y: event.clientY + }); +}); + +// Listen for requests from the extension +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (!request.command) return; + if (helpers[request.command]) { + const result = helpers[request.command](request.params); + Promise.resolve(result).then(sendResponse); + return true; // Keep channel open for async response + } + else { + sendResponse(respondWith({ + error: { code: 'UNKNOWN_COMMAND', message: `Command '${request.command}' not found` } + })); + } +}); diff --git a/extension-kapture/panel.css b/extension-kapture/panel.css new file mode 100644 index 0000000..8dce45e --- /dev/null +++ b/extension-kapture/panel.css @@ -0,0 +1,377 @@ +:root { + --bg-primary: #1e1e1e; + --bg-secondary: #252526; + --bg-tertiary: #2d2d30; + --text-primary: #cccccc; + --text-secondary: #969696; + --border-color: #464647; + --accent-blue: #3794ff; + --accent-green: #4ec9b0; + --accent-orange: #ff9800; + --accent-red: #f44336; + --hover-bg: #2a2a2a; + --selection-bg: #094771; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; + font-size: 13px; + background: var(--bg-primary); + color: var(--text-primary); + overflow: hidden; +} + +/* Header */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); +} + +/* Connection Toggle */ +.connection-toggle { + display: flex; + align-items: center; + gap: 12px; +} + +/* Status */ +.status { + color: var(--text-primary); + font-size: 13px; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; +} + +.status.disconnected { + color: #8ab4f8; +} + +.status.retrying { + color: #fdd663; +} + +.status.connected { + color: #81c995; +} + +/* Toggle switch container */ +.toggle-container { + position: relative; + width: 48px; + height: 24px; + flex-shrink: 0; +} + +/* Hidden checkbox */ +.toggle-input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +/* Toggle switch track */ +.toggle-track { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #5f6368; + transition: all 0.3s ease; + border-radius: 24px; +} + +/* Toggle switch thumb */ +.toggle-track::before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: #e8eaed; + transition: all 0.3s ease; + border-radius: 50%; +} + +/* Checked state */ +.toggle-input:checked + .toggle-track { + background-color: #81c995; +} + +.toggle-input:checked + .toggle-track::before { + transform: translateX(24px); +} + +/* Retrying state - must come after checked state to override */ +.retrying .toggle-input:checked + .toggle-track { + background-color: #5f5034; +} + +.retrying .toggle-input:checked + .toggle-track::before { + background-color: #fdd663; +} + +/* Spinner */ +.spinner { + width: 12px; + height: 12px; + border: 2px solid rgba(253, 214, 99, 0.3); + border-top-color: #fdd663; + border-radius: 50%; + animation: spin 1s linear infinite; + display: none; +} + +.status.retrying .spinner { + display: inline-block; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.tab-info { + color: var(--text-secondary); + font-size: 12px; +} + +/* Messages Container */ +.messages-container { + flex: 1; + display: flex; + flex-direction: column; + height: calc(100vh - 120px); +} + +.messages-header { + display: flex; + padding: 8px 12px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + font-size: 12px; + color: var(--text-secondary); +} + +.messages-header-data { + flex: 1; +} + +.messages-header-time { + width: 80px; + text-align: right; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.clear-messages-button { + background: none; + border: none; + padding: 4px; + cursor: pointer; + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s; +} + +.clear-messages-button:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--text-primary); +} + +/* Messages List */ +.messages-list { + flex: 1; + overflow-y: auto; + background: var(--bg-primary); +} + +.message { + display: flex; + padding: 6px 12px; + border-bottom: 1px solid var(--border-color); + cursor: pointer; + transition: background 0.1s; +} + +.message:hover { + background: var(--hover-bg); +} + +.message.selected { + background: var(--selection-bg); +} + +.message-data { + flex: 1; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.message-arrow { + margin-right: 8px; + font-weight: bold; +} + +.message-arrow.outgoing { + color: var(--accent-blue); +} + +.message-arrow.incoming { + color: var(--accent-green); +} + +.message-time { + width: 80px; + text-align: right; + color: var(--text-secondary); + font-size: 11px; +} + +.empty-state { + align-items: center; + justify-content: center; + height: 200px; + color: var(--text-secondary); + font-style: italic; +} + +/* Show/hide based on parent class */ +.messages-container .empty-state { + display: flex; +} + +.messages-container .messages-list { + display: none; +} + +.messages-container.has-messages .empty-state { + display: none; +} + +.messages-container.has-messages .messages-list { + display: block; +} + +/* Detail View */ +.detail-container { + height: 200px; + display: none; + flex-direction: column; + border-top: 1px solid var(--border-color); +} + +.detail-container.visible { + display: flex; +} + +.resize-handle { + height: 4px; + background: var(--border-color); + cursor: ns-resize; + user-select: none; +} + +.resize-handle:hover { + background: var(--accent-blue); +} + +.detail-content { + flex: 1; + padding: 12px; + overflow: auto; + background: var(--bg-secondary); + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; + font-size: 12px; +} + +.detail-content pre { + margin: 0; + white-space: pre; +} + +.detail-content img { + max-width: 100%; + height: auto; +} + +/* Footer */ +.footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12px; + padding: 8px 12px; + background: var(--bg-secondary); + border-top: 1px solid var(--border-color); +} + +.console-count { + color: var(--text-secondary); + font-size: 12px; +} + +.clear-button { + padding: 4px 12px; + background: transparent; + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-secondary); + cursor: pointer; + font-size: 12px; + transition: all 0.2s; +} + +.clear-button:hover { + background: var(--hover-bg); + color: var(--text-primary); +} + +/* Layout */ +.panel-container { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: var(--bg-primary); +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} \ No newline at end of file diff --git a/extension-kapture/panel.html b/extension-kapture/panel.html new file mode 100644 index 0000000..578cf3c --- /dev/null +++ b/extension-kapture/panel.html @@ -0,0 +1,56 @@ + + + + + + +
    + +
    + Tab: Not connected +
    +
    + + Disconnected +
    + +
    +
    + + +
    +
    +
    Date
    +
    + Time + +
    +
    + +
    No messages yet
    +
    + + +
    +
    +
    +
    +
    + + + +
    + + + + diff --git a/extension-kapture/panel.js b/extension-kapture/panel.js new file mode 100644 index 0000000..805f1f5 --- /dev/null +++ b/extension-kapture/panel.js @@ -0,0 +1,304 @@ +// DevTools Panel UI with Real Connection and Data + +// Debug what you're actually getting +console.log('inspectedWindow.tabId:', chrome.devtools.inspectedWindow.tabId); + +// Let's also check what chrome.tabs.query gives us for comparison +chrome.tabs.query({}, (tabs) => { + console.log('All tabs:', tabs); + + // Find the tab with the same ID + const inspectedTab = tabs.find(tab => tab.id === chrome.devtools.inspectedWindow.tabId); + console.log('Inspected tab details:', inspectedTab); +}); + +// Also check the inspected window URL +console.log('inspectedWindow.url:', chrome.devtools.inspectedWindow.url); + +const tabId = chrome.devtools.inspectedWindow.tabId; +let selectedMessageIndex = -1; +let messages = []; +let consoleLogCount = 0; +let port = null; + +// Initialize UI +function initializeUI() { + // Connect to background script + port = chrome.runtime.connect({ name: 'panel' }); + + // Listen for state updates + port.onMessage.addListener((msg) => { + if (msg.type === 'state' && msg.tabId === tabId) { + updateUI(msg.connected, msg.status); + } else if (msg.type === 'messages' && msg.tabId === tabId) { + // Update messages from background + messages = msg.messages || []; + renderMessages(); + } else if (msg.type === 'consoleCount' && msg.tabId === tabId) { + // Update console count from background + consoleLogCount = msg.count || 0; + updateConsoleCount(); + } + }); + + // Subscribe to state updates for this tab + port.postMessage({ type: 'subscribe', tabId }); + + // Event listeners + document.getElementById('toggle').addEventListener('change', handleToggleChange); + document.getElementById('clear-logs').addEventListener('click', handleClearLogs); + document.getElementById('clear-messages').addEventListener('click', handleClearMessages); + document.getElementById('messages-list').addEventListener('click', handleMessageClick); + document.addEventListener('keydown', handleKeyDown); + + // Resize handle + initializeResizeHandle(); +} + +// Update UI based on connection state +function updateUI(connected, status = 'disconnected') { + const toggle = document.getElementById('toggle'); + const toggleContainer = toggle.parentElement; + const statusEl = document.getElementById('status'); + const statusText = statusEl.querySelector('.status-text'); + const tabInfo = document.getElementById('tab-info'); + + // Remove existing classes + statusEl.classList.remove('connected', 'disconnected', 'retrying'); + toggleContainer.classList.remove('connected', 'disconnected', 'retrying'); + + switch (status) { + case 'connected': + toggle.checked = true; + toggle.disabled = false; + statusEl.classList.add('connected'); + toggleContainer.classList.add('connected'); + statusText.textContent = 'Connected'; + tabInfo.textContent = `Tab: ${tabId} - Connected`; + break; + + case 'retrying': + toggle.checked = true; + toggle.disabled = false; + statusEl.classList.add('retrying'); + toggleContainer.classList.add('retrying'); + statusText.textContent = 'Retrying...'; + tabInfo.textContent = `Tab: ${tabId} - Reconnecting`; + break; + + case 'disconnected': + default: + toggle.checked = false; + toggle.disabled = false; + statusEl.classList.add('disconnected'); + toggleContainer.classList.add('disconnected'); + statusText.textContent = 'Disconnected'; + tabInfo.textContent = 'Tab: Not connected'; + break; + } +} + +// Render messages +function renderMessages() { + const messagesList = document.getElementById('messages-list'); + const messagesContainer = document.querySelector('.messages-container'); + + // Toggle class based on whether we have messages + messagesContainer.classList.toggle('has-messages', messages.length > 0); + + if (messages.length === 0) { + return; + } + + messagesList.innerHTML = ''; + + messages.forEach((msg, index) => { + const messageEl = document.createElement('div'); + messageEl.className = 'message'; + if (index === selectedMessageIndex) { + messageEl.classList.add('selected'); + } + + const arrow = msg.direction === 'outgoing' ? '⬆' : '⬇'; // Up arrow for outgoing, down arrow for incoming + const arrowClass = msg.direction === 'outgoing' ? 'outgoing' : 'incoming'; + + // Show the raw JSON data + const dataText = JSON.stringify(msg.data); + + messageEl.innerHTML = ` +
    + ${arrow} + ${dataText} +
    +
    ${formatTime(msg.timestamp)}
    + `; + + messageEl.dataset.index = index; + messagesList.appendChild(messageEl); + }); + + // Scroll to bottom + messagesList.scrollTop = messagesList.scrollHeight; +} + +// Format timestamp +function formatTime(date) { + // Convert string to Date if needed + const dateObj = typeof date === 'string' ? new Date(date) : date; + return dateObj.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +} + +// Handle message click +function handleMessageClick(e) { + const messageEl = e.target.closest('.message'); + if (!messageEl) return; + + const index = parseInt(messageEl.dataset.index); + selectMessage(index); +} + +// Select message +function selectMessage(index) { + selectedMessageIndex = index; + + // Update selected state + document.querySelectorAll('.message').forEach((el, i) => { + el.classList.toggle('selected', i === index); + }); + + // Show detail view + const detailContainer = document.getElementById('detail-container'); + const detailContent = document.getElementById('detail-content'); + + if (index >= 0 && index < messages.length) { + detailContainer.classList.add('visible'); + const message = messages[index]; + const data = message.data; + + // Check if this is a screenshot response + if (data.type === 'response' && data.success && data.result && data.result.data && + data.result.mimeType && data.result.mimeType.startsWith('image/')) { + // Create image preview for screenshot + const img = document.createElement('img'); + img.src = `data:${data.result.mimeType};base64,${data.result.data}`; + img.style.cssText = 'max-width: 100%; height: auto; cursor: pointer; display: block; border: 1px solid #e0e0e0; border-radius: 4px;'; + img.title = 'Click to open in new tab'; + + // Open in new tab on click + img.addEventListener('click', () => { + window.open(img.src, '_blank'); + }); + + // Show image and data + detailContent.innerHTML = ''; + detailContent.appendChild(img); + + // Add the rest of the data below the image + const dataInfo = document.createElement('pre'); + dataInfo.textContent = JSON.stringify(data.result, null, 2); + dataInfo.style.cssText = 'margin-top: 10px; font-size: 12px; overflow: auto;'; + detailContent.appendChild(dataInfo); + } else { + // Show only the actual message data, not the wrapper + detailContent.innerHTML = ''; + const pre = document.createElement('pre'); + pre.style.cssText = 'margin: 0; font-size: 12px; overflow: auto;'; + pre.textContent = JSON.stringify(message.data, null, 2); + detailContent.appendChild(pre); + } + } else { + detailContainer.classList.remove('visible'); + } +} + +// Handle keyboard navigation +function handleKeyDown(e) { + if (e.key === 'ArrowUp' && selectedMessageIndex > 0) { + selectMessage(selectedMessageIndex - 1); + e.preventDefault(); + } else if (e.key === 'ArrowDown' && selectedMessageIndex < messages.length - 1) { + selectMessage(selectedMessageIndex + 1); + e.preventDefault(); + } else if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + // Clear messages + handleClearMessages(); + e.preventDefault(); + } +} + +// Handle toggle change +function handleToggleChange(e) { + const checked = e.target.checked; + + chrome.runtime.sendMessage( + { + type: checked ? 'connect' : 'disconnect', + tabId: tabId + }, + (response) => { + if (response?.error) { + console.error('Toggle error:', response.error); + } + } + ); +} + +// Handle clear logs +function handleClearLogs() { + // Request background to clear console logs + port.postMessage({ type: 'clearConsoleLogs', tabId }); +} + +// Handle clear messages +function handleClearMessages() { + // Request background to clear messages + port.postMessage({ type: 'clearMessages', tabId }); + + // Clear the detail view + const detailContainer = document.getElementById('detail-container'); + detailContainer.classList.remove('visible'); + selectedMessageIndex = -1; +} + +// Update console count +function updateConsoleCount() { + document.getElementById('console-count').textContent = `Console: ${consoleLogCount}`; +} + +// Initialize resize handle +function initializeResizeHandle() { + const resizeHandle = document.getElementById('resize-handle'); + const detailContainer = document.getElementById('detail-container'); + let isResizing = false; + let startY = 0; + let startHeight = 0; + + resizeHandle.addEventListener('mousedown', (e) => { + isResizing = true; + startY = e.clientY; + startHeight = detailContainer.offsetHeight; + document.body.style.cursor = 'ns-resize'; + e.preventDefault(); + }); + + document.addEventListener('mousemove', (e) => { + if (!isResizing) return; + + const deltaY = startY - e.clientY; + const newHeight = Math.min(Math.max(100, startHeight + deltaY), 500); + detailContainer.style.height = `${newHeight}px`; + }); + + document.addEventListener('mouseup', () => { + isResizing = false; + document.body.style.cursor = ''; + }); +} + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', initializeUI); diff --git a/extension-kapture/popup.html b/extension-kapture/popup.html new file mode 100644 index 0000000..ddc805d --- /dev/null +++ b/extension-kapture/popup.html @@ -0,0 +1,130 @@ + + + + + + +
    + + Disconnected +
    + + + + \ No newline at end of file diff --git a/extension-kapture/popup.js b/extension-kapture/popup.js new file mode 100644 index 0000000..8716435 --- /dev/null +++ b/extension-kapture/popup.js @@ -0,0 +1,80 @@ +// Popup UI + +let tabId; +let port; +let isUpdatingUI = false; + +// Get current tab and set up +chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + tabId = tabs[0].id; + + // Connect to background + port = chrome.runtime.connect(); + port.postMessage({ type: 'subscribe', tabId }); + + // Listen for state updates + port.onMessage.addListener((msg) => { + if (msg.type === 'state' && msg.tabId === tabId) { + updateUI(msg.connected, msg.status); + } + }); + + // Get initial state + chrome.runtime.sendMessage({ type: 'getState', tabId }, (state) => { + updateUI(state.connected, state.status); + }); +}); + +// Handle toggle switch +document.getElementById('toggle').addEventListener('change', (e) => { + if (isUpdatingUI) return; // Prevent feedback loop + + if (e.target.checked) { + chrome.runtime.sendMessage({ type: 'connect', tabId }); + } else { + chrome.runtime.sendMessage({ type: 'disconnect', tabId }); + } +}); + +// Update UI +function updateUI(connected, status = 'disconnected') { + isUpdatingUI = true; + + const toggle = document.getElementById('toggle'); + const toggleContainer = toggle.parentElement; + const statusEl = document.getElementById('status'); + const statusText = statusEl.querySelector('.status-text'); + + // Remove all state classes + statusEl.classList.remove('connected', 'disconnected', 'retrying'); + toggleContainer.classList.remove('connected', 'disconnected', 'retrying'); + + switch (status) { + case 'connected': + toggle.checked = true; + toggle.disabled = false; + statusEl.classList.add('connected'); + toggleContainer.classList.add('connected'); + statusText.textContent = 'Connected'; + break; + + case 'retrying': + toggle.checked = true; + toggle.disabled = false; + statusEl.classList.add('retrying'); + toggleContainer.classList.add('retrying'); + statusText.textContent = 'Connecting'; + break; + + case 'disconnected': + default: + toggle.checked = false; + toggle.disabled = false; + statusEl.classList.add('disconnected'); + toggleContainer.classList.add('disconnected'); + statusText.textContent = 'Disconnected'; + break; + } + + setTimeout(() => { isUpdatingUI = false; }, 100); +} \ No newline at end of file diff --git a/extension-playwright/README.md b/extension-playwright/README.md new file mode 100644 index 0000000..6421798 --- /dev/null +++ b/extension-playwright/README.md @@ -0,0 +1,48 @@ +# Playwright MCP Chrome Extension + +## Introduction + +The Playwright MCP Chrome Extension allows you to connect to pages in your existing browser and leverage the state of your default user profile. This means the AI assistant can interact with websites where you're already logged in, using your existing cookies, sessions, and browser state, providing a seamless experience without requiring separate authentication or setup. + +## Prerequisites + +- Chrome/Edge/Chromium browser + +## Installation Steps + +### Download the Extension + +Download the latest Chrome extension from GitHub: +- **Download link**: https://github.com/microsoft/playwright-mcp/releases + +### Load Chrome Extension + +1. Open Chrome and navigate to `chrome://extensions/` +2. Enable "Developer mode" (toggle in the top right corner) +3. Click "Load unpacked" and select the extension directory + +### Configure Playwright MCP server + +Configure Playwright MCP server to connect to the browser using the extension by passing the `--extension` option when running the MCP server: + +```json +{ + "mcpServers": { + "playwright-extension": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--extension" + ] + } + } +} +``` + +## Usage + +### Browser Tab Selection + +When the LLM interacts with the browser for the first time, it will load a page where you can select which browser tab the LLM will connect to. This allows you to control which specific page the AI assistant will interact with during the session. + + diff --git a/extension-playwright/icons/icon-128.png b/extension-playwright/icons/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..c4bc8b02508b40cec8a0d73465324a9612744172 GIT binary patch literal 6352 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 JPDHLkV1gAA4qyNP literal 0 HcmV?d00001 diff --git a/extension-playwright/icons/icon-32.png b/extension-playwright/icons/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9a8ccb89bdbccd6bdc1c7f945f72eed9b94017 GIT binary patch literal 1258 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 Z{soKhKGhw*_GbV9002ovPDHLkV1gda&R_rl literal 0 HcmV?d00001 diff --git a/extension-playwright/manifest.json b/extension-playwright/manifest.json new file mode 100644 index 0000000..2efe81e --- /dev/null +++ b/extension-playwright/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 3, + "name": "Playwright MCP Bridge", + "version": "0.0.46", + "description": "Share browser tabs with Playwright MCP server", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB", + "permissions": [ + "debugger", + "activeTab", + "tabs", + "storage" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "lib/background.mjs", + "type": "module" + }, + "action": { + "default_title": "Playwright MCP Bridge", + "default_icon": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } + }, + "icons": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } +} diff --git a/extension-playwright/package-lock.json b/extension-playwright/package-lock.json new file mode 100644 index 0000000..313e31c --- /dev/null +++ b/extension-playwright/package-lock.json @@ -0,0 +1,1885 @@ +{ + "name": "@playwright/mcp-extension", + "version": "0.0.46", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@playwright/mcp-extension", + "version": "0.0.46", + "license": "Apache-2.0", + "devDependencies": { + "@types/chrome": "^0.0.315", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.8.2", + "vite": "^5.4.21", + "vite-plugin-static-copy": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.1.tgz", + "integrity": "sha512-oENme6QxtLCqjChRUUo3S6X8hjCXnWmJWnedD7VbGML5GUtaOtAyx+fEEXnBXVf0CBZApMQU0Idwi0FmyxzQhw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.1.tgz", + "integrity": "sha512-OikvNT3qYTl9+4qQ9Bpn6+XHM+ogtFadRLuT2EXiFQMiNkXFLQfNVppi5o28wvYdHL2s3fM0D/MZJ8UkNFZWsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.1.tgz", + "integrity": "sha512-EFYNNGij2WllnzljQDQnlFTXzSJw87cpAs4TVBAWLdkvic5Uh5tISrIL6NRcxoh/b2EFBG/TK8hgRrGx94zD4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.1.tgz", + "integrity": "sha512-ZaNH06O1KeTug9WI2+GRBE5Ujt9kZw4a1+OIwnBHal92I8PxSsl5KpsrPvthRynkhMck4XPdvY0z26Cym/b7oA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.1.tgz", + "integrity": "sha512-n4SLVebZP8uUlJ2r04+g2U/xFeiQlw09Me5UFqny8HGbARl503LNH5CqFTb5U5jNxTouhRjai6qPT0CR5c/Iig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.1.tgz", + "integrity": "sha512-8vu9c02F16heTqpvo3yeiu7Vi1REDEC/yES/dIfq3tSXe6mLndiwvYr3AAvd1tMNUqE9yeGYa5w7PRbI5QUV+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.1.tgz", + "integrity": "sha512-K4ncpWl7sQuyp6rWiGUvb6Q18ba8mzM0rjWJ5JgYKlIXAau1db7hZnR0ldJvqKWWJDxqzSLwGUhA4jp+KqgDtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.1.tgz", + "integrity": "sha512-YykPnXsjUjmXE6j6k2QBBGAn1YsJUix7pYaPLK3RVE0bQL2jfdbfykPxfF8AgBlqtYbfEnYHmLXNa6QETjdOjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.1.tgz", + "integrity": "sha512-kKvqBGbZ8i9pCGW3a1FH3HNIVg49dXXTsChGFsHGXQaVJPLA4f/O+XmTxfklhccxdF5FefUn2hvkoGJH0ScWOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.1.tgz", + "integrity": "sha512-zzX5nTw1N1plmqC9RGC9vZHFuiM7ZP7oSWQGqpbmfjK7p947D518cVK1/MQudsBdcD84t6k70WNczJOct6+hdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.1.tgz", + "integrity": "sha512-O8CwgSBo6ewPpktFfSDgB6SJN9XDcPSvuwxfejiddbIC/hn9Tg6Ai0f0eYDf3XvB/+PIWzOQL+7+TZoB8p9Yuw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.1.tgz", + "integrity": "sha512-JnCfFVEKeq6G3h3z8e60kAp8Rd7QVnWCtPm7cxx+5OtP80g/3nmPtfdCXbVl063e3KsRnGSKDHUQMydmzc/wBA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.1.tgz", + "integrity": "sha512-dVxuDqS237eQXkbYzQQfdf/njgeNw6LZuVyEdUaWwRpKHhsLI+y4H/NJV8xJGU19vnOJCVwaBFgr936FHOnJsQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.1.tgz", + "integrity": "sha512-CvvgNl2hrZrTR9jXK1ye0Go0HQRT6ohQdDfWR47/KFKiLd5oN5T14jRdUVGF4tnsN8y9oSfMOqH6RuHh+ck8+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.1.tgz", + "integrity": "sha512-x7ANt2VOg2565oGHJ6rIuuAon+A8sfe1IeUx25IKqi49OjSr/K3awoNqr9gCwGEJo9OuXlOn+H2p1VJKx1psxA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.1.tgz", + "integrity": "sha512-9OADZYryz/7E8/qt0vnaHQgmia2Y0wrjSSn1V/uL+zw/i7NUhxbX4cHXdEQ7dnJgzYDS81d8+tf6nbIdRFZQoQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.1.tgz", + "integrity": "sha512-NuvSCbXEKY+NGWHyivzbjSVJi68Xfq1VnIvGmsuXs6TCtveeoDRKutI5vf2ntmNnVq64Q4zInet0UDQ+yMB6tA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.1.tgz", + "integrity": "sha512-mWz+6FSRb82xuUMMV1X3NGiaPFqbLN9aIueHleTZCc46cJvwTlvIh7reQLk4p97dv0nddyewBhwzryBHH7wtPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.1.tgz", + "integrity": "sha512-7Thzy9TMXDw9AU4f4vsLNBxh7/VOKuXi73VH3d/kHGr0tZ3x/ewgL9uC7ojUKmH1/zvmZe2tLapYcZllk3SO8Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.1.tgz", + "integrity": "sha512-7GVB4luhFmGUNXXJhH2jJwZCFB3pIOixv2E3s17GQHBFUOQaISlt7aGcQgqvCaDSxTZJUzlK/QJ1FN8S94MrzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.315", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.315.tgz", + "integrity": "sha512-Oy1dYWkr6BCmgwBtOngLByCHstQ3whltZg7/7lubgIZEYvKobDneqplgc6LKERNRBwckFviV4UU5AZZNUFrJ4A==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rollup": { + "version": "4.46.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.1.tgz", + "integrity": "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.1", + "@rollup/rollup-android-arm64": "4.46.1", + "@rollup/rollup-darwin-arm64": "4.46.1", + "@rollup/rollup-darwin-x64": "4.46.1", + "@rollup/rollup-freebsd-arm64": "4.46.1", + "@rollup/rollup-freebsd-x64": "4.46.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.1", + "@rollup/rollup-linux-arm-musleabihf": "4.46.1", + "@rollup/rollup-linux-arm64-gnu": "4.46.1", + "@rollup/rollup-linux-arm64-musl": "4.46.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.1", + "@rollup/rollup-linux-ppc64-gnu": "4.46.1", + "@rollup/rollup-linux-riscv64-gnu": "4.46.1", + "@rollup/rollup-linux-riscv64-musl": "4.46.1", + "@rollup/rollup-linux-s390x-gnu": "4.46.1", + "@rollup/rollup-linux-x64-gnu": "4.46.1", + "@rollup/rollup-linux-x64-musl": "4.46.1", + "@rollup/rollup-win32-arm64-msvc": "4.46.1", + "@rollup/rollup-win32-ia32-msvc": "4.46.1", + "@rollup/rollup-win32-x64-msvc": "4.46.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-static-copy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.1.tgz", + "integrity": "sha512-oR53SkL5cX4KT1t18E/xU50vJDo0N8oaHza4EMk0Fm+2/u6nQivxavOfrDk3udWj+dizRizB/QnBvJOOQrTTAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "fs-extra": "^11.3.0", + "p-map": "^7.0.3", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.14" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/extension-playwright/package.json b/extension-playwright/package.json new file mode 100644 index 0000000..e37e112 --- /dev/null +++ b/extension-playwright/package.json @@ -0,0 +1,35 @@ +{ + "name": "@playwright/mcp-extension", + "version": "0.0.46", + "description": "Playwright MCP Browser Extension", + "private": true, + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/playwright-mcp.git" + }, + "homepage": "https://playwright.dev", + "engines": { + "node": ">=18" + }, + "author": { + "name": "Microsoft Corporation" + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc --project . && tsc --project tsconfig.ui.json && vite build && vite build --config vite.sw.config.mts", + "watch": "tsc --watch --project . & tsc --watch --project tsconfig.ui.json & vite build --watch & vite build --watch --config vite.sw.config.mts", + "test": "playwright test", + "clean": "rm -rf dist" + }, + "devDependencies": { + "@types/chrome": "^0.0.315", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.8.2", + "vite": "^5.4.21", + "vite-plugin-static-copy": "^3.1.1" + } +} diff --git a/extension-playwright/playwright.config.ts b/extension-playwright/playwright.config.ts new file mode 100644 index 0000000..7bf4f0e --- /dev/null +++ b/extension-playwright/playwright.config.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineConfig } from '@playwright/test'; + +import type { TestOptions } from '../tests/fixtures'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'list', + projects: [ + { name: 'chromium', use: { mcpBrowser: 'chromium' } }, + ], +}); diff --git a/extension-playwright/src/background.ts b/extension-playwright/src/background.ts new file mode 100644 index 0000000..e74ebb8 --- /dev/null +++ b/extension-playwright/src/background.ts @@ -0,0 +1,222 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RelayConnection, debugLog } from './relayConnection'; + +type PageMessage = { + type: 'connectToMCPRelay'; + mcpRelayUrl: string; +} | { + type: 'getTabs'; +} | { + type: 'connectToTab'; + tabId?: number; + windowId?: number; + mcpRelayUrl: string; +} | { + type: 'getConnectionStatus'; +} | { + type: 'disconnect'; +}; + +class TabShareExtension { + private _activeConnection: RelayConnection | undefined; + private _connectedTabId: number | null = null; + private _pendingTabSelection = new Map(); + + constructor() { + chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this)); + chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this)); + chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this)); + chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); + chrome.action.onClicked.addListener(this._onActionClicked.bind(this)); + } + + // Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031 + private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) { + switch (message.type) { + case 'connectToMCPRelay': + this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl).then( + () => sendResponse({ success: true }), + (error: any) => sendResponse({ success: false, error: error.message })); + return true; + case 'getTabs': + this._getTabs().then( + tabs => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }), + (error: any) => sendResponse({ success: false, error: error.message })); + return true; + case 'connectToTab': + const tabId = message.tabId || sender.tab?.id!; + const windowId = message.windowId || sender.tab?.windowId!; + this._connectTab(sender.tab!.id!, tabId, windowId, message.mcpRelayUrl!).then( + () => sendResponse({ success: true }), + (error: any) => sendResponse({ success: false, error: error.message })); + return true; // Return true to indicate that the response will be sent asynchronously + case 'getConnectionStatus': + sendResponse({ + connectedTabId: this._connectedTabId + }); + return false; + case 'disconnect': + this._disconnect().then( + () => sendResponse({ success: true }), + (error: any) => sendResponse({ success: false, error: error.message })); + return true; + } + return false; + } + + private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise { + try { + debugLog(`Connecting to relay at ${mcpRelayUrl}`); + const socket = new WebSocket(mcpRelayUrl); + await new Promise((resolve, reject) => { + socket.onopen = () => resolve(); + socket.onerror = () => reject(new Error('WebSocket error')); + setTimeout(() => reject(new Error('Connection timeout')), 5000); + }); + + const connection = new RelayConnection(socket); + connection.onclose = () => { + debugLog('Connection closed'); + this._pendingTabSelection.delete(selectorTabId); + // TODO: show error in the selector tab? + }; + this._pendingTabSelection.set(selectorTabId, { connection }); + debugLog(`Connected to MCP relay`); + } catch (error: any) { + const message = `Failed to connect to MCP relay: ${error.message}`; + debugLog(message); + throw new Error(message); + } + } + + private async _connectTab(selectorTabId: number, tabId: number, windowId: number, mcpRelayUrl: string): Promise { + try { + debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`); + try { + this._activeConnection?.close('Another connection is requested'); + } catch (error: any) { + debugLog(`Error closing active connection:`, error); + } + await this._setConnectedTabId(null); + + this._activeConnection = this._pendingTabSelection.get(selectorTabId)?.connection; + if (!this._activeConnection) + throw new Error('No active MCP relay connection'); + this._pendingTabSelection.delete(selectorTabId); + + this._activeConnection.setTabId(tabId); + this._activeConnection.onclose = () => { + debugLog('MCP connection closed'); + this._activeConnection = undefined; + void this._setConnectedTabId(null); + }; + + await Promise.all([ + this._setConnectedTabId(tabId), + chrome.tabs.update(tabId, { active: true }), + chrome.windows.update(windowId, { focused: true }), + ]); + debugLog(`Connected to MCP bridge`); + } catch (error: any) { + await this._setConnectedTabId(null); + debugLog(`Failed to connect tab ${tabId}:`, error.message); + throw error; + } + } + + private async _setConnectedTabId(tabId: number | null): Promise { + const oldTabId = this._connectedTabId; + this._connectedTabId = tabId; + if (oldTabId && oldTabId !== tabId) + await this._updateBadge(oldTabId, { text: '' }); + if (tabId) + await this._updateBadge(tabId, { text: '✓', color: '#4CAF50', title: 'Connected to MCP client' }); + } + + private async _updateBadge(tabId: number, { text, color, title }: { text: string; color?: string, title?: string }): Promise { + try { + await chrome.action.setBadgeText({ tabId, text }); + await chrome.action.setTitle({ tabId, title: title || '' }); + if (color) + await chrome.action.setBadgeBackgroundColor({ tabId, color }); + } catch (error: any) { + // Ignore errors as the tab may be closed already. + } + } + + private async _onTabRemoved(tabId: number): Promise { + const pendingConnection = this._pendingTabSelection.get(tabId)?.connection; + if (pendingConnection) { + this._pendingTabSelection.delete(tabId); + pendingConnection.close('Browser tab closed'); + return; + } + if (this._connectedTabId !== tabId) + return; + this._activeConnection?.close('Browser tab closed'); + this._activeConnection = undefined; + this._connectedTabId = null; + } + + private _onTabActivated(activeInfo: chrome.tabs.TabActiveInfo) { + for (const [tabId, pending] of this._pendingTabSelection) { + if (tabId === activeInfo.tabId) { + if (pending.timerId) { + clearTimeout(pending.timerId); + pending.timerId = undefined; + } + continue; + } + if (!pending.timerId) { + pending.timerId = setTimeout(() => { + const existed = this._pendingTabSelection.delete(tabId); + if (existed) { + pending.connection.close('Tab has been inactive for 5 seconds'); + chrome.tabs.sendMessage(tabId, { type: 'connectionTimeout' }); + } + }, 5000); + return; + } + } + } + + private _onTabUpdated(tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) { + if (this._connectedTabId === tabId) + void this._setConnectedTabId(tabId); + } + + private async _getTabs(): Promise { + const tabs = await chrome.tabs.query({}); + return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme))); + } + + private async _onActionClicked(): Promise { + await chrome.tabs.create({ + url: chrome.runtime.getURL('status.html'), + active: true + }); + } + + private async _disconnect(): Promise { + this._activeConnection?.close('User disconnected'); + this._activeConnection = undefined; + await this._setConnectedTabId(null); + } +} + +new TabShareExtension(); diff --git a/extension-playwright/src/relayConnection.ts b/extension-playwright/src/relayConnection.ts new file mode 100644 index 0000000..b203af4 --- /dev/null +++ b/extension-playwright/src/relayConnection.ts @@ -0,0 +1,178 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function debugLog(...args: unknown[]): void { + const enabled = true; + if (enabled) { + // eslint-disable-next-line no-console + console.log('[Extension]', ...args); + } +} + +type ProtocolCommand = { + id: number; + method: string; + params?: any; +}; + +type ProtocolResponse = { + id?: number; + method?: string; + params?: any; + result?: any; + error?: string; +}; + +export class RelayConnection { + private _debuggee: chrome.debugger.Debuggee; + private _ws: WebSocket; + private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void; + private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void; + private _tabPromise: Promise; + private _tabPromiseResolve!: () => void; + private _closed = false; + + onclose?: () => void; + + constructor(ws: WebSocket) { + this._debuggee = { }; + this._tabPromise = new Promise(resolve => this._tabPromiseResolve = resolve); + this._ws = ws; + this._ws.onmessage = this._onMessage.bind(this); + this._ws.onclose = () => this._onClose(); + // Store listeners for cleanup + this._eventListener = this._onDebuggerEvent.bind(this); + this._detachListener = this._onDebuggerDetach.bind(this); + chrome.debugger.onEvent.addListener(this._eventListener); + chrome.debugger.onDetach.addListener(this._detachListener); + } + + // Either setTabId or close is called after creating the connection. + setTabId(tabId: number): void { + this._debuggee = { tabId }; + this._tabPromiseResolve(); + } + + close(message: string): void { + this._ws.close(1000, message); + // ws.onclose is called asynchronously, so we call it here to avoid forwarding + // CDP events to the closed connection. + this._onClose(); + } + + private _onClose() { + if (this._closed) + return; + this._closed = true; + chrome.debugger.onEvent.removeListener(this._eventListener); + chrome.debugger.onDetach.removeListener(this._detachListener); + chrome.debugger.detach(this._debuggee).catch(() => {}); + this.onclose?.(); + } + + private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void { + if (source.tabId !== this._debuggee.tabId) + return; + debugLog('Forwarding CDP event:', method, params); + const sessionId = source.sessionId; + this._sendMessage({ + method: 'forwardCDPEvent', + params: { + sessionId, + method, + params, + }, + }); + } + + private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void { + if (source.tabId !== this._debuggee.tabId) + return; + this.close(`Debugger detached: ${reason}`); + this._debuggee = { }; + } + + private _onMessage(event: MessageEvent): void { + this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e)); + } + + private async _onMessageAsync(event: MessageEvent): Promise { + let message: ProtocolCommand; + try { + message = JSON.parse(event.data); + } catch (error: any) { + debugLog('Error parsing message:', error); + this._sendError(-32700, `Error parsing message: ${error.message}`); + return; + } + + debugLog('Received message:', message); + + const response: ProtocolResponse = { + id: message.id, + }; + try { + response.result = await this._handleCommand(message); + } catch (error: any) { + debugLog('Error handling command:', error); + response.error = error.message; + } + debugLog('Sending response:', response); + this._sendMessage(response); + } + + private async _handleCommand(message: ProtocolCommand): Promise { + if (message.method === 'attachToTab') { + await this._tabPromise; + debugLog('Attaching debugger to tab:', this._debuggee); + await chrome.debugger.attach(this._debuggee, '1.3'); + const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo'); + return { + targetInfo: result?.targetInfo, + }; + } + if (!this._debuggee.tabId) + throw new Error('No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.'); + if (message.method === 'forwardCDPCommand') { + const { sessionId, method, params } = message.params; + debugLog('CDP command:', method, params); + const debuggerSession: chrome.debugger.DebuggerSession = { + ...this._debuggee, + sessionId, + }; + // Forward CDP command to chrome.debugger + return await chrome.debugger.sendCommand( + debuggerSession, + method, + params + ); + } + } + + private _sendError(code: number, message: string): void { + this._sendMessage({ + error: { + code, + message, + }, + }); + } + + private _sendMessage(message: any): void { + if (this._ws.readyState === WebSocket.OPEN) + this._ws.send(JSON.stringify(message)); + } +} diff --git a/extension-playwright/src/ui/authToken.css b/extension-playwright/src/ui/authToken.css new file mode 100644 index 0000000..adfa30c --- /dev/null +++ b/extension-playwright/src/ui/authToken.css @@ -0,0 +1,142 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.auth-token-section { + margin: 16px 0; + padding: 16px; + background-color: #f6f8fa; + border-radius: 6px; +} + +.auth-token-description { + font-size: 12px; + color: #656d76; + margin-bottom: 12px; +} + +.auth-token-container { + display: flex; + align-items: center; + gap: 8px; + background-color: #ffffff; + padding: 8px; +} + +.auth-token-code { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 12px; + color: #1f2328; + border: none; + flex: 1; + padding: 0; + word-break: break-all; +} + +.auth-token-refresh { + flex: none; + height: 24px; + width: 24px; + border: none; + outline: none; + color: var(--color-fg-muted); + background: transparent; + padding: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 4px; +} + +.auth-token-refresh svg { + margin: 0; +} + +.auth-token-refresh:not(:disabled):hover { + background-color: var(--color-btn-selected-bg); +} + +.auth-token-example-section { + margin-top: 16px; +} + +.auth-token-example-toggle { + display: flex; + align-items: center; + gap: 8px; + background: none; + border: none; + padding: 8px 0; + font-size: 12px; + color: #656d76; + cursor: pointer; + outline: none; + text-align: left; + width: 100%; +} + +.auth-token-example-toggle:hover { + color: #1f2328; +} + +.auth-token-chevron { + display: inline-flex; + align-items: center; + justify-content: center; + transform: rotate(-90deg); + flex-shrink: 0; +} + +.auth-token-chevron.expanded { + transform: rotate(0deg); +} + +.auth-token-chevron svg { + width: 12px; + height: 12px; +} + +.auth-token-chevron .octicon { + margin: 0px; +} + +.auth-token-example-content { + margin-top: 12px; + padding: 12px 0; +} + +.auth-token-example-description { + font-size: 12px; + color: #656d76; + margin-bottom: 12px; +} + +.auth-token-example-config { + display: flex; + align-items: flex-start; + gap: 8px; + background-color: #ffffff; + padding: 12px; +} + +.auth-token-example-code { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 11px; + color: #1f2328; + white-space: pre; + flex: 1; + line-height: 1.4; +} diff --git a/extension-playwright/src/ui/authToken.tsx b/extension-playwright/src/ui/authToken.tsx new file mode 100644 index 0000000..699cf2f --- /dev/null +++ b/extension-playwright/src/ui/authToken.tsx @@ -0,0 +1,118 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useState } from 'react'; +import { CopyToClipboard } from './copyToClipboard'; +import * as icons from './icons'; +import './authToken.css'; + +export const AuthTokenSection: React.FC<{}> = ({}) => { + const [authToken, setAuthToken] = useState(getOrCreateAuthToken); + const [isExampleExpanded, setIsExampleExpanded] = useState(false); + + const onRegenerateToken = useCallback(() => { + const newToken = generateAuthToken(); + localStorage.setItem('auth-token', newToken); + setAuthToken(newToken); + }, []); + + const toggleExample = useCallback(() => { + setIsExampleExpanded(!isExampleExpanded); + }, [isExampleExpanded]); + + return ( +
    +
    + Set this environment variable to bypass the connection dialog: +
    +
    + {authTokenCode(authToken)} + + +
    + +
    + + + {isExampleExpanded && ( +
    +
    + Add this configuration to your MCP client (e.g., VS Code) to connect to the Playwright MCP Bridge: +
    +
    + {exampleConfig(authToken)} + +
    +
    + )} +
    +
    + ); +}; + +function authTokenCode(authToken: string) { + return `PLAYWRIGHT_MCP_EXTENSION_TOKEN=${authToken}`; +} + +function exampleConfig(authToken: string) { + return `{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest", "--extension"], + "env": { + "PLAYWRIGHT_MCP_EXTENSION_TOKEN": + "${authToken}" + } + } + } +}`; +} + +function generateAuthToken(): string { + // Generate a cryptographically secure random token + const array = new Uint8Array(32); + crypto.getRandomValues(array); + // Convert to base64 and make it URL-safe + return btoa(String.fromCharCode.apply(null, Array.from(array))) + .replace(/[+/=]/g, match => { + switch (match) { + case '+': return '-'; + case '/': return '_'; + case '=': return ''; + default: return match; + } + }); +} + +export const getOrCreateAuthToken = (): string => { + let token = localStorage.getItem('auth-token'); + if (!token) { + token = generateAuthToken(); + localStorage.setItem('auth-token', token); + } + return token; +} diff --git a/extension-playwright/src/ui/colors.css b/extension-playwright/src/ui/colors.css new file mode 100644 index 0000000..42b254f --- /dev/null +++ b/extension-playwright/src/ui/colors.css @@ -0,0 +1,891 @@ +/* The MIT License (MIT) + +Copyright (c) 2021 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +:root { + --color-canvas-default-transparent: rgba(255,255,255,0); + --color-marketing-icon-primary: #218bff; + --color-marketing-icon-secondary: #54aeff; + --color-diff-blob-addition-num-text: #24292f; + --color-diff-blob-addition-fg: #24292f; + --color-diff-blob-addition-num-bg: #CCFFD8; + --color-diff-blob-addition-line-bg: #E6FFEC; + --color-diff-blob-addition-word-bg: #ABF2BC; + --color-diff-blob-deletion-num-text: #24292f; + --color-diff-blob-deletion-fg: #24292f; + --color-diff-blob-deletion-num-bg: #FFD7D5; + --color-diff-blob-deletion-line-bg: #FFEBE9; + --color-diff-blob-deletion-word-bg: rgba(255,129,130,0.4); + --color-diff-blob-hunk-num-bg: rgba(84,174,255,0.4); + --color-diff-blob-expander-icon: #57606a; + --color-diff-blob-selected-line-highlight-mix-blend-mode: multiply; + --color-diffstat-deletion-border: rgba(27,31,36,0.15); + --color-diffstat-addition-border: rgba(27,31,36,0.15); + --color-diffstat-addition-bg: #2da44e; + --color-search-keyword-hl: #fff8c5; + --color-prettylights-syntax-comment: #6e7781; + --color-prettylights-syntax-constant: #0550ae; + --color-prettylights-syntax-entity: #8250df; + --color-prettylights-syntax-storage-modifier-import: #24292f; + --color-prettylights-syntax-entity-tag: #116329; + --color-prettylights-syntax-keyword: #cf222e; + --color-prettylights-syntax-string: #0a3069; + --color-prettylights-syntax-variable: #953800; + --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; + --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; + --color-prettylights-syntax-invalid-illegal-bg: #82071e; + --color-prettylights-syntax-carriage-return-text: #f6f8fa; + --color-prettylights-syntax-carriage-return-bg: #cf222e; + --color-prettylights-syntax-string-regexp: #116329; + --color-prettylights-syntax-markup-list: #3b2300; + --color-prettylights-syntax-markup-heading: #0550ae; + --color-prettylights-syntax-markup-italic: #24292f; + --color-prettylights-syntax-markup-bold: #24292f; + --color-prettylights-syntax-markup-deleted-text: #82071e; + --color-prettylights-syntax-markup-deleted-bg: #FFEBE9; + --color-prettylights-syntax-markup-inserted-text: #116329; + --color-prettylights-syntax-markup-inserted-bg: #dafbe1; + --color-prettylights-syntax-markup-changed-text: #953800; + --color-prettylights-syntax-markup-changed-bg: #ffd8b5; + --color-prettylights-syntax-markup-ignored-text: #eaeef2; + --color-prettylights-syntax-markup-ignored-bg: #0550ae; + --color-prettylights-syntax-meta-diff-range: #8250df; + --color-prettylights-syntax-brackethighlighter-angle: #57606a; + --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; + --color-prettylights-syntax-constant-other-reference-link: #0a3069; + --color-codemirror-text: #24292f; + --color-codemirror-bg: #ffffff; + --color-codemirror-gutters-bg: #ffffff; + --color-codemirror-guttermarker-text: #ffffff; + --color-codemirror-guttermarker-subtle-text: #6e7781; + --color-codemirror-linenumber-text: #57606a; + --color-codemirror-cursor: #24292f; + --color-codemirror-selection-bg: rgba(84,174,255,0.4); + --color-codemirror-activeline-bg: rgba(234,238,242,0.5); + --color-codemirror-matchingbracket-text: #24292f; + --color-codemirror-lines-bg: #ffffff; + --color-codemirror-syntax-comment: #24292f; + --color-codemirror-syntax-constant: #0550ae; + --color-codemirror-syntax-entity: #8250df; + --color-codemirror-syntax-keyword: #cf222e; + --color-codemirror-syntax-storage: #cf222e; + --color-codemirror-syntax-string: #0a3069; + --color-codemirror-syntax-support: #0550ae; + --color-codemirror-syntax-variable: #953800; + --color-checks-bg: #24292f; + --color-checks-run-border-width: 0px; + --color-checks-container-border-width: 0px; + --color-checks-text-primary: #f6f8fa; + --color-checks-text-secondary: #8c959f; + --color-checks-text-link: #54aeff; + --color-checks-btn-icon: #afb8c1; + --color-checks-btn-hover-icon: #f6f8fa; + --color-checks-btn-hover-bg: rgba(255,255,255,0.125); + --color-checks-input-text: #eaeef2; + --color-checks-input-placeholder-text: #8c959f; + --color-checks-input-focus-text: #8c959f; + --color-checks-input-bg: #32383f; + --color-checks-input-shadow: none; + --color-checks-donut-error: #fa4549; + --color-checks-donut-pending: #bf8700; + --color-checks-donut-success: #2da44e; + --color-checks-donut-neutral: #afb8c1; + --color-checks-dropdown-text: #afb8c1; + --color-checks-dropdown-bg: #32383f; + --color-checks-dropdown-border: #424a53; + --color-checks-dropdown-shadow: rgba(27,31,36,0.3); + --color-checks-dropdown-hover-text: #f6f8fa; + --color-checks-dropdown-hover-bg: #424a53; + --color-checks-dropdown-btn-hover-text: #f6f8fa; + --color-checks-dropdown-btn-hover-bg: #32383f; + --color-checks-scrollbar-thumb-bg: #57606a; + --color-checks-header-label-text: #d0d7de; + --color-checks-header-label-open-text: #f6f8fa; + --color-checks-header-border: #32383f; + --color-checks-header-icon: #8c959f; + --color-checks-line-text: #d0d7de; + --color-checks-line-num-text: rgba(140,149,159,0.75); + --color-checks-line-timestamp-text: #8c959f; + --color-checks-line-hover-bg: #32383f; + --color-checks-line-selected-bg: rgba(33,139,255,0.15); + --color-checks-line-selected-num-text: #54aeff; + --color-checks-line-dt-fm-text: #24292f; + --color-checks-line-dt-fm-bg: #9a6700; + --color-checks-gate-bg: rgba(125,78,0,0.15); + --color-checks-gate-text: #d0d7de; + --color-checks-gate-waiting-text: #afb8c1; + --color-checks-step-header-open-bg: #32383f; + --color-checks-step-error-text: #ff8182; + --color-checks-step-warning-text: #d4a72c; + --color-checks-logline-text: #8c959f; + --color-checks-logline-num-text: rgba(140,149,159,0.75); + --color-checks-logline-debug-text: #c297ff; + --color-checks-logline-error-text: #d0d7de; + --color-checks-logline-error-num-text: #ff8182; + --color-checks-logline-error-bg: rgba(164,14,38,0.15); + --color-checks-logline-warning-text: #d0d7de; + --color-checks-logline-warning-num-text: #d4a72c; + --color-checks-logline-warning-bg: rgba(125,78,0,0.15); + --color-checks-logline-command-text: #54aeff; + --color-checks-logline-section-text: #4ac26b; + --color-checks-ansi-black: #24292f; + --color-checks-ansi-black-bright: #32383f; + --color-checks-ansi-white: #d0d7de; + --color-checks-ansi-white-bright: #d0d7de; + --color-checks-ansi-gray: #8c959f; + --color-checks-ansi-red: #ff8182; + --color-checks-ansi-red-bright: #ffaba8; + --color-checks-ansi-green: #4ac26b; + --color-checks-ansi-green-bright: #6fdd8b; + --color-checks-ansi-yellow: #d4a72c; + --color-checks-ansi-yellow-bright: #eac54f; + --color-checks-ansi-blue: #54aeff; + --color-checks-ansi-blue-bright: #80ccff; + --color-checks-ansi-magenta: #c297ff; + --color-checks-ansi-magenta-bright: #d8b9ff; + --color-checks-ansi-cyan: #76e3ea; + --color-checks-ansi-cyan-bright: #b3f0ff; + --color-project-header-bg: #24292f; + --color-project-sidebar-bg: #ffffff; + --color-project-gradient-in: #ffffff; + --color-project-gradient-out: rgba(255,255,255,0); + --color-mktg-success: rgba(36,146,67,1); + --color-mktg-info: rgba(19,119,234,1); + --color-mktg-bg-shade-gradient-top: rgba(27,31,36,0.065); + --color-mktg-bg-shade-gradient-bottom: rgba(27,31,36,0); + --color-mktg-btn-bg-top: hsla(228,82%,66%,1); + --color-mktg-btn-bg-bottom: #4969ed; + --color-mktg-btn-bg-overlay-top: hsla(228,74%,59%,1); + --color-mktg-btn-bg-overlay-bottom: #3355e0; + --color-mktg-btn-text: #ffffff; + --color-mktg-btn-primary-bg-top: hsla(137,56%,46%,1); + --color-mktg-btn-primary-bg-bottom: #2ea44f; + --color-mktg-btn-primary-bg-overlay-top: hsla(134,60%,38%,1); + --color-mktg-btn-primary-bg-overlay-bottom: #22863a; + --color-mktg-btn-primary-text: #ffffff; + --color-mktg-btn-enterprise-bg-top: hsla(249,100%,72%,1); + --color-mktg-btn-enterprise-bg-bottom: #6f57ff; + --color-mktg-btn-enterprise-bg-overlay-top: hsla(248,65%,63%,1); + --color-mktg-btn-enterprise-bg-overlay-bottom: #614eda; + --color-mktg-btn-enterprise-text: #ffffff; + --color-mktg-btn-outline-text: #4969ed; + --color-mktg-btn-outline-border: rgba(73,105,237,0.3); + --color-mktg-btn-outline-hover-text: #3355e0; + --color-mktg-btn-outline-hover-border: rgba(51,85,224,0.5); + --color-mktg-btn-outline-focus-border: #4969ed; + --color-mktg-btn-outline-focus-border-inset: rgba(73,105,237,0.5); + --color-mktg-btn-dark-text: #ffffff; + --color-mktg-btn-dark-border: rgba(255,255,255,0.3); + --color-mktg-btn-dark-hover-text: #ffffff; + --color-mktg-btn-dark-hover-border: rgba(255,255,255,0.5); + --color-mktg-btn-dark-focus-border: #ffffff; + --color-mktg-btn-dark-focus-border-inset: rgba(255,255,255,0.5); + --color-avatar-bg: #ffffff; + --color-avatar-border: rgba(27,31,36,0.15); + --color-avatar-stack-fade: #afb8c1; + --color-avatar-stack-fade-more: #d0d7de; + --color-avatar-child-shadow: -2px -2px 0 rgba(255,255,255,0.8); + --color-topic-tag-border: rgba(0,0,0,0); + --color-select-menu-backdrop-border: rgba(0,0,0,0); + --color-select-menu-tap-highlight: rgba(175,184,193,0.5); + --color-select-menu-tap-focus-bg: #b6e3ff; + --color-overlay-shadow: 0 1px 3px rgba(27,31,36,0.12), 0 8px 24px rgba(66,74,83,0.12); + --color-header-text: rgba(255,255,255,0.7); + --color-header-bg: #24292f; + --color-header-logo: #ffffff; + --color-header-search-bg: #24292f; + --color-header-search-border: #57606a; + --color-sidenav-selected-bg: #ffffff; + --color-menu-bg-active: rgba(0,0,0,0); + --color-control-transparent-bg-hover: #818b981a; + --color-input-disabled-bg: rgba(175,184,193,0.2); + --color-timeline-badge-bg: #eaeef2; + --color-ansi-black: #24292f; + --color-ansi-black-bright: #57606a; + --color-ansi-white: #6e7781; + --color-ansi-white-bright: #8c959f; + --color-ansi-gray: #6e7781; + --color-ansi-red: #cf222e; + --color-ansi-red-bright: #a40e26; + --color-ansi-green: #116329; + --color-ansi-green-bright: #1a7f37; + --color-ansi-yellow: #4d2d00; + --color-ansi-yellow-bright: #633c01; + --color-ansi-blue: #0969da; + --color-ansi-blue-bright: #218bff; + --color-ansi-magenta: #8250df; + --color-ansi-magenta-bright: #a475f9; + --color-ansi-cyan: #1b7c83; + --color-ansi-cyan-bright: #3192aa; + --color-btn-text: #24292f; + --color-btn-bg: #f6f8fa; + --color-btn-border: rgba(27,31,36,0.15); + --color-btn-shadow: 0 1px 0 rgba(27,31,36,0.04); + --color-btn-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.25); + --color-btn-hover-bg: #f3f4f6; + --color-btn-hover-border: rgba(27,31,36,0.15); + --color-btn-active-bg: hsla(220,14%,93%,1); + --color-btn-active-border: rgba(27,31,36,0.15); + --color-btn-selected-bg: hsla(220,14%,94%,1); + --color-btn-focus-bg: #f6f8fa; + --color-btn-focus-border: rgba(27,31,36,0.15); + --color-btn-focus-shadow: 0 0 0 3px rgba(9,105,218,0.3); + --color-btn-shadow-active: inset 0 0.15em 0.3em rgba(27,31,36,0.15); + --color-btn-shadow-input-focus: 0 0 0 0.2em rgba(9,105,218,0.3); + --color-btn-counter-bg: rgba(27,31,36,0.08); + --color-btn-primary-text: #ffffff; + --color-btn-primary-bg: #2da44e; + --color-btn-primary-border: rgba(27,31,36,0.15); + --color-btn-primary-shadow: 0 1px 0 rgba(27,31,36,0.1); + --color-btn-primary-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03); + --color-btn-primary-hover-bg: #2c974b; + --color-btn-primary-hover-border: rgba(27,31,36,0.15); + --color-btn-primary-selected-bg: hsla(137,55%,36%,1); + --color-btn-primary-selected-shadow: inset 0 1px 0 rgba(0,45,17,0.2); + --color-btn-primary-disabled-text: rgba(255,255,255,0.8); + --color-btn-primary-disabled-bg: #94d3a2; + --color-btn-primary-disabled-border: rgba(27,31,36,0.15); + --color-btn-primary-focus-bg: #2da44e; + --color-btn-primary-focus-border: rgba(27,31,36,0.15); + --color-btn-primary-focus-shadow: 0 0 0 3px rgba(45,164,78,0.4); + --color-btn-primary-icon: rgba(255,255,255,0.8); + --color-btn-primary-counter-bg: rgba(255,255,255,0.2); + --color-btn-outline-text: #0969da; + --color-btn-outline-hover-text: #ffffff; + --color-btn-outline-hover-bg: #0969da; + --color-btn-outline-hover-border: rgba(27,31,36,0.15); + --color-btn-outline-hover-shadow: 0 1px 0 rgba(27,31,36,0.1); + --color-btn-outline-hover-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03); + --color-btn-outline-hover-counter-bg: rgba(255,255,255,0.2); + --color-btn-outline-selected-text: #ffffff; + --color-btn-outline-selected-bg: hsla(212,92%,42%,1); + --color-btn-outline-selected-border: rgba(27,31,36,0.15); + --color-btn-outline-selected-shadow: inset 0 1px 0 rgba(0,33,85,0.2); + --color-btn-outline-disabled-text: rgba(9,105,218,0.5); + --color-btn-outline-disabled-bg: #f6f8fa; + --color-btn-outline-disabled-counter-bg: rgba(9,105,218,0.05); + --color-btn-outline-focus-border: rgba(27,31,36,0.15); + --color-btn-outline-focus-shadow: 0 0 0 3px rgba(5,80,174,0.4); + --color-btn-outline-counter-bg: rgba(9,105,218,0.1); + --color-btn-danger-text: #cf222e; + --color-btn-danger-hover-text: #ffffff; + --color-btn-danger-hover-bg: #a40e26; + --color-btn-danger-hover-border: rgba(27,31,36,0.15); + --color-btn-danger-hover-shadow: 0 1px 0 rgba(27,31,36,0.1); + --color-btn-danger-hover-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03); + --color-btn-danger-hover-counter-bg: rgba(255,255,255,0.2); + --color-btn-danger-selected-text: #ffffff; + --color-btn-danger-selected-bg: hsla(356,72%,44%,1); + --color-btn-danger-selected-border: rgba(27,31,36,0.15); + --color-btn-danger-selected-shadow: inset 0 1px 0 rgba(76,0,20,0.2); + --color-btn-danger-disabled-text: rgba(207,34,46,0.5); + --color-btn-danger-disabled-bg: #f6f8fa; + --color-btn-danger-disabled-counter-bg: rgba(207,34,46,0.05); + --color-btn-danger-focus-border: rgba(27,31,36,0.15); + --color-btn-danger-focus-shadow: 0 0 0 3px rgba(164,14,38,0.4); + --color-btn-danger-counter-bg: rgba(207,34,46,0.1); + --color-btn-danger-icon: #cf222e; + --color-btn-danger-hover-icon: #ffffff; + --color-underlinenav-icon: #6e7781; + --color-underlinenav-border-hover: rgba(175,184,193,0.2); + --color-fg-default: #24292f; + --color-fg-muted: #57606a; + --color-fg-subtle: #6e7781; + --color-fg-on-emphasis: #ffffff; + --color-canvas-default: #ffffff; + --color-canvas-overlay: #ffffff; + --color-canvas-inset: #f6f8fa; + --color-canvas-subtle: #f6f8fa; + --color-border-default: #d0d7de; + --color-border-muted: hsla(210,18%,87%,1); + --color-border-subtle: rgba(27,31,36,0.15); + --color-shadow-small: 0 1px 0 rgba(27,31,36,0.04); + --color-shadow-medium: 0 3px 6px rgba(140,149,159,0.15); + --color-shadow-large: 0 8px 24px rgba(140,149,159,0.2); + --color-shadow-extra-large: 0 12px 28px rgba(140,149,159,0.3); + --color-neutral-emphasis-plus: #24292f; + --color-neutral-emphasis: #6e7781; + --color-neutral-muted: rgba(175,184,193,0.2); + --color-neutral-subtle: rgba(234,238,242,0.5); + --color-accent-fg: #0969da; + --color-accent-emphasis: #0969da; + --color-accent-muted: rgba(84,174,255,0.4); + --color-accent-subtle: #ddf4ff; + --color-success-fg: #1a7f37; + --color-success-emphasis: #2da44e; + --color-success-muted: rgba(74,194,107,0.4); + --color-success-subtle: #dafbe1; + --color-attention-fg: #9a6700; + --color-attention-emphasis: #bf8700; + --color-attention-muted: rgba(212,167,44,0.4); + --color-attention-subtle: #fff8c5; + --color-severe-fg: #bc4c00; + --color-severe-emphasis: #bc4c00; + --color-severe-muted: rgba(251,143,68,0.4); + --color-severe-subtle: #fff1e5; + --color-danger-fg: #cf222e; + --color-danger-emphasis: #cf222e; + --color-danger-muted: rgba(255,129,130,0.4); + --color-danger-subtle: #FFEBE9; + --color-done-fg: #8250df; + --color-done-emphasis: #8250df; + --color-done-muted: rgba(194,151,255,0.4); + --color-done-subtle: #fbefff; + --color-sponsors-fg: #bf3989; + --color-sponsors-emphasis: #bf3989; + --color-sponsors-muted: rgba(255,128,200,0.4); + --color-sponsors-subtle: #ffeff7; + --color-primer-canvas-backdrop: rgba(27,31,36,0.5); + --color-primer-canvas-sticky: rgba(255,255,255,0.95); + --color-primer-border-active: #FD8C73; + --color-primer-border-contrast: rgba(27,31,36,0.1); + --color-primer-shadow-highlight: inset 0 1px 0 rgba(255,255,255,0.25); + --color-primer-shadow-inset: inset 0 1px 0 rgba(208,215,222,0.2); + --color-primer-shadow-focus: 0 0 0 3px rgba(9,105,218,0.3); + --color-scale-black: #1b1f24; + --color-scale-white: #ffffff; + --color-scale-gray-0: #f6f8fa; + --color-scale-gray-1: #eaeef2; + --color-scale-gray-2: #d0d7de; + --color-scale-gray-3: #afb8c1; + --color-scale-gray-4: #8c959f; + --color-scale-gray-5: #6e7781; + --color-scale-gray-6: #57606a; + --color-scale-gray-7: #424a53; + --color-scale-gray-8: #32383f; + --color-scale-gray-9: #24292f; + --color-scale-blue-0: #ddf4ff; + --color-scale-blue-1: #b6e3ff; + --color-scale-blue-2: #80ccff; + --color-scale-blue-3: #54aeff; + --color-scale-blue-4: #218bff; + --color-scale-blue-5: #0969da; + --color-scale-blue-6: #0550ae; + --color-scale-blue-7: #033d8b; + --color-scale-blue-8: #0a3069; + --color-scale-blue-9: #002155; + --color-scale-green-0: #dafbe1; + --color-scale-green-1: #aceebb; + --color-scale-green-2: #6fdd8b; + --color-scale-green-3: #4ac26b; + --color-scale-green-4: #2da44e; + --color-scale-green-5: #1a7f37; + --color-scale-green-6: #116329; + --color-scale-green-7: #044f1e; + --color-scale-green-8: #003d16; + --color-scale-green-9: #002d11; + --color-scale-yellow-0: #fff8c5; + --color-scale-yellow-1: #fae17d; + --color-scale-yellow-2: #eac54f; + --color-scale-yellow-3: #d4a72c; + --color-scale-yellow-4: #bf8700; + --color-scale-yellow-5: #9a6700; + --color-scale-yellow-6: #7d4e00; + --color-scale-yellow-7: #633c01; + --color-scale-yellow-8: #4d2d00; + --color-scale-yellow-9: #3b2300; + --color-scale-orange-0: #fff1e5; + --color-scale-orange-1: #ffd8b5; + --color-scale-orange-2: #ffb77c; + --color-scale-orange-3: #fb8f44; + --color-scale-orange-4: #e16f24; + --color-scale-orange-5: #bc4c00; + --color-scale-orange-6: #953800; + --color-scale-orange-7: #762c00; + --color-scale-orange-8: #5c2200; + --color-scale-orange-9: #471700; + --color-scale-red-0: #FFEBE9; + --color-scale-red-1: #ffcecb; + --color-scale-red-2: #ffaba8; + --color-scale-red-3: #ff8182; + --color-scale-red-4: #fa4549; + --color-scale-red-5: #cf222e; + --color-scale-red-6: #a40e26; + --color-scale-red-7: #82071e; + --color-scale-red-8: #660018; + --color-scale-red-9: #4c0014; + --color-scale-purple-0: #fbefff; + --color-scale-purple-1: #ecd8ff; + --color-scale-purple-2: #d8b9ff; + --color-scale-purple-3: #c297ff; + --color-scale-purple-4: #a475f9; + --color-scale-purple-5: #8250df; + --color-scale-purple-6: #6639ba; + --color-scale-purple-7: #512a97; + --color-scale-purple-8: #3e1f79; + --color-scale-purple-9: #2e1461; + --color-scale-pink-0: #ffeff7; + --color-scale-pink-1: #ffd3eb; + --color-scale-pink-2: #ffadda; + --color-scale-pink-3: #ff80c8; + --color-scale-pink-4: #e85aad; + --color-scale-pink-5: #bf3989; + --color-scale-pink-6: #99286e; + --color-scale-pink-7: #772057; + --color-scale-pink-8: #611347; + --color-scale-pink-9: #4d0336; + --color-scale-coral-0: #FFF0EB; + --color-scale-coral-1: #FFD6CC; + --color-scale-coral-2: #FFB4A1; + --color-scale-coral-3: #FD8C73; + --color-scale-coral-4: #EC6547; + --color-scale-coral-5: #C4432B; + --color-scale-coral-6: #9E2F1C; + --color-scale-coral-7: #801F0F; + --color-scale-coral-8: #691105; + --color-scale-coral-9: #510901 +} + +@media(prefers-color-scheme: dark) { + :root { + --color-canvas-default-transparent: rgba(13,17,23,0); + --color-marketing-icon-primary: #79c0ff; + --color-marketing-icon-secondary: #1f6feb; + --color-diff-blob-addition-num-text: #c9d1d9; + --color-diff-blob-addition-fg: #c9d1d9; + --color-diff-blob-addition-num-bg: rgba(63,185,80,0.3); + --color-diff-blob-addition-line-bg: rgba(46,160,67,0.15); + --color-diff-blob-addition-word-bg: rgba(46,160,67,0.4); + --color-diff-blob-deletion-num-text: #c9d1d9; + --color-diff-blob-deletion-fg: #c9d1d9; + --color-diff-blob-deletion-num-bg: rgba(248,81,73,0.3); + --color-diff-blob-deletion-line-bg: rgba(248,81,73,0.15); + --color-diff-blob-deletion-word-bg: rgba(248,81,73,0.4); + --color-diff-blob-hunk-num-bg: rgba(56,139,253,0.4); + --color-diff-blob-expander-icon: #8b949e; + --color-diff-blob-selected-line-highlight-mix-blend-mode: screen; + --color-diffstat-deletion-border: rgba(240,246,252,0.1); + --color-diffstat-addition-border: rgba(240,246,252,0.1); + --color-diffstat-addition-bg: #3fb950; + --color-search-keyword-hl: rgba(210,153,34,0.4); + --color-prettylights-syntax-comment: #8b949e; + --color-prettylights-syntax-constant: #79c0ff; + --color-prettylights-syntax-entity: #d2a8ff; + --color-prettylights-syntax-storage-modifier-import: #c9d1d9; + --color-prettylights-syntax-entity-tag: #7ee787; + --color-prettylights-syntax-keyword: #ff7b72; + --color-prettylights-syntax-string: #a5d6ff; + --color-prettylights-syntax-variable: #ffa657; + --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; + --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; + --color-prettylights-syntax-invalid-illegal-bg: #8e1519; + --color-prettylights-syntax-carriage-return-text: #f0f6fc; + --color-prettylights-syntax-carriage-return-bg: #b62324; + --color-prettylights-syntax-string-regexp: #7ee787; + --color-prettylights-syntax-markup-list: #f2cc60; + --color-prettylights-syntax-markup-heading: #1f6feb; + --color-prettylights-syntax-markup-italic: #c9d1d9; + --color-prettylights-syntax-markup-bold: #c9d1d9; + --color-prettylights-syntax-markup-deleted-text: #ffdcd7; + --color-prettylights-syntax-markup-deleted-bg: #67060c; + --color-prettylights-syntax-markup-inserted-text: #aff5b4; + --color-prettylights-syntax-markup-inserted-bg: #033a16; + --color-prettylights-syntax-markup-changed-text: #ffdfb6; + --color-prettylights-syntax-markup-changed-bg: #5a1e02; + --color-prettylights-syntax-markup-ignored-text: #c9d1d9; + --color-prettylights-syntax-markup-ignored-bg: #1158c7; + --color-prettylights-syntax-meta-diff-range: #d2a8ff; + --color-prettylights-syntax-brackethighlighter-angle: #8b949e; + --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; + --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; + --color-codemirror-text: #c9d1d9; + --color-codemirror-bg: #0d1117; + --color-codemirror-gutters-bg: #0d1117; + --color-codemirror-guttermarker-text: #0d1117; + --color-codemirror-guttermarker-subtle-text: #484f58; + --color-codemirror-linenumber-text: #8b949e; + --color-codemirror-cursor: #c9d1d9; + --color-codemirror-selection-bg: rgba(56,139,253,0.4); + --color-codemirror-activeline-bg: rgba(110,118,129,0.1); + --color-codemirror-matchingbracket-text: #c9d1d9; + --color-codemirror-lines-bg: #0d1117; + --color-codemirror-syntax-comment: #8b949e; + --color-codemirror-syntax-constant: #79c0ff; + --color-codemirror-syntax-entity: #d2a8ff; + --color-codemirror-syntax-keyword: #ff7b72; + --color-codemirror-syntax-storage: #ff7b72; + --color-codemirror-syntax-string: #a5d6ff; + --color-codemirror-syntax-support: #79c0ff; + --color-codemirror-syntax-variable: #ffa657; + --color-checks-bg: #010409; + --color-checks-run-border-width: 1px; + --color-checks-container-border-width: 1px; + --color-checks-text-primary: #c9d1d9; + --color-checks-text-secondary: #8b949e; + --color-checks-text-link: #58a6ff; + --color-checks-btn-icon: #8b949e; + --color-checks-btn-hover-icon: #c9d1d9; + --color-checks-btn-hover-bg: rgba(110,118,129,0.1); + --color-checks-input-text: #8b949e; + --color-checks-input-placeholder-text: #484f58; + --color-checks-input-focus-text: #c9d1d9; + --color-checks-input-bg: #161b22; + --color-checks-input-shadow: none; + --color-checks-donut-error: #f85149; + --color-checks-donut-pending: #d29922; + --color-checks-donut-success: #2ea043; + --color-checks-donut-neutral: #8b949e; + --color-checks-dropdown-text: #c9d1d9; + --color-checks-dropdown-bg: #161b22; + --color-checks-dropdown-border: #30363d; + --color-checks-dropdown-shadow: rgba(1,4,9,0.3); + --color-checks-dropdown-hover-text: #c9d1d9; + --color-checks-dropdown-hover-bg: rgba(110,118,129,0.1); + --color-checks-dropdown-btn-hover-text: #c9d1d9; + --color-checks-dropdown-btn-hover-bg: rgba(110,118,129,0.1); + --color-checks-scrollbar-thumb-bg: rgba(110,118,129,0.4); + --color-checks-header-label-text: #8b949e; + --color-checks-header-label-open-text: #c9d1d9; + --color-checks-header-border: #21262d; + --color-checks-header-icon: #8b949e; + --color-checks-line-text: #8b949e; + --color-checks-line-num-text: #484f58; + --color-checks-line-timestamp-text: #484f58; + --color-checks-line-hover-bg: rgba(110,118,129,0.1); + --color-checks-line-selected-bg: rgba(56,139,253,0.15); + --color-checks-line-selected-num-text: #58a6ff; + --color-checks-line-dt-fm-text: #f0f6fc; + --color-checks-line-dt-fm-bg: #9e6a03; + --color-checks-gate-bg: rgba(187,128,9,0.15); + --color-checks-gate-text: #8b949e; + --color-checks-gate-waiting-text: #d29922; + --color-checks-step-header-open-bg: #161b22; + --color-checks-step-error-text: #f85149; + --color-checks-step-warning-text: #d29922; + --color-checks-logline-text: #8b949e; + --color-checks-logline-num-text: #484f58; + --color-checks-logline-debug-text: #a371f7; + --color-checks-logline-error-text: #8b949e; + --color-checks-logline-error-num-text: #484f58; + --color-checks-logline-error-bg: rgba(248,81,73,0.15); + --color-checks-logline-warning-text: #8b949e; + --color-checks-logline-warning-num-text: #d29922; + --color-checks-logline-warning-bg: rgba(187,128,9,0.15); + --color-checks-logline-command-text: #58a6ff; + --color-checks-logline-section-text: #3fb950; + --color-checks-ansi-black: #0d1117; + --color-checks-ansi-black-bright: #161b22; + --color-checks-ansi-white: #b1bac4; + --color-checks-ansi-white-bright: #b1bac4; + --color-checks-ansi-gray: #6e7681; + --color-checks-ansi-red: #ff7b72; + --color-checks-ansi-red-bright: #ffa198; + --color-checks-ansi-green: #3fb950; + --color-checks-ansi-green-bright: #56d364; + --color-checks-ansi-yellow: #d29922; + --color-checks-ansi-yellow-bright: #e3b341; + --color-checks-ansi-blue: #58a6ff; + --color-checks-ansi-blue-bright: #79c0ff; + --color-checks-ansi-magenta: #bc8cff; + --color-checks-ansi-magenta-bright: #d2a8ff; + --color-checks-ansi-cyan: #76e3ea; + --color-checks-ansi-cyan-bright: #b3f0ff; + --color-project-header-bg: #0d1117; + --color-project-sidebar-bg: #161b22; + --color-project-gradient-in: #161b22; + --color-project-gradient-out: rgba(22,27,34,0); + --color-mktg-success: rgba(41,147,61,1); + --color-mktg-info: rgba(42,123,243,1); + --color-mktg-bg-shade-gradient-top: rgba(1,4,9,0.065); + --color-mktg-bg-shade-gradient-bottom: rgba(1,4,9,0); + --color-mktg-btn-bg-top: hsla(228,82%,66%,1); + --color-mktg-btn-bg-bottom: #4969ed; + --color-mktg-btn-bg-overlay-top: hsla(228,74%,59%,1); + --color-mktg-btn-bg-overlay-bottom: #3355e0; + --color-mktg-btn-text: #f0f6fc; + --color-mktg-btn-primary-bg-top: hsla(137,56%,46%,1); + --color-mktg-btn-primary-bg-bottom: #2ea44f; + --color-mktg-btn-primary-bg-overlay-top: hsla(134,60%,38%,1); + --color-mktg-btn-primary-bg-overlay-bottom: #22863a; + --color-mktg-btn-primary-text: #f0f6fc; + --color-mktg-btn-enterprise-bg-top: hsla(249,100%,72%,1); + --color-mktg-btn-enterprise-bg-bottom: #6f57ff; + --color-mktg-btn-enterprise-bg-overlay-top: hsla(248,65%,63%,1); + --color-mktg-btn-enterprise-bg-overlay-bottom: #614eda; + --color-mktg-btn-enterprise-text: #f0f6fc; + --color-mktg-btn-outline-text: #f0f6fc; + --color-mktg-btn-outline-border: rgba(240,246,252,0.3); + --color-mktg-btn-outline-hover-text: #f0f6fc; + --color-mktg-btn-outline-hover-border: rgba(240,246,252,0.5); + --color-mktg-btn-outline-focus-border: #f0f6fc; + --color-mktg-btn-outline-focus-border-inset: rgba(240,246,252,0.5); + --color-mktg-btn-dark-text: #f0f6fc; + --color-mktg-btn-dark-border: rgba(240,246,252,0.3); + --color-mktg-btn-dark-hover-text: #f0f6fc; + --color-mktg-btn-dark-hover-border: rgba(240,246,252,0.5); + --color-mktg-btn-dark-focus-border: #f0f6fc; + --color-mktg-btn-dark-focus-border-inset: rgba(240,246,252,0.5); + --color-avatar-bg: rgba(240,246,252,0.1); + --color-avatar-border: rgba(240,246,252,0.1); + --color-avatar-stack-fade: #30363d; + --color-avatar-stack-fade-more: #21262d; + --color-avatar-child-shadow: -2px -2px 0 #0d1117; + --color-topic-tag-border: rgba(0,0,0,0); + --color-select-menu-backdrop-border: #484f58; + --color-select-menu-tap-highlight: rgba(48,54,61,0.5); + --color-select-menu-tap-focus-bg: #0c2d6b; + --color-overlay-shadow: 0 0 0 1px #30363d, 0 16px 32px rgba(1,4,9,0.85); + --color-header-text: rgba(240,246,252,0.7); + --color-header-bg: #161b22; + --color-header-logo: #f0f6fc; + --color-header-search-bg: #0d1117; + --color-header-search-border: #30363d; + --color-sidenav-selected-bg: #21262d; + --color-menu-bg-active: #161b22; + --color-control-transparent-bg-hover: #656c7633; + --color-input-disabled-bg: rgba(110,118,129,0); + --color-timeline-badge-bg: #21262d; + --color-ansi-black: #484f58; + --color-ansi-black-bright: #6e7681; + --color-ansi-white: #b1bac4; + --color-ansi-white-bright: #f0f6fc; + --color-ansi-gray: #6e7681; + --color-ansi-red: #ff7b72; + --color-ansi-red-bright: #ffa198; + --color-ansi-green: #3fb950; + --color-ansi-green-bright: #56d364; + --color-ansi-yellow: #d29922; + --color-ansi-yellow-bright: #e3b341; + --color-ansi-blue: #58a6ff; + --color-ansi-blue-bright: #79c0ff; + --color-ansi-magenta: #bc8cff; + --color-ansi-magenta-bright: #d2a8ff; + --color-ansi-cyan: #39c5cf; + --color-ansi-cyan-bright: #56d4dd; + --color-btn-text: #c9d1d9; + --color-btn-bg: #21262d; + --color-btn-border: rgba(240,246,252,0.1); + --color-btn-shadow: 0 0 transparent; + --color-btn-inset-shadow: 0 0 transparent; + --color-btn-hover-bg: #30363d; + --color-btn-hover-border: #8b949e; + --color-btn-active-bg: hsla(212,12%,18%,1); + --color-btn-active-border: #6e7681; + --color-btn-selected-bg: #161b22; + --color-btn-focus-bg: #21262d; + --color-btn-focus-border: #8b949e; + --color-btn-focus-shadow: 0 0 0 3px rgba(139,148,158,0.3); + --color-btn-shadow-active: inset 0 0.15em 0.3em rgba(1,4,9,0.15); + --color-btn-shadow-input-focus: 0 0 0 0.2em rgba(31,111,235,0.3); + --color-btn-counter-bg: #30363d; + --color-btn-primary-text: #ffffff; + --color-btn-primary-bg: #238636; + --color-btn-primary-border: rgba(240,246,252,0.1); + --color-btn-primary-shadow: 0 0 transparent; + --color-btn-primary-inset-shadow: 0 0 transparent; + --color-btn-primary-hover-bg: #2ea043; + --color-btn-primary-hover-border: rgba(240,246,252,0.1); + --color-btn-primary-selected-bg: #238636; + --color-btn-primary-selected-shadow: 0 0 transparent; + --color-btn-primary-disabled-text: rgba(240,246,252,0.5); + --color-btn-primary-disabled-bg: rgba(35,134,54,0.6); + --color-btn-primary-disabled-border: rgba(240,246,252,0.1); + --color-btn-primary-focus-bg: #238636; + --color-btn-primary-focus-border: rgba(240,246,252,0.1); + --color-btn-primary-focus-shadow: 0 0 0 3px rgba(46,164,79,0.4); + --color-btn-primary-icon: #f0f6fc; + --color-btn-primary-counter-bg: rgba(240,246,252,0.2); + --color-btn-outline-text: #58a6ff; + --color-btn-outline-hover-text: #58a6ff; + --color-btn-outline-hover-bg: #30363d; + --color-btn-outline-hover-border: rgba(240,246,252,0.1); + --color-btn-outline-hover-shadow: 0 1px 0 rgba(1,4,9,0.1); + --color-btn-outline-hover-inset-shadow: inset 0 1px 0 rgba(240,246,252,0.03); + --color-btn-outline-hover-counter-bg: rgba(240,246,252,0.2); + --color-btn-outline-selected-text: #f0f6fc; + --color-btn-outline-selected-bg: #0d419d; + --color-btn-outline-selected-border: rgba(240,246,252,0.1); + --color-btn-outline-selected-shadow: 0 0 transparent; + --color-btn-outline-disabled-text: rgba(88,166,255,0.5); + --color-btn-outline-disabled-bg: #0d1117; + --color-btn-outline-disabled-counter-bg: rgba(31,111,235,0.05); + --color-btn-outline-focus-border: rgba(240,246,252,0.1); + --color-btn-outline-focus-shadow: 0 0 0 3px rgba(17,88,199,0.4); + --color-btn-outline-counter-bg: rgba(31,111,235,0.1); + --color-btn-danger-text: #f85149; + --color-btn-danger-hover-text: #f0f6fc; + --color-btn-danger-hover-bg: #da3633; + --color-btn-danger-hover-border: #f85149; + --color-btn-danger-hover-shadow: 0 0 transparent; + --color-btn-danger-hover-inset-shadow: 0 0 transparent; + --color-btn-danger-hover-icon: #f0f6fc; + --color-btn-danger-hover-counter-bg: rgba(255,255,255,0.2); + --color-btn-danger-selected-text: #ffffff; + --color-btn-danger-selected-bg: #b62324; + --color-btn-danger-selected-border: #ff7b72; + --color-btn-danger-selected-shadow: 0 0 transparent; + --color-btn-danger-disabled-text: rgba(248,81,73,0.5); + --color-btn-danger-disabled-bg: #0d1117; + --color-btn-danger-disabled-counter-bg: rgba(218,54,51,0.05); + --color-btn-danger-focus-border: #f85149; + --color-btn-danger-focus-shadow: 0 0 0 3px rgba(248,81,73,0.4); + --color-btn-danger-counter-bg: rgba(218,54,51,0.1); + --color-btn-danger-icon: #f85149; + --color-underlinenav-icon: #484f58; + --color-underlinenav-border-hover: rgba(110,118,129,0.4); + --color-fg-default: #c9d1d9; + --color-fg-muted: #8b949e; + --color-fg-subtle: #484f58; + --color-fg-on-emphasis: #f0f6fc; + --color-canvas-default: #0d1117; + --color-canvas-overlay: #161b22; + --color-canvas-inset: #010409; + --color-canvas-subtle: #161b22; + --color-border-default: #30363d; + --color-border-muted: #21262d; + --color-border-subtle: rgba(240,246,252,0.1); + --color-shadow-small: 0 0 transparent; + --color-shadow-medium: 0 3px 6px #010409; + --color-shadow-large: 0 8px 24px #010409; + --color-shadow-extra-large: 0 12px 48px #010409; + --color-neutral-emphasis-plus: #6e7681; + --color-neutral-emphasis: #6e7681; + --color-neutral-muted: rgba(110,118,129,0.4); + --color-neutral-subtle: rgba(110,118,129,0.1); + --color-accent-fg: #58a6ff; + --color-accent-emphasis: #1f6feb; + --color-accent-muted: rgba(56,139,253,0.4); + --color-accent-subtle: rgba(56,139,253,0.15); + --color-success-fg: #3fb950; + --color-success-emphasis: #238636; + --color-success-muted: rgba(46,160,67,0.4); + --color-success-subtle: rgba(46,160,67,0.15); + --color-attention-fg: #d29922; + --color-attention-emphasis: #9e6a03; + --color-attention-muted: rgba(187,128,9,0.4); + --color-attention-subtle: rgba(187,128,9,0.15); + --color-severe-fg: #db6d28; + --color-severe-emphasis: #bd561d; + --color-severe-muted: rgba(219,109,40,0.4); + --color-severe-subtle: rgba(219,109,40,0.15); + --color-danger-fg: #f85149; + --color-danger-emphasis: #da3633; + --color-danger-muted: rgba(248,81,73,0.4); + --color-danger-subtle: rgba(248,81,73,0.15); + --color-done-fg: #a371f7; + --color-done-emphasis: #8957e5; + --color-done-muted: rgba(163,113,247,0.4); + --color-done-subtle: rgba(163,113,247,0.15); + --color-sponsors-fg: #db61a2; + --color-sponsors-emphasis: #bf4b8a; + --color-sponsors-muted: rgba(219,97,162,0.4); + --color-sponsors-subtle: rgba(219,97,162,0.15); + --color-primer-canvas-backdrop: rgba(1,4,9,0.8); + --color-primer-canvas-sticky: rgba(13,17,23,0.95); + --color-primer-border-active: #F78166; + --color-primer-border-contrast: rgba(240,246,252,0.2); + --color-primer-shadow-highlight: 0 0 transparent; + --color-primer-shadow-inset: 0 0 transparent; + --color-primer-shadow-focus: 0 0 0 3px #0c2d6b; + --color-scale-black: #010409; + --color-scale-white: #f0f6fc; + --color-scale-gray-0: #f0f6fc; + --color-scale-gray-1: #c9d1d9; + --color-scale-gray-2: #b1bac4; + --color-scale-gray-3: #8b949e; + --color-scale-gray-4: #6e7681; + --color-scale-gray-5: #484f58; + --color-scale-gray-6: #30363d; + --color-scale-gray-7: #21262d; + --color-scale-gray-8: #161b22; + --color-scale-gray-9: #0d1117; + --color-scale-blue-0: #cae8ff; + --color-scale-blue-1: #a5d6ff; + --color-scale-blue-2: #79c0ff; + --color-scale-blue-3: #58a6ff; + --color-scale-blue-4: #388bfd; + --color-scale-blue-5: #1f6feb; + --color-scale-blue-6: #1158c7; + --color-scale-blue-7: #0d419d; + --color-scale-blue-8: #0c2d6b; + --color-scale-blue-9: #051d4d; + --color-scale-green-0: #aff5b4; + --color-scale-green-1: #7ee787; + --color-scale-green-2: #56d364; + --color-scale-green-3: #3fb950; + --color-scale-green-4: #2ea043; + --color-scale-green-5: #238636; + --color-scale-green-6: #196c2e; + --color-scale-green-7: #0f5323; + --color-scale-green-8: #033a16; + --color-scale-green-9: #04260f; + --color-scale-yellow-0: #f8e3a1; + --color-scale-yellow-1: #f2cc60; + --color-scale-yellow-2: #e3b341; + --color-scale-yellow-3: #d29922; + --color-scale-yellow-4: #bb8009; + --color-scale-yellow-5: #9e6a03; + --color-scale-yellow-6: #845306; + --color-scale-yellow-7: #693e00; + --color-scale-yellow-8: #4b2900; + --color-scale-yellow-9: #341a00; + --color-scale-orange-0: #ffdfb6; + --color-scale-orange-1: #ffc680; + --color-scale-orange-2: #ffa657; + --color-scale-orange-3: #f0883e; + --color-scale-orange-4: #db6d28; + --color-scale-orange-5: #bd561d; + --color-scale-orange-6: #9b4215; + --color-scale-orange-7: #762d0a; + --color-scale-orange-8: #5a1e02; + --color-scale-orange-9: #3d1300; + --color-scale-red-0: #ffdcd7; + --color-scale-red-1: #ffc1ba; + --color-scale-red-2: #ffa198; + --color-scale-red-3: #ff7b72; + --color-scale-red-4: #f85149; + --color-scale-red-5: #da3633; + --color-scale-red-6: #b62324; + --color-scale-red-7: #8e1519; + --color-scale-red-8: #67060c; + --color-scale-red-9: #490202; + --color-scale-purple-0: #eddeff; + --color-scale-purple-1: #e2c5ff; + --color-scale-purple-2: #d2a8ff; + --color-scale-purple-3: #bc8cff; + --color-scale-purple-4: #a371f7; + --color-scale-purple-5: #8957e5; + --color-scale-purple-6: #6e40c9; + --color-scale-purple-7: #553098; + --color-scale-purple-8: #3c1e70; + --color-scale-purple-9: #271052; + --color-scale-pink-0: #ffdaec; + --color-scale-pink-1: #ffbedd; + --color-scale-pink-2: #ff9bce; + --color-scale-pink-3: #f778ba; + --color-scale-pink-4: #db61a2; + --color-scale-pink-5: #bf4b8a; + --color-scale-pink-6: #9e3670; + --color-scale-pink-7: #7d2457; + --color-scale-pink-8: #5e103e; + --color-scale-pink-9: #42062a; + --color-scale-coral-0: #FFDDD2; + --color-scale-coral-1: #FFC2B2; + --color-scale-coral-2: #FFA28B; + --color-scale-coral-3: #F78166; + --color-scale-coral-4: #EA6045; + --color-scale-coral-5: #CF462D; + --color-scale-coral-6: #AC3220; + --color-scale-coral-7: #872012; + --color-scale-coral-8: #640D04; + --color-scale-coral-9: #460701 + } +} diff --git a/extension-playwright/src/ui/connect.css b/extension-playwright/src/ui/connect.css new file mode 100644 index 0000000..fe2588f --- /dev/null +++ b/extension-playwright/src/ui/connect.css @@ -0,0 +1,262 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +body { + margin: 0; + padding: 0; +} + +/* Base styles */ +.app-container { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; + background-color: #ffffff; + color: #1f2328; + margin: 0; + padding: 16px; + min-height: 100vh; + font-size: 14px; +} + +.content-wrapper { + max-width: 600px; + margin: 0 auto; +} + +/* Status Banner */ +.status-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + padding-right: 12px; +} + +.status-banner { + padding: 12px; + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + flex: 1; +} + +.status-banner.connected { + color: #1f2328; +} + +.status-banner.connected::before { + content: "\2705"; + margin-right: 8px; +} + +.status-banner.error { + color: #1f2328; +} + +.status-banner.error::before { + content: "\274C"; + margin-right: 8px; +} + +/* Buttons */ +.button-container { + margin-bottom: 16px; + display: flex; + justify-content: flex-end; + padding-right: 12px; +} + +.button { + padding: 8px 16px; + border-radius: 6px; + border: none; + font-size: 14px; + font-weight: 500; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + margin-right: 8px; + min-width: 90px; +} + +.button.primary { + background-color: #f8f9fa; + color: #3c4043; + border: 1px solid #dadce0; +} + +.button.primary:hover { + background-color: #f1f3f4; + border-color: #dadce0; + box-shadow: 0 1px 2px 0 rgba(60,64,67,.1); +} + +.button.default { + background-color: #f6f8fa; + color: #24292f; +} + +.button.default:hover { + background-color: #f3f4f6; +} + +.button.reject { + background-color: #da3633; + color: #ffffff; + border: 1px solid #da3633; +} + +.button.reject:hover { + background-color: #c73836; + border-color: #c73836; +} + +/* Tab selection */ +.tab-section-title { + padding-left: 12px; + font-size: 12px; + font-weight: 400; + margin-bottom: 12px; + color: #656d76; +} + +.tab-item { + display: flex; + align-items: center; + padding: 12px; + margin-bottom: 8px; + background-color: #ffffff; + cursor: pointer; + border-radius: 6px; + transition: background-color 0.2s ease; +} + +.tab-item:hover { + background-color: #f8f9fa; +} + +.tab-item.selected { + background-color: #f6f8fa; +} + +.tab-item.disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.tab-radio { + margin-right: 12px; + flex-shrink: 0; +} + +.tab-favicon { + width: 16px; + height: 16px; + margin-right: 8px; + flex-shrink: 0; +} + +.tab-content { + flex: 1; + min-width: 0; +} + +.tab-title { + font-weight: 500; + color: #1f2328; + margin-bottom: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.tab-url { + font-size: 12px; + color: #656d76; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Link-style button */ +.link-button { + background: none; + border: none; + color: #0066cc; + text-decoration: underline; + cursor: pointer; + padding: 0; + font: inherit; +} + +/* Auth token section */ +.auth-token-section { + margin: 16px 0; + padding: 16px; + background-color: #f6f8fa; + border-radius: 6px; +} + +.auth-token-description { + font-size: 12px; + color: #656d76; + margin-bottom: 12px; +} + +.auth-token-container { + display: flex; + align-items: center; + gap: 8px; + background-color: #ffffff; + padding: 8px; +} + +.auth-token-code { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 12px; + color: #1f2328; + border: none; + flex: 1; + padding: 0; + word-break: break-all; +} + +.auth-token-refresh { + flex: none; + height: 24px; + width: 24px; + border: none; + outline: none; + color: var(--color-fg-muted); + background: transparent; + padding: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 4px; +} + +.auth-token-refresh svg { + margin: 0; +} + +.auth-token-refresh:not(:disabled):hover { + background-color: var(--color-btn-selected-bg); +} diff --git a/extension-playwright/src/ui/connect.html b/extension-playwright/src/ui/connect.html new file mode 100644 index 0000000..3f20e4b --- /dev/null +++ b/extension-playwright/src/ui/connect.html @@ -0,0 +1,29 @@ + + + + + Playwright MCP extension + + + + + + +
    + + + \ No newline at end of file diff --git a/extension-playwright/src/ui/connect.tsx b/extension-playwright/src/ui/connect.tsx new file mode 100644 index 0000000..e9e410e --- /dev/null +++ b/extension-playwright/src/ui/connect.tsx @@ -0,0 +1,253 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Button, TabItem } from './tabItem'; +import { AuthTokenSection, getOrCreateAuthToken } from './authToken'; + +import type { TabInfo } from './tabItem'; + +type Status = + | { type: 'connecting'; message: string } + | { type: 'connected'; message: string } + | { type: 'error'; message: string } + | { type: 'error'; versionMismatch: { extensionVersion: string; } }; + +const SUPPORTED_PROTOCOL_VERSION = 1; + +const ConnectApp: React.FC = () => { + const [tabs, setTabs] = useState([]); + const [status, setStatus] = useState(null); + const [showButtons, setShowButtons] = useState(true); + const [showTabList, setShowTabList] = useState(true); + const [clientInfo, setClientInfo] = useState('unknown'); + const [mcpRelayUrl, setMcpRelayUrl] = useState(''); + const [newTab, setNewTab] = useState(false); + + useEffect(() => { + const runAsync = async () => { + const params = new URLSearchParams(window.location.search); + const relayUrl = params.get('mcpRelayUrl'); + + if (!relayUrl) { + setShowButtons(false); + setStatus({ type: 'error', message: 'Missing mcpRelayUrl parameter in URL.' }); + return; + } + + setMcpRelayUrl(relayUrl); + + try { + const client = JSON.parse(params.get('client') || '{}'); + const info = `${client.name}/${client.version}`; + setClientInfo(info); + setStatus({ + type: 'connecting', + message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?` + }); + } catch (e) { + setStatus({ type: 'error', message: 'Failed to parse client version.' }); + return; + } + + const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10); + const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion; + if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) { + const extensionVersion = chrome.runtime.getManifest().version; + setShowButtons(false); + setShowTabList(false); + setStatus({ + type: 'error', + versionMismatch: { + extensionVersion, + } + }); + return; + } + + const expectedToken = getOrCreateAuthToken(); + const token = params.get('token'); + if (token === expectedToken) { + await connectToMCPRelay(relayUrl); + await handleConnectToTab(); + return; + } + if (token) { + handleReject('Invalid token provided.'); + return; + } + + await connectToMCPRelay(relayUrl); + + // If this is a browser_navigate command, hide the tab list and show simple allow/reject + if (params.get('newTab') === 'true') { + setNewTab(true); + setShowTabList(false); + } else { + await loadTabs(); + } + }; + void runAsync(); + }, []); + + const handleReject = useCallback((message: string) => { + setShowButtons(false); + setShowTabList(false); + setStatus({ type: 'error', message }); + }, []); + + const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => { + const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl }); + if (!response.success) + handleReject(response.error); + }, [handleReject]); + + const loadTabs = useCallback(async () => { + const response = await chrome.runtime.sendMessage({ type: 'getTabs' }); + if (response.success) + setTabs(response.tabs); + else + setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error }); + }, []); + + const handleConnectToTab = useCallback(async (tab?: TabInfo) => { + setShowButtons(false); + setShowTabList(false); + + try { + const response = await chrome.runtime.sendMessage({ + type: 'connectToTab', + mcpRelayUrl, + tabId: tab?.id, + windowId: tab?.windowId, + }); + + if (response?.success) { + setStatus({ type: 'connected', message: `MCP client "${clientInfo}" connected.` }); + } else { + setStatus({ + type: 'error', + message: response?.error || `MCP client "${clientInfo}" failed to connect.` + }); + } + } catch (e) { + setStatus({ + type: 'error', + message: `MCP client "${clientInfo}" failed to connect: ${e}` + }); + } + }, [clientInfo, mcpRelayUrl]); + + useEffect(() => { + const listener = (message: any) => { + if (message.type === 'connectionTimeout') + handleReject('Connection timed out.'); + }; + chrome.runtime.onMessage.addListener(listener); + return () => { + chrome.runtime.onMessage.removeListener(listener); + }; + }, [handleReject]); + + return ( +
    +
    + {status && ( +
    + + {showButtons && ( +
    + {newTab ? ( + <> + + + + ) : ( + + )} +
    + )} +
    + )} + + {status?.type === 'connecting' && ( + + )} + + {showTabList && ( +
    +
    + Select page to expose to MCP server: +
    +
    + {tabs.map(tab => ( + handleConnectToTab(tab)}> + Connect + + } + /> + ))} +
    +
    + )} +
    +
    + ); +}; + +const VersionMismatchError: React.FC<{ extensionVersion: string }> = ({ extensionVersion }) => { + const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md'; + const latestReleaseUrl = 'https://github.com/microsoft/playwright-mcp/releases/latest'; + return ( +
    + Playwright MCP version trying to connect requires newer extension version (current version: {extensionVersion}).{' '} + Click here to download latest version of the extension, then drag and drop it into the Chrome Extensions page.{' '} + See installation instructions for more details. +
    + ); +}; + +const StatusBanner: React.FC<{ status: Status }> = ({ status }) => { + return ( +
    + {'versionMismatch' in status ? ( + + ) : ( + status.message + )} +
    + ); +}; + +// Initialize the React app +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(); +} diff --git a/extension-playwright/src/ui/copyToClipboard.css b/extension-playwright/src/ui/copyToClipboard.css new file mode 100644 index 0000000..d929947 --- /dev/null +++ b/extension-playwright/src/ui/copyToClipboard.css @@ -0,0 +1,39 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.copy-icon { + flex: none; + height: 24px; + width: 24px; + border: none; + outline: none; + color: var(--color-fg-muted); + background: transparent; + padding: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 4px; +} + +.copy-icon svg { + margin: 0; +} + +.copy-icon:not(:disabled):hover { + background-color: var(--color-btn-selected-bg); +} diff --git a/extension-playwright/src/ui/copyToClipboard.tsx b/extension-playwright/src/ui/copyToClipboard.tsx new file mode 100644 index 0000000..487eaba --- /dev/null +++ b/extension-playwright/src/ui/copyToClipboard.tsx @@ -0,0 +1,54 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from 'react'; +import * as icons from './icons'; +import './copyToClipboard.css'; + +type CopyToClipboardProps = { + value: string; +}; + +/** + * A copy to clipboard button. + */ +export const CopyToClipboard: React.FunctionComponent = ({ value }) => { + type IconType = 'copy' | 'check' | 'cross'; + const [icon, setIcon] = React.useState('copy'); + + React.useEffect(() => { + setIcon('copy'); + }, [value]); + + React.useEffect(() => { + if (icon === 'check') { + const timeout = setTimeout(() => { + setIcon('copy'); + }, 3000); + return () => clearTimeout(timeout); + } + }, [icon]); + + const handleCopy = React.useCallback(() => { + navigator.clipboard.writeText(value).then(() => { + setIcon('check'); + }, () => { + setIcon('cross'); + }); + }, [value]); + const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy(); + return ; +}; diff --git a/extension-playwright/src/ui/icons.css b/extension-playwright/src/ui/icons.css new file mode 100644 index 0000000..8abcf98 --- /dev/null +++ b/extension-playwright/src/ui/icons.css @@ -0,0 +1,32 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.octicon { + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + fill: currentColor; + margin-right: 7px; + flex: none; +} + +.color-icon-success { + color: var(--color-success-fg) !important; +} + +.color-text-danger { + color: var(--color-danger-fg) !important; +} diff --git a/extension-playwright/src/ui/icons.tsx b/extension-playwright/src/ui/icons.tsx new file mode 100644 index 0000000..fcf74b8 --- /dev/null +++ b/extension-playwright/src/ui/icons.tsx @@ -0,0 +1,49 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import './icons.css'; +import './colors.css'; + +export const cross = () => { + return ; +}; + +export const check = () => { + return ; +}; + +export const copy = () => { + return ; +}; + +export const refresh = () => { + return ; +}; + +export const chevronDown = () => { + return ; +}; diff --git a/extension-playwright/src/ui/status.html b/extension-playwright/src/ui/status.html new file mode 100644 index 0000000..ccc1f04 --- /dev/null +++ b/extension-playwright/src/ui/status.html @@ -0,0 +1,13 @@ + + + + + + Playwright MCP Bridge Status + + + +
    + + + \ No newline at end of file diff --git a/extension-playwright/src/ui/status.tsx b/extension-playwright/src/ui/status.tsx new file mode 100644 index 0000000..ca2e1ef --- /dev/null +++ b/extension-playwright/src/ui/status.tsx @@ -0,0 +1,112 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Button, TabItem } from './tabItem'; + +import type { TabInfo } from './tabItem'; +import { AuthTokenSection } from './authToken'; + +interface ConnectionStatus { + isConnected: boolean; + connectedTabId: number | null; + connectedTab?: TabInfo; +} + +const StatusApp: React.FC = () => { + const [status, setStatus] = useState({ + isConnected: false, + connectedTabId: null + }); + + useEffect(() => { + void loadStatus(); + }, []); + + const loadStatus = async () => { + // Get current connection status from background script + const { connectedTabId } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' }); + if (connectedTabId) { + const tab = await chrome.tabs.get(connectedTabId); + setStatus({ + isConnected: true, + connectedTabId, + connectedTab: { + id: tab.id!, + windowId: tab.windowId!, + title: tab.title!, + url: tab.url!, + favIconUrl: tab.favIconUrl + } + }); + } else { + setStatus({ + isConnected: false, + connectedTabId: null + }); + } + }; + + const openConnectedTab = async () => { + if (!status.connectedTabId) + return; + await chrome.tabs.update(status.connectedTabId, { active: true }); + window.close(); + }; + + const disconnect = async () => { + await chrome.runtime.sendMessage({ type: 'disconnect' }); + window.close(); + }; + + return ( +
    +
    + {status.isConnected && status.connectedTab ? ( +
    +
    + Page with connected MCP client: +
    +
    + + Disconnect + + } + onClick={openConnectedTab} + /> +
    +
    + ) : ( +
    + No MCP clients are currently connected. +
    + )} + +
    +
    + ); +}; + +// Initialize the React app +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(); +} diff --git a/extension-playwright/src/ui/tabItem.tsx b/extension-playwright/src/ui/tabItem.tsx new file mode 100644 index 0000000..1483742 --- /dev/null +++ b/extension-playwright/src/ui/tabItem.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +export interface TabInfo { + id: number; + windowId: number; + title: string; + url: string; + favIconUrl?: string; +} + +export const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({ + variant, + onClick, + children +}) => { + return ( + + ); +}; + + +export interface TabItemProps { + tab: TabInfo; + onClick?: () => void; + button?: React.ReactNode; +} + +export const TabItem: React.FC = ({ + tab, + onClick, + button +}) => { + return ( +
    + '} + alt='' + className='tab-favicon' + /> +
    +
    + {tab.title || 'Untitled'} +
    +
    {tab.url}
    +
    + {button} +
    + ); +}; diff --git a/extension-playwright/src/ui/tsconfig.json b/extension-playwright/src/ui/tsconfig.json new file mode 100644 index 0000000..77839b6 --- /dev/null +++ b/extension-playwright/src/ui/tsconfig.json @@ -0,0 +1,4 @@ +// Help VSCode to find right tsconfig file. +{ + "extends": "../../tsconfig.ui.json" +} diff --git a/extension-playwright/tests/extension.spec.ts b/extension-playwright/tests/extension.spec.ts new file mode 100644 index 0000000..291390e --- /dev/null +++ b/extension-playwright/tests/extension.spec.ts @@ -0,0 +1,336 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import { chromium } from 'playwright'; +import { test as base, expect } from '../../tests/fixtures'; + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { BrowserContext } from 'playwright'; +import type { StartClient } from '../../tests/fixtures'; + +type BrowserWithExtension = { + userDataDir: string; + launch: (mode?: 'disable-extension') => Promise; +}; + +type TestFixtures = { + browserWithExtension: BrowserWithExtension, + pathToExtension: string, + useShortConnectionTimeout: (timeoutMs: number) => void + overrideProtocolVersion: (version: number) => void +}; + +const test = base.extend({ + pathToExtension: async ({}, use) => { + await use(path.resolve(__dirname, '../dist')); + }, + + browserWithExtension: async ({ mcpBrowser, pathToExtension }, use, testInfo) => { + // The flags no longer work in Chrome since + // https://chromium.googlesource.com/chromium/src/+/290ed8046692651ce76088914750cb659b65fb17%5E%21/chrome/browser/extensions/extension_service.cc?pli=1# + test.skip('chromium' !== mcpBrowser, '--load-extension is not supported for official builds of Chromium'); + + let browserContext: BrowserContext | undefined; + const userDataDir = testInfo.outputPath('extension-user-data-dir'); + await use({ + userDataDir, + launch: async (mode?: 'disable-extension') => { + browserContext = await chromium.launchPersistentContext(userDataDir, { + channel: mcpBrowser, + // Opening the browser singleton only works in headed. + headless: false, + // Automation disables singleton browser process behavior, which is necessary for the extension. + ignoreDefaultArgs: ['--enable-automation'], + args: mode === 'disable-extension' ? [] : [ + `--disable-extensions-except=${pathToExtension}`, + `--load-extension=${pathToExtension}`, + ], + }); + + // for manifest v3: + let [serviceWorker] = browserContext.serviceWorkers(); + if (!serviceWorker) + serviceWorker = await browserContext.waitForEvent('serviceworker'); + + return browserContext; + } + }); + await browserContext?.close(); + }, + + useShortConnectionTimeout: async ({}, use) => { + await use((timeoutMs: number) => { + process.env.PWMCP_TEST_CONNECTION_TIMEOUT = timeoutMs.toString(); + }); + process.env.PWMCP_TEST_CONNECTION_TIMEOUT = undefined; + }, + + overrideProtocolVersion: async ({}, use) => { + await use((version: number) => { + process.env.PWMCP_TEST_PROTOCOL_VERSION = version.toString(); + }); + process.env.PWMCP_TEST_PROTOCOL_VERSION = undefined; + } +}); + +async function startAndCallConnectTool(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise { + const { client } = await startClient({ + args: [`--connect-tool`], + config: { + browser: { + userDataDir: browserWithExtension.userDataDir, + } + }, + }); + + expect(await client.callTool({ + name: 'browser_connect', + arguments: { + name: 'extension' + } + })).toHaveResponse({ + result: 'Successfully changed connection method.', + }); + + return client; +} + +async function startWithExtensionFlag(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise { + const { client } = await startClient({ + args: [`--extension`], + config: { + browser: { + userDataDir: browserWithExtension.userDataDir, + } + }, + }); + return client; +} + +const testWithOldExtensionVersion = test.extend({ + pathToExtension: async ({}, use, testInfo) => { + const extensionDir = testInfo.outputPath('extension'); + const oldPath = path.resolve(__dirname, '../dist'); + + await fs.promises.cp(oldPath, extensionDir, { recursive: true }); + const manifestPath = path.join(extensionDir, 'manifest.json'); + const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8')); + manifest.version = '0.0.1'; + await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n'); + + await use(extensionDir); + }, +}); + +for (const [mode, startClientMethod] of [ + ['connect-tool', startAndCallConnectTool], + ['extension-flag', startWithExtensionFlag], +] as const) { + + test(`navigate with extension (${mode})`, async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + + const client = await startClientMethod(browserWithExtension, startClient); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + const navigateResponse = client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const selectorPage = await confirmationPagePromise; + // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector + await selectorPage.getByRole('button', { name: 'Allow' }).click(); + + expect(await navigateResponse).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + }); + + test(`snapshot of an existing page (${mode})`, async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + + const page = await browserContext.newPage(); + await page.goto(server.HELLO_WORLD); + + // Another empty page. + await browserContext.newPage(); + expect(browserContext.pages()).toHaveLength(3); + + const client = await startClientMethod(browserWithExtension, startClient); + expect(browserContext.pages()).toHaveLength(3); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + const navigateResponse = client.callTool({ + name: 'browser_snapshot', + arguments: { }, + }); + + const selectorPage = await confirmationPagePromise; + expect(browserContext.pages()).toHaveLength(4); + + await selectorPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click(); + + expect(await navigateResponse).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + + expect(browserContext.pages()).toHaveLength(4); + }); + + test(`extension not installed timeout (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => { + useShortConnectionTimeout(100); + + const browserContext = await browserWithExtension.launch(); + + const client = await startClientMethod(browserWithExtension, startClient); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + result: expect.stringContaining('Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed.'), + isError: true, + }); + + await confirmationPagePromise; + }); + + testWithOldExtensionVersion(`works with old extension version (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => { + useShortConnectionTimeout(500); + + // Prelaunch the browser, so that it is properly closed after the test. + const browserContext = await browserWithExtension.launch(); + + const client = await startClientMethod(browserWithExtension, startClient); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + const navigateResponse = client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const selectorPage = await confirmationPagePromise; + // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector + await selectorPage.getByRole('button', { name: 'Allow' }).click(); + + expect(await navigateResponse).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + }); + + test(`extension needs update (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout, overrideProtocolVersion }) => { + useShortConnectionTimeout(500); + overrideProtocolVersion(1000); + + // Prelaunch the browser, so that it is properly closed after the test. + const browserContext = await browserWithExtension.launch(); + + const client = await startClientMethod(browserWithExtension, startClient); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + const navigateResponse = client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const confirmationPage = await confirmationPagePromise; + await expect(confirmationPage.locator('.status-banner')).toContainText(`Playwright MCP version trying to connect requires newer extension version`); + + expect(await navigateResponse).toHaveResponse({ + result: expect.stringContaining('Extension connection timeout.'), + isError: true, + }); + }); + +} + +test(`custom executablePath`, async ({ startClient, server, useShortConnectionTimeout }) => { + useShortConnectionTimeout(1000); + + const executablePath = test.info().outputPath('echo.sh'); + await fs.promises.writeFile(executablePath, '#!/bin/bash\necho "Custom exec args: $@" > "$(dirname "$0")/output.txt"', { mode: 0o755 }); + + const { client } = await startClient({ + args: [`--extension`], + config: { + browser: { + launchOptions: { + executablePath, + }, + } + }, + }); + + const navigateResponse = await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + timeout: 1000, + }); + expect(await navigateResponse).toHaveResponse({ + result: expect.stringContaining('Extension connection timeout.'), + isError: true, + }); + expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?'); +}); + +test(`bypass connection dialog with token`, async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + + const page = await browserContext.newPage(); + await page.goto('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/status.html'); + const token = await page.locator('.auth-token-code').textContent(); + const [name, value] = token?.split('=') || []; + + const { client } = await startClient({ + args: [`--extension`], + extensionToken: value, + config: { + browser: { + userDataDir: browserWithExtension.userDataDir, + } + }, + }); + + const navigateResponse = await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + expect(await navigateResponse).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + + +}); diff --git a/extension-playwright/tsconfig.json b/extension-playwright/tsconfig.json new file mode 100644 index 0000000..9c22b0b --- /dev/null +++ b/extension-playwright/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "esModuleInterop": true, + "moduleResolution": "node", + "strict": true, + "module": "ESNext", + "rootDir": "src", + "outDir": "./dist/lib", + "resolveJsonModule": true, + "types": ["chrome"], + "jsx": "react-jsx", + "jsxImportSource": "react", + "noEmit": true + }, + "include": [ + "src", + ], + "exclude": [ + "src/ui", + ] +} diff --git a/extension-playwright/tsconfig.ui.json b/extension-playwright/tsconfig.ui.json new file mode 100644 index 0000000..f62dd8a --- /dev/null +++ b/extension-playwright/tsconfig.ui.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "esModuleInterop": true, + "moduleResolution": "node", + "strict": true, + "module": "ESNext", + "rootDir": "src", + "outDir": "./lib", + "resolveJsonModule": true, + "types": ["chrome"], + "jsx": "react-jsx", + "jsxImportSource": "react", + "noEmit": true, + }, + "include": [ + "src/ui", + ], +} diff --git a/extension-playwright/vite.config.mts b/extension-playwright/vite.config.mts new file mode 100644 index 0000000..89ec56c --- /dev/null +++ b/extension-playwright/vite.config.mts @@ -0,0 +1,54 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + viteStaticCopy({ + targets: [ + { + src: '../../icons/*', + dest: 'icons' + }, + { + src: '../../manifest.json', + dest: '.' + } + ] + }) + ], + root: resolve(__dirname, 'src/ui'), + build: { + outDir: resolve(__dirname, 'dist/'), + emptyOutDir: false, + minify: false, + rollupOptions: { + input: ['src/ui/connect.html', 'src/ui/status.html'], + output: { + manualChunks: undefined, + entryFileNames: 'lib/ui/[name].js', + chunkFileNames: 'lib/ui/[name].js', + assetFileNames: 'lib/ui/[name].[ext]' + } + } + } +}); diff --git a/extension-playwright/vite.sw.config.mts b/extension-playwright/vite.sw.config.mts new file mode 100644 index 0000000..a383e4b --- /dev/null +++ b/extension-playwright/vite.sw.config.mts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolve } from 'path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'src/background.ts'), + fileName: 'lib/background', + formats: ['es'] + }, + outDir: 'dist', + emptyOutDir: false, + minify: false + } +});