UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

282 lines 10.4 kB
/** * Captures and manages application state including DOM, storage, and framework-specific state * Extracted from LocalDebugEngine for better separation of concerns */ export class StateCapture { reactStateEngine; jsExecutionEngine; flutterEnhancedEngine; page = null; constructor(reactStateEngine, jsExecutionEngine, flutterEnhancedEngine) { this.reactStateEngine = reactStateEngine; this.jsExecutionEngine = jsExecutionEngine; this.flutterEnhancedEngine = flutterEnhancedEngine; } /** * Attach to a page for state capturing */ async attachToPage(page) { this.page = page; } /** * Capture comprehensive debug state including storage, cookies, and URL */ async captureState() { if (!this.page) { throw new Error('No page attached'); } // Check if page/context is still valid before accessing let cookies = []; let url = ''; try { if (!this.page.isClosed()) { url = await this.page.url(); // Safely try to get cookies, but don't fail if context is closed try { cookies = await this.page.context().cookies(); } catch (cookieError) { if (cookieError.message.includes('Target page, context or browser has been closed')) { console.warn('⚠️ Cannot access cookies - browser context closed'); cookies = []; } else { throw cookieError; } } } else { console.warn('⚠️ Cannot capture state - page is closed'); url = 'about:blank'; } } catch (error) { if (error.message.includes('Target page, context or browser has been closed')) { console.warn('⚠️ Cannot access page state - browser closed'); url = 'about:blank'; } else { throw error; } } const [localStorage, sessionStorage] = await Promise.all([ this.captureStorage('localStorage').catch(() => ({})), this.captureStorage('sessionStorage').catch(() => ({})) ]); return { url, localStorage, sessionStorage, cookies, timestamp: new Date() }; } /** * Capture browser storage (localStorage or sessionStorage) */ async captureStorage(type) { if (!this.page) return {}; try { return await this.page.evaluate((storageType) => { const storage = window[storageType]; const items = {}; for (let i = 0; i < storage.length; i++) { const key = storage.key(i); if (key) { try { items[key] = JSON.parse(storage.getItem(key) || ''); } catch { items[key] = storage.getItem(key); } } } return items; }, type); } catch (error) { return {}; } } /** * Capture DOM structure snapshot */ async captureDOMSnapshot() { if (!this.page) { throw new Error('No page attached'); } const snapshot = await this.page.evaluate(() => { const serializeNode = (node) => { const attributes = {}; for (const attr of Array.from(node.attributes)) { attributes[attr.name] = attr.value; } return { tagName: node.tagName.toLowerCase(), attributes, children: Array.from(node.children).map(serializeNode), textContent: node.textContent?.trim() || '' }; }; return { html: document.documentElement.outerHTML, structure: serializeNode(document.documentElement), timestamp: new Date().toISOString() }; }); return { ...snapshot, timestamp: new Date(snapshot.timestamp) }; } /** * Find React components and their state */ async findReactComponents() { if (!this.page) { throw new Error('No page attached'); } return await this.page.evaluate(() => { const components = []; // Find React fiber nodes const findReactFiber = (element) => { const keys = Object.keys(element); const fiberKey = keys.find(key => key.startsWith('__reactFiber')); return fiberKey ? element[fiberKey] : null; }; // Walk the DOM and find React components const walkDOM = (element) => { const fiber = findReactFiber(element); if (fiber && fiber.elementType && typeof fiber.elementType === 'function') { components.push({ name: fiber.elementType.name || 'Anonymous', props: fiber.memoizedProps || {}, state: fiber.memoizedState || {} }); } Array.from(element.children).forEach(walkDOM); }; walkDOM(document.body); return components; }).catch(() => []); } /** * Find Phoenix LiveView state */ async findPhoenixLiveViewState() { if (!this.page) { throw new Error('No page attached'); } return await this.page.evaluate(() => { // Enhanced Phoenix LiveView detection const liveSocket = window.liveSocket; const phoenixSocket = window.phoenixSocket; const hasLiveViewElements = document.querySelector('[data-phx-main]') || document.querySelector('[phx-socket]') || document.querySelector('[data-phx-session]'); // Check for Phoenix LiveView JavaScript patterns const hasLiveViewJS = document.querySelector('script[src*="phoenix_live_view"]') || document.querySelector('script[src*="live_view"]') || window.location.pathname.includes('/live/'); if (!liveSocket && !phoenixSocket && !hasLiveViewElements && !hasLiveViewJS) { return null; } const views = []; // Find all LiveView elements document.querySelectorAll('[data-phx-view]').forEach((element) => { const phxView = element.getAttribute('data-phx-view'); const phxSession = element.getAttribute('data-phx-session'); views.push({ id: phxView, session: phxSession, element: { id: element.id, className: element.className, tagName: element.tagName } }); }); return { connected: liveSocket.isConnected(), views, hooks: Object.keys(window.Hooks || {}) }; }).catch(() => null); } /** * Get React component tree via ReactStateEngine */ async getReactComponentTree() { if (!this.reactStateEngine) { throw new Error('React state engine not initialized'); } return await this.reactStateEngine.captureComponentTree(); } /** * Get React state via ReactStateEngine */ async getReactState() { if (!this.reactStateEngine) { throw new Error('React state engine not initialized'); } return await this.reactStateEngine.getCurrentState(); } /** * Capture JavaScript variable state via JavaScriptExecutionEngine */ async captureVariableState(variablePath, scope) { if (!this.jsExecutionEngine) { throw new Error('JavaScript execution engine not initialized'); } return await this.jsExecutionEngine.captureVariableState(variablePath, scope); } /** * Capture Flutter state snapshot via FlutterDebugEngineEnhanced */ async captureFlutterStateSnapshot() { if (!this.flutterEnhancedEngine) { throw new Error('Enhanced Flutter engine not initialized'); } return await this.flutterEnhancedEngine.captureStateSnapshot(); } /** * Inspect element properties and computed styles */ async inspectElement(selector) { if (!this.page) { throw new Error('No page attached'); } return await this.page.evaluate((sel) => { const element = document.querySelector(sel); if (!element) { return { exists: false }; } const rect = element.getBoundingClientRect(); const computedStyle = window.getComputedStyle(element); return { exists: true, visible: rect.width > 0 && rect.height > 0 && computedStyle.visibility !== 'hidden' && computedStyle.display !== 'none', properties: { tagName: element.tagName, id: element.id, className: element.className, textContent: element.textContent, innerHTML: element.innerHTML, rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height } }, computedStyles: { display: computedStyle.display, visibility: computedStyle.visibility, opacity: computedStyle.opacity } }; }, selector); } } //# sourceMappingURL=state-capture.js.map