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
JavaScript
/**
* 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