From b4f6ea8237ed7e524109ffc618bc7f58b33d85fc Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Fri, 14 Nov 2025 13:33:52 +0100 Subject: [PATCH] remove extension --- extension-kapture/KaptureDevToolsPanel.webp | Bin 54278 -> 0 bytes extension-kapture/PRIVACY_POLICY.md | 87 --- .../ScreenshotWithExtensionPanel.webp | Bin 54290 -> 0 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 7391 -> 0 bytes extension-kapture/icons/icon16.png | Bin 744 -> 0 bytes extension-kapture/icons/icon48.png | Bin 2791 -> 0 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 -- plan.md | 1 + 28 files changed, 1 insertion(+), 3265 deletions(-) delete mode 100644 extension-kapture/KaptureDevToolsPanel.webp delete mode 100644 extension-kapture/PRIVACY_POLICY.md delete mode 100644 extension-kapture/ScreenshotWithExtensionPanel.webp delete mode 100644 extension-kapture/background.js delete mode 100644 extension-kapture/console-listener.js delete mode 100644 extension-kapture/content-script.js delete mode 100644 extension-kapture/devtools.html delete mode 100644 extension-kapture/devtools.js delete mode 100644 extension-kapture/icons/icon128.png delete mode 100644 extension-kapture/icons/icon16.png delete mode 100644 extension-kapture/icons/icon48.png delete mode 100644 extension-kapture/manifest.json delete mode 100644 extension-kapture/modules/background-click.js delete mode 100644 extension-kapture/modules/background-commands.js delete mode 100644 extension-kapture/modules/background-console.js delete mode 100644 extension-kapture/modules/background-keypress.js delete mode 100644 extension-kapture/modules/background-navigate.js delete mode 100644 extension-kapture/modules/background-screenshot.js delete mode 100644 extension-kapture/modules/models.js delete mode 100644 extension-kapture/modules/tab-manager.js delete mode 100644 extension-kapture/modules/tab-state.js delete mode 100644 extension-kapture/page-helpers.js delete mode 100644 extension-kapture/panel.css delete mode 100644 extension-kapture/panel.html delete mode 100644 extension-kapture/panel.js delete mode 100644 extension-kapture/popup.html delete mode 100644 extension-kapture/popup.js create mode 100644 plan.md diff --git a/extension-kapture/KaptureDevToolsPanel.webp b/extension-kapture/KaptureDevToolsPanel.webp deleted file mode 100644 index 9075392e409e39ead0db32d333d41b678a4ddbf1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54278 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 diff --git a/extension-kapture/PRIVACY_POLICY.md b/extension-kapture/PRIVACY_POLICY.md deleted file mode 100644 index 116dfa5..0000000 --- a/extension-kapture/PRIVACY_POLICY.md +++ /dev/null @@ -1,87 +0,0 @@ -# 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 deleted file mode 100644 index b73b5a783d6f7aa9c35be416ae5fd77c59e8925f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54290 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 V7$p6i6<|T-*l9GazG6%O0066WDDD6N diff --git a/extension-kapture/background.js b/extension-kapture/background.js deleted file mode 100644 index ff567b1..0000000 --- a/extension-kapture/background.js +++ /dev/null @@ -1,239 +0,0 @@ -// 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 deleted file mode 100644 index 1ffca9c..0000000 --- a/extension-kapture/console-listener.js +++ /dev/null @@ -1,88 +0,0 @@ -(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 deleted file mode 100644 index 67d6b86..0000000 --- a/extension-kapture/content-script.js +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index 5ca8c2f..0000000 --- a/extension-kapture/devtools.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/extension-kapture/devtools.js b/extension-kapture/devtools.js deleted file mode 100644 index 153e6a1..0000000 --- a/extension-kapture/devtools.js +++ /dev/null @@ -1,6 +0,0 @@ -// 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 deleted file mode 100644 index edcbaa64962b6ddd13e1c6f0c5338c69ed226ef0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7391 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 a&*8sRBfV4cDCf`s0000}5 diff --git a/extension-kapture/icons/icon48.png b/extension-kapture/icons/icon48.png deleted file mode 100644 index cf5876557b19576909df75bc5c42a1a10ac078bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2791 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 diff --git a/extension-kapture/manifest.json b/extension-kapture/manifest.json deleted file mode 100644 index f3107d1..0000000 --- a/extension-kapture/manifest.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "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 deleted file mode 100644 index b583c6b..0000000 --- a/extension-kapture/modules/background-click.js +++ /dev/null @@ -1,122 +0,0 @@ -// 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 deleted file mode 100644 index 8739f8b..0000000 --- a/extension-kapture/modules/background-commands.js +++ /dev/null @@ -1,88 +0,0 @@ -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 deleted file mode 100644 index 3afffb4..0000000 --- a/extension-kapture/modules/background-console.js +++ /dev/null @@ -1,21 +0,0 @@ -// 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 deleted file mode 100644 index 9540d6a..0000000 --- a/extension-kapture/modules/background-keypress.js +++ /dev/null @@ -1,214 +0,0 @@ -// 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 deleted file mode 100644 index c24256e..0000000 --- a/extension-kapture/modules/background-navigate.js +++ /dev/null @@ -1,82 +0,0 @@ -// 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 deleted file mode 100644 index 0f543fa..0000000 --- a/extension-kapture/modules/background-screenshot.js +++ /dev/null @@ -1,54 +0,0 @@ -// 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 deleted file mode 100644 index 60f14a5..0000000 --- a/extension-kapture/modules/models.js +++ /dev/null @@ -1,33 +0,0 @@ - -// 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 deleted file mode 100644 index 61f9c58..0000000 --- a/extension-kapture/modules/tab-manager.js +++ /dev/null @@ -1,342 +0,0 @@ -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 deleted file mode 100644 index 5ba7a17..0000000 --- a/extension-kapture/modules/tab-state.js +++ /dev/null @@ -1,177 +0,0 @@ - -// 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 deleted file mode 100644 index 3a5ffc7..0000000 --- a/extension-kapture/page-helpers.js +++ /dev/null @@ -1,683 +0,0 @@ -// 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 deleted file mode 100644 index 8dce45e..0000000 --- a/extension-kapture/panel.css +++ /dev/null @@ -1,377 +0,0 @@ -: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 deleted file mode 100644 index 578cf3c..0000000 --- a/extension-kapture/panel.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - -
    - -
    - Tab: Not connected -
    -
    - - Disconnected -
    - -
    -
    - - -
    -
    -
    Date
    -
    - Time - -
    -
    - -
    No messages yet
    -
    - - -
    -
    -
    -
    -
    - - - -
    - - - - diff --git a/extension-kapture/panel.js b/extension-kapture/panel.js deleted file mode 100644 index 805f1f5..0000000 --- a/extension-kapture/panel.js +++ /dev/null @@ -1,304 +0,0 @@ -// 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 deleted file mode 100644 index ddc805d..0000000 --- a/extension-kapture/popup.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - -
    - - Disconnected -
    - - - - \ No newline at end of file diff --git a/extension-kapture/popup.js b/extension-kapture/popup.js deleted file mode 100644 index 8716435..0000000 --- a/extension-kapture/popup.js +++ /dev/null @@ -1,80 +0,0 @@ -// 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/plan.md b/plan.md new file mode 100644 index 0000000..97e7da6 --- /dev/null +++ b/plan.md @@ -0,0 +1 @@ +now see where attachToTab is sent and received. see that we basically attach to the tab as soon as the playwright instance connects. I want to change this: instead attach to the tab when we click on the extension icon, meaning inside onClicked. when this happens we should also send a