Skip to main content
Glama

WebEvalAgent MCP Server

Official
by withRefresh
agent_overlay.js22 kB
(function() { // --- Injection Guard --- if (window.__agentOverlayInjected === true) { // console.log('Agent overlay already injected or running, skipping.'); // Optional: reduce noise return; } // Set flag immediately to prevent race conditions window.__agentOverlayInjected = true; console.log('Setting injection flag.'); try { // Start of try block wrapping the entire setup console.log('Agent control overlay script starting setup...'); // --- Configuration --- const HOST_ELEMENT_ID = 'agent-control-host'; const OVERLAY_ID = 'agent-control-overlay'; // ID inside shadow DOM // --- Cleanup existing host(s) --- const existingHosts = document.querySelectorAll('#' + HOST_ELEMENT_ID); existingHosts.forEach(host => { host.remove(); console.log('Removed existing overlay host'); }); // --- Create Host Element in Main DOM --- const hostElement = document.createElement('div'); hostElement.id = HOST_ELEMENT_ID; // Minimal host styling - just needs to exist to host the shadow DOM hostElement.style.cssText = ` position: absolute; /* Or static, doesn't strictly matter */ width: 0; height: 0; overflow: hidden; border: none; padding: 0; margin: 0; `; // NOTE: The overlay *inside* the shadow DOM uses its own fixed positioning relative to the viewport. if (document.body) { document.body.appendChild(hostElement); console.log('Added overlay host element to page body'); } else { console.error('Cannot add overlay host: document.body is not available'); // Wait for body to be available const hostBodyCheckInterval = setInterval(() => { if (document.body) { document.body.appendChild(hostElement); console.log('Added overlay host element to page body (delayed)'); clearInterval(hostBodyCheckInterval); } }, 100); // If body never appears, we can't proceed setTimeout(() => clearInterval(hostBodyCheckInterval), 5000); // If body isn't found, reset flag and exit window.__agentOverlayInjected = false; console.log('Resetting injection flag: body not found.'); return; } // --- Attach Closed Shadow Root --- const shadowRoot = hostElement.attachShadow({ mode: 'closed' }); console.log('Attached closed shadow root to host element'); // --- Define Styles for Shadow DOM --- const styles = ` #${OVERLAY_ID} { position: fixed; /* Position relative to viewport */ top: 20px; /* 20px from the top */ left: 50%; /* Left edge at 50% */ transform: translateX(-50%); /* Shift left by half its width */ background-color: rgba(0, 0, 0, 0.8); color: white; border-radius: 8px; padding: 10px; z-index: 2147483647; /* Higher than host */ font-family: Arial, sans-serif; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; display: flex; flex-direction: column; min-width: 180px; box-sizing: border-box; /* Include padding in width/height */ } .overlay-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; cursor: move; user-select: none; /* Prevent text selection during drag */ } .overlay-title { font-weight: bold; } .minimize-btn { background: none; border: none; color: white; font-size: 16px; cursor: pointer; padding: 0 5px; line-height: 1; /* Ensure consistent height */ } .overlay-content { display: flex; flex-direction: column; gap: 8px; } .agent-status { padding: 5px; text-align: center; border-radius: 4px; margin-bottom: 8px; font-size: 12px; background-color: #28a745; /* Default: Running */ } .control-btn { padding: 8px 12px; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: opacity 0.2s; display: flex; align-items: center; justify-content: center; gap: 5px; /* Space between icon and text */ } .control-btn:hover { opacity: 0.85; } #pause-agent-btn { background-color: #ffc107; } #resume-agent-btn { background-color: #28a745; display: none; } /* Initially hidden */ #stop-agent-btn { background-color: #dc3545; } `; // --- Inject Styles into Shadow Root --- const styleSheet = document.createElement('style'); styleSheet.textContent = styles; shadowRoot.appendChild(styleSheet); console.log('Injected styles into shadow root'); // --- Create Overlay Structure Inside Shadow Root --- const overlay = document.createElement('div'); overlay.id = OVERLAY_ID; // Header const header = document.createElement('div'); header.className = 'overlay-header'; const title = document.createElement('div'); title.className = 'overlay-title'; title.textContent = 'Agent Controls'; const minimizeBtn = document.createElement('button'); minimizeBtn.className = 'minimize-btn'; minimizeBtn.innerHTML = '−'; header.appendChild(title); header.appendChild(minimizeBtn); // Content const content = document.createElement('div'); content.className = 'overlay-content'; const statusIndicator = document.createElement('div'); statusIndicator.className = 'agent-status'; statusIndicator.id = 'agent-status-shadow'; // Unique ID within shadow DOM statusIndicator.textContent = 'Running'; const pauseBtn = document.createElement('button'); pauseBtn.id = 'pause-agent-btn-shadow'; pauseBtn.className = 'control-btn'; pauseBtn.innerHTML = '⏸️ Pause'; pauseBtn.style.backgroundColor = '#ffc107'; // Set initial color via style const resumeBtn = document.createElement('button'); resumeBtn.id = 'resume-agent-btn-shadow'; resumeBtn.className = 'control-btn'; resumeBtn.innerHTML = '▶️ Resume'; resumeBtn.style.backgroundColor = '#28a745'; resumeBtn.style.display = 'none'; // Initially hidden const stopBtn = document.createElement('button'); stopBtn.id = 'stop-agent-btn-shadow'; stopBtn.className = 'control-btn'; stopBtn.innerHTML = '⏹️ Stop'; stopBtn.style.backgroundColor = '#dc3545'; content.appendChild(statusIndicator); content.appendChild(pauseBtn); content.appendChild(resumeBtn); content.appendChild(stopBtn); overlay.appendChild(header); overlay.appendChild(content); // --- Append Overlay to Shadow Root --- shadowRoot.appendChild(overlay); console.log('Appended overlay structure to shadow root'); // --- Functionality (Listeners, etc.) --- // Minimize/maximize let isMinimized = false; minimizeBtn.addEventListener('click', () => { if (isMinimized) { content.style.display = 'flex'; minimizeBtn.innerHTML = '−'; isMinimized = false; } else { content.style.display = 'none'; minimizeBtn.innerHTML = '+'; isMinimized = true; } }); // Draggable (using the host element for positioning calculations relative to viewport) makeDraggable(overlay, header); // Pass overlay element directly // Button listeners pauseBtn.addEventListener('click', async () => { try { if (typeof window.pauseAgent === 'function') { await window.pauseAgent(); updateOverlayStatus('paused'); } else { console.error('window.pauseAgent is not defined'); } } catch (e) { console.error('Failed to pause agent:', e); } }); resumeBtn.addEventListener('click', async () => { try { if (typeof window.resumeAgent === 'function') { await window.resumeAgent(); updateOverlayStatus('running'); } else { console.error('window.resumeAgent is not defined'); } } catch (e) { console.error('Failed to resume agent:', e); } }); stopBtn.addEventListener('click', async () => { try { if (typeof window.stopAgent === 'function') { await window.stopAgent(); updateOverlayStatus('stopped'); } else { console.error('window.stopAgent is not defined'); } } catch (e) { console.error('Failed to stop agent:', e); } }); // Function to update the overlay status (operates on elements within shadow DOM) function updateOverlayStatus(status) { // Use shadowRoot.getElementById for elements inside the shadow DOM const statusEl = shadowRoot.getElementById('agent-status-shadow'); const pauseBtnShadow = shadowRoot.getElementById('pause-agent-btn-shadow'); const resumeBtnShadow = shadowRoot.getElementById('resume-agent-btn-shadow'); const stopBtnShadow = shadowRoot.getElementById('stop-agent-btn-shadow'); if (!statusEl || !pauseBtnShadow || !resumeBtnShadow || !stopBtnShadow) { console.error('Could not find all status elements within shadow DOM'); return; } if (status === 'running') { statusEl.textContent = 'Running'; statusEl.style.backgroundColor = '#28a745'; pauseBtnShadow.style.display = 'flex'; // Use flex for consistency resumeBtnShadow.style.display = 'none'; stopBtnShadow.style.display = 'flex'; // Ensure stop is visible when running } else if (status === 'paused') { statusEl.textContent = 'Paused'; statusEl.style.backgroundColor = '#ffc107'; pauseBtnShadow.style.display = 'none'; resumeBtnShadow.style.display = 'flex'; stopBtnShadow.style.display = 'flex'; // Ensure stop is visible when paused } else if (status === 'stopped') { statusEl.textContent = 'Stopped'; statusEl.style.backgroundColor = '#dc3545'; pauseBtnShadow.style.display = 'none'; resumeBtnShadow.style.display = 'none'; stopBtnShadow.style.display = 'none'; // Hide stop when stopped } } // Make update function globally accessible if needed by external calls (less likely now) // window.updateOverlayStatus = updateOverlayStatus; // Probably not needed // Check agent state periodically (relies on functions exposed on window) const stateCheckInterval = setInterval(async () => { try { if (typeof window.getAgentState === 'function') { const state = await window.getAgentState(); if (state.stopped) { updateOverlayStatus('stopped'); clearInterval(stateCheckInterval); // Stop checking if agent stopped } else if (state.paused) { updateOverlayStatus('paused'); } else { updateOverlayStatus('running'); } } else { // If function doesn't exist, stop checking // console.warn('window.getAgentState not found, stopping state check.'); // clearInterval(stateCheckInterval); } } catch (e) { console.error('Failed to get agent state:', e); // clearInterval(stateCheckInterval); // Stop checking on error } }, 1000); // --- Helper Functions --- // Draggable function rewritten for correct positioning function makeDraggable(element, handle) { let startX = 0, startY = 0, initialElementX = 0, initialElementY = 0; handle.onmousedown = dragMouseDown; function dragMouseDown(e) { // Prevent default dragging behavior (e.g., text selection) e.preventDefault(); // Get the mouse cursor position at startup startX = e.clientX; startY = e.clientY; // Get the element's initial position using getBoundingClientRect for accuracy const rect = element.getBoundingClientRect(); initialElementX = rect.left; initialElementY = rect.top; // Ensure position is fixed and remove transform/conflicting styles // This ensures subsequent position updates use direct top/left element.style.transform = 'none'; element.style.position = 'fixed'; element.style.left = initialElementX + 'px'; // Set initial pixel value element.style.top = initialElementY + 'px'; // Set initial pixel value element.style.bottom = 'auto'; element.style.right = 'auto'; // Attach listeners to the document to capture mouse movements anywhere on the page document.addEventListener('mouseup', closeDragElement, true); document.addEventListener('mousemove', elementDrag, true); } function elementDrag(e) { e.preventDefault(); // Calculate the distance the mouse has moved const dx = e.clientX - startX; const dy = e.clientY - startY; // Calculate the new element position by adding the mouse delta to the initial position element.style.left = (initialElementX + dx) + 'px'; element.style.top = (initialElementY + dy) + 'px'; } function closeDragElement() { // Remove the listeners when the mouse button is released document.removeEventListener('mouseup', closeDragElement, true); document.removeEventListener('mousemove', elementDrag, true); } } // --- Patch elementFromPoint to ignore overlay --- // Keep references you’ll need inside the closure const host = hostElement; // div#agent-control-host const panelRoot = shadowRoot; // Reference the created shadowRoot // Utility: is a node somewhere inside the overlay? const isInsidePanel = node => node === host || (node && (host.contains(node) || (node.getRootNode() === panelRoot))); // Patch elementFromPoint / elementsFromPoint for Document and ShadowRoot ---- // ---- generic helper that works on any root object ---- function makeSafeElementFromPoint(root, origEFP, origEFPS) { // Note: We need the original EFP from the specific root's prototype const originalRootEFP = origEFP || root.elementFromPoint; return function safeEFP(x, y) { // `this` is the root (Document or ShadowRoot instance) let el = originalRootEFP.call(this, x, y); // Use call() with the correct context if (!isInsidePanel(el)) return el; const original = host.style.pointerEvents; host.style.pointerEvents = 'none'; try { el = originalRootEFP.call(this, x, y); } // Use call() again finally { host.style.pointerEvents = original; } return el; }; } function makeSafeElementsFromPoint(root, origEFP, origEFPS) { // Note: We need the original EFPs from the specific root's prototype const originalRootEFP = origEFP || root.elementFromPoint; const originalRootEFPS = origEFPS || root.elementsFromPoint; // Fallback function using the single safe EFP if elementsFromPoint doesn't exist const fallbackEFP = makeSafeElementFromPoint(root, originalRootEFP, originalRootEFPS); return function safeEFPS(x, y) { // `this` is the root (Document or ShadowRoot instance) if (!originalRootEFPS) return [fallbackEFP.call(this, x, y)]; // Use call() for fallback const original = host.style.pointerEvents; host.style.pointerEvents = 'none'; let elements = []; try { // Use call() with the correct context elements = originalRootEFPS.call(this, x, y).filter(el => !isInsidePanel(el)); } finally { host.style.pointerEvents = original; } return elements; }; } // ---- install on Document ---- const docOrigEFP = document.elementFromPoint; const docOrigEFPS = document.elementsFromPoint; document.elementFromPoint = makeSafeElementFromPoint(document, docOrigEFP, docOrigEFPS); console.log('Patched document.elementFromPoint'); if (docOrigEFPS) { document.elementsFromPoint = makeSafeElementsFromPoint(document, docOrigEFP, docOrigEFPS); console.log('Patched document.elementsFromPoint'); } // ---- install on *all* ShadowRoots (via prototype) ---- if (typeof ShadowRoot !== "undefined" && ShadowRoot.prototype) { const SRproto = ShadowRoot.prototype; const protoOrigEFP = SRproto.elementFromPoint; // Get originals from prototype const protoOrigEFPS = SRproto.elementsFromPoint; // Patch prototype methods SRproto.elementFromPoint = makeSafeElementFromPoint(SRproto, protoOrigEFP, protoOrigEFPS); console.log('Patched ShadowRoot.prototype.elementFromPoint'); if (protoOrigEFPS) { SRproto.elementsFromPoint = makeSafeElementsFromPoint(SRproto, protoOrigEFP, protoOrigEFPS); console.log('Patched ShadowRoot.prototype.elementsFromPoint'); } } else { console.warn('ShadowRoot prototype not available for patching.'); } // --- End Patch --- // --- Ensure Host is Last Element (for stacking) --- (function bringPanelToFront() { const TARGET_ID = "playwright-highlight-container"; // ID used by buildDomTree.js // helper: put hostElement at the very end of <body> const moveHostToEnd = () => { if (document.body && hostElement.parentNode === document.body) { document.body.appendChild(hostElement); // console.log('Moved host element to end of body'); } else if (document.body && hostElement.parentNode !== document.body) { // If host somehow got detached, re-append it document.body.appendChild(hostElement); // console.log('Re-appended host element to end of body'); } }; // 1. if the highlight container already exists, just move once if (document.getElementById(TARGET_ID)) { moveHostToEnd(); return; } // 2. otherwise observe <body> for that node, then move and stop. // Ensure body exists before observing if (!document.body) { console.warn('bringPanelToFront: document.body not ready for MutationObserver'); // Optionally, retry after a short delay setTimeout(bringPanelToFront, 100); return; } const obs = new MutationObserver(muts => { for (const m of muts) { for (const n of m.addedNodes) { // Check nodeType just in case non-elements are added if (n.nodeType === Node.ELEMENT_NODE && n.id === TARGET_ID) { moveHostToEnd(); obs.disconnect(); // console.log('Disconnected highlight container observer'); return; } } } }); obs.observe(document.body, { childList: true }); // console.log('Observing body for highlight container'); // Optional: Timeout to stop observing if highlight container never appears setTimeout(() => { obs.disconnect(); // console.log('Timeout reached for highlight container observer'); }, 10000); // Stop observing after 10 seconds })(); // --- End Stacking Fix --- console.log('Agent overlay setup complete.'); // End of successful try block logic } catch (error) { // Catch block Start console.error('Error during agent overlay setup:', error); // Reset flag on error to allow retry on next injection attempt window.__agentOverlayInjected = false; console.log('Resetting injection flag due to error.'); } // Catch block End })(); // End of main IIFE

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/withRefresh/web-eval-agent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server