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

357 lines (308 loc) • 9.95 kB
import { Page } from 'playwright'; import WebSocket from 'ws'; export interface FlutterWidget { id: string; type: string; properties: Record<string, any>; children: FlutterWidget[]; renderObject?: { type: string; size?: { width: number; height: number }; position?: { x: number; y: number }; }; } export interface FlutterState { widgets: FlutterWidget[]; performance: { fps: number; frameTime: number; widgetBuildTime: number; rasterTime: number; }; memory: { used: number; capacity: number; }; } export class FlutterDebugEngine { private ws?: WebSocket; private debugPort?: number; private vmServiceUrl?: string; private isConnected: boolean = false; async detectFlutterWeb(page: Page): Promise<boolean> { return await page.evaluate(() => { // Check for Flutter-specific globals and elements const hasFlutterGlobals = !!(window as any).flutterConfiguration || !!(window as any).$flutterDriver; const hasFlutterElements = document.querySelector('flt-scene-host') !== null || document.querySelector('flt-glass-pane') !== null || document.querySelector('flutter-view') !== null; // Check for Flutter-specific scripts const scripts = Array.from(document.querySelectorAll('script')); const hasFlutterScripts = scripts.some(script => script.src.includes('flutter.js') || script.src.includes('flutter_web') ); return hasFlutterGlobals || hasFlutterElements || hasFlutterScripts; }); } async findDebugPort(page: Page): Promise<number | null> { // Try to find Flutter DevTools port from console logs const logs = await page.evaluate(() => { return (window as any).__consoleHistory || []; }); // Look for Observatory/DevTools URL in logs const portMatch = logs.find((log: string) => log.includes('Observatory listening on') || log.includes('DevTools listening on') ); if (portMatch) { const match = portMatch.match(/:(\d+)/); if (match) { return parseInt(match[1]); } } // Try common Flutter debug ports const commonPorts = [9100, 9101, 9102, 9200, 9201]; for (const port of commonPorts) { if (await this.checkPort(port)) { return port; } } return null; } private async checkPort(port: number): Promise<boolean> { try { const response = await fetch(`http://localhost:${port}/ws`); return response.ok; } catch { return false; } } async connect(port: number): Promise<boolean> { if (this.isConnected) { return true; } try { this.debugPort = port; this.vmServiceUrl = `ws://localhost:${port}/ws`; return new Promise((resolve) => { this.ws = new WebSocket(this.vmServiceUrl!); this.ws.on('open', () => { this.isConnected = true; console.log('Connected to Flutter DevTools'); resolve(true); }); this.ws.on('error', (error) => { console.error('Flutter DevTools connection error:', error); resolve(false); }); this.ws.on('message', (data) => { this.handleMessage(data.toString()); }); }); } catch (error) { console.error('Failed to connect to Flutter DevTools:', error); return false; } } private handleMessage(message: string) { try { const data = JSON.parse(message); // Handle VM service protocol messages if (data.method === 'streamNotify') { this.handleStreamNotification(data.params); } } catch (error) { console.error('Error parsing Flutter message:', error); } } private handleStreamNotification(params: any) { // Handle different stream types switch (params.streamId) { case 'Extension': // Flutter extension events break; case 'Timeline': // Performance timeline events break; case 'Debug': // Debug events break; } } async getWidgetTree(): Promise<FlutterWidget | null> { if (!this.isConnected || !this.ws) { return null; } // Send request for widget tree const request = { id: Date.now().toString(), method: 'ext.flutter.inspector.getWidgetTree', params: {} }; return new Promise((resolve) => { const timeout = setTimeout(() => resolve(null), 5000); const handler = (data: WebSocket.Data) => { try { const response = JSON.parse(data.toString()); if (response.id === request.id) { clearTimeout(timeout); this.ws!.off('message', handler); resolve(this.parseWidgetTree(response.result)); } } catch (error) { console.error('Error parsing widget tree response:', error); } }; this.ws!.on('message', handler); this.ws!.send(JSON.stringify(request)); }); } private parseWidgetTree(data: any): FlutterWidget { return { id: data.id || 'root', type: data.description?.split('(')[0] || 'Unknown', properties: data.properties || {}, children: (data.children || []).map((child: any) => this.parseWidgetTree(child)), renderObject: data.renderObject ? { type: data.renderObject.description, size: data.renderObject.size, position: data.renderObject.position } : undefined }; } async getPerformanceInfo(): Promise<any> { if (!this.isConnected || !this.ws) { return null; } const request = { id: Date.now().toString(), method: 'ext.flutter.getVMTimeline', params: { timeOriginMicros: Date.now() * 1000 - 5000000, // Last 5 seconds timeExtentMicros: 5000000 } }; return new Promise((resolve) => { const timeout = setTimeout(() => resolve(null), 3000); const handler = (data: WebSocket.Data) => { try { const response = JSON.parse(data.toString()); if (response.id === request.id) { clearTimeout(timeout); this.ws!.off('message', handler); resolve(this.parsePerformanceData(response.result)); } } catch (error) { console.error('Error parsing performance data:', error); } }; this.ws!.on('message', handler); this.ws!.send(JSON.stringify(request)); }); } private parsePerformanceData(data: any): any { const events = data.traceEvents || []; // Calculate FPS from frame events const frameEvents = events.filter((e: any) => e.name === 'Frame'); const fps = frameEvents.length > 0 ? Math.round(1000000 / (frameEvents[frameEvents.length - 1].ts - frameEvents[0].ts) * frameEvents.length) : 0; // Calculate average frame times const buildTimes = events .filter((e: any) => e.name === 'Build') .map((e: any) => e.dur || 0); const avgBuildTime = buildTimes.length > 0 ? buildTimes.reduce((a: number, b: number) => a + b, 0) / buildTimes.length / 1000 : 0; return { fps, frameTime: fps > 0 ? 1000 / fps : 0, widgetBuildTime: avgBuildTime, rasterTime: 0, // Would need raster thread events eventCount: events.length }; } async inspectWidget(widgetId: string): Promise<any> { if (!this.isConnected || !this.ws) { return null; } const request = { id: Date.now().toString(), method: 'ext.flutter.inspector.getProperties', params: { objectId: widgetId, depth: 2 } }; return new Promise((resolve) => { const timeout = setTimeout(() => resolve(null), 3000); const handler = (data: WebSocket.Data) => { try { const response = JSON.parse(data.toString()); if (response.id === request.id) { clearTimeout(timeout); this.ws!.off('message', handler); resolve(response.result); } } catch (error) { console.error('Error parsing widget properties:', error); } }; this.ws!.on('message', handler); this.ws!.send(JSON.stringify(request)); }); } async highlightWidget(widgetId: string): Promise<void> { if (!this.isConnected || !this.ws) { return; } const request = { id: Date.now().toString(), method: 'ext.flutter.inspector.highlight', params: { objectId: widgetId } }; this.ws.send(JSON.stringify(request)); } async getMemoryUsage(): Promise<any> { if (!this.isConnected || !this.ws) { return null; } const request = { id: Date.now().toString(), method: 'getMemoryUsage', params: {} }; return new Promise((resolve) => { const timeout = setTimeout(() => resolve(null), 3000); const handler = (data: WebSocket.Data) => { try { const response = JSON.parse(data.toString()); if (response.id === request.id) { clearTimeout(timeout); this.ws!.off('message', handler); resolve({ used: response.result.heapUsage, capacity: response.result.heapCapacity, external: response.result.externalUsage }); } } catch (error) { console.error('Error parsing memory data:', error); } }; this.ws!.on('message', handler); this.ws!.send(JSON.stringify(request)); }); } disconnect(): void { if (this.ws) { this.ws.close(); this.ws = undefined; this.isConnected = false; } } }