diff --git a/extension-kapture/KaptureDevToolsPanel.webp b/extension-kapture/KaptureDevToolsPanel.webp deleted file mode 100644 index 9075392..0000000 Binary files a/extension-kapture/KaptureDevToolsPanel.webp and /dev/null differ 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 b73b5a7..0000000 Binary files a/extension-kapture/ScreenshotWithExtensionPanel.webp and /dev/null differ 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 edcbaa6..0000000 Binary files a/extension-kapture/icons/icon128.png and /dev/null differ diff --git a/extension-kapture/icons/icon16.png b/extension-kapture/icons/icon16.png deleted file mode 100644 index 46cd181..0000000 Binary files a/extension-kapture/icons/icon16.png and /dev/null differ diff --git a/extension-kapture/icons/icon48.png b/extension-kapture/icons/icon48.png deleted file mode 100644 index cf58765..0000000 Binary files a/extension-kapture/icons/icon48.png and /dev/null differ 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