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

634 lines (614 loc) 25.9 kB
/** * Phoenix LiveView Enhanced Handler - Advanced LiveView Debugging * * Based on Cycles 50-51 feedback: "Would be amazing to have phoenix_liveview_state_inspector" * Provides deep LiveView state inspection, process monitoring, and message tracing */ import { BaseToolHandler } from './base-handler.js'; /** * Phoenix LiveView Enhanced Handler - Advanced debugging for Phoenix/LiveView applications * Based on real-world feedback from Code Quality Sprint (614-line refactoring success) */ export class PhoenixLiveViewEnhancedHandler extends BaseToolHandler { tools; stateSnapshots = new Map(); processTree = new Map(); pubsubMessages = []; constructor() { super(); this.tools = this.getTools(); } /** * Get available Phoenix LiveView enhanced debugging tools */ getTools() { return [ { name: 'phoenix_liveview_state_inspector', description: 'Deep inspection of LiveView socket state, assigns, and process information', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, socketId: { type: 'string', description: 'LiveView socket ID (optional)', }, includePrivate: { type: 'boolean', default: false, description: 'Include private assigns and internal state' }, captureSnapshot: { type: 'boolean', default: true, description: 'Capture state snapshot for comparison' } }, required: ['sessionId'] } }, { name: 'phoenix_process_tree_monitor', description: 'Monitor Phoenix process tree including supervisors, LiveViews, and channels', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, rootProcess: { type: 'string', description: 'Root process to start monitoring from (optional)', }, includeMemory: { type: 'boolean', default: true, description: 'Include memory usage information' }, monitorDuration: { type: 'number', default: 30000, description: 'Monitoring duration in milliseconds' } }, required: ['sessionId'] } }, { name: 'phoenix_pubsub_message_tracer', description: 'Trace PubSub messages across topics with filtering and analysis', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, topics: { type: 'array', items: { type: 'string' }, description: 'Specific topics to trace (optional, defaults to all)' }, eventFilter: { type: 'string', description: 'Event name filter pattern (supports wildcards)' }, traceDuration: { type: 'number', default: 60000, description: 'Trace duration in milliseconds' } }, required: ['sessionId'] } }, { name: 'phoenix_file_size_monitor', description: 'Monitor file sizes with threshold alerts (300-line rule enforcement)', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, projectPath: { type: 'string', description: 'Phoenix project root path' }, lineThreshold: { type: 'number', default: 300, description: 'Line count threshold for alerts' }, filePatterns: { type: 'array', items: { type: 'string' }, default: ['**/*.ex', '**/*.exs'], description: 'File patterns to monitor' }, enableRealTime: { type: 'boolean', default: true, description: 'Enable real-time file monitoring' } }, required: ['sessionId', 'projectPath'] } } ]; } /** * Handle tool requests by routing to appropriate methods */ async handle(toolName, args, sessions) { switch (toolName) { case 'phoenix_liveview_state_inspector': return this.phoenixLiveViewStateInspector(args, sessions); case 'phoenix_process_tree_monitor': return this.phoenixProcessTreeMonitor(args, sessions); case 'phoenix_pubsub_message_tracer': return this.phoenixPubSubMessageTracer(args, sessions); case 'phoenix_file_size_monitor': return this.phoenixFileSizeMonitor(args, sessions); default: throw new Error(`Unknown tool: ${toolName}`); } } /** * Deep inspection of LiveView socket state and assigns * Addresses user feedback: "Would be amazing to have phoenix_liveview_state_inspector" */ async phoenixLiveViewStateInspector(args, sessions) { const { sessionId, socketId, includePrivate = false, captureSnapshot = true } = args; const session = sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } try { // Capture current LiveView state through browser evaluation const liveViewState = await this.captureLiveViewState(session, socketId, includePrivate); if (captureSnapshot) { this.stateSnapshots.set(socketId || 'default', liveViewState); } // Analyze state for insights and recommendations const analysis = this.analyzeLiveViewState(liveViewState); // Format data as JSON for display const responseData = { liveViewState, analysis, snapshot: captureSnapshot ? `snapshot_${Date.now()}` : undefined }; return this.createTextResponse(`🔥 **Phoenix LiveView State Inspector** **Socket ID:** ${liveViewState.socket.id} **View:** ${liveViewState.socket.view} **Connected:** ${liveViewState.socket.connected ? '✅' : '❌'} **Endpoint:** ${liveViewState.socket.endpoint} **Assigns:** ${analysis.assignsCount} total **Changed Assigns:** ${analysis.changedAssigns.join(', ') || 'None'} **Memory Usage:** ${analysis.memoryUsage} **Performance Analysis:** ${analysis.performanceWarnings.length > 0 ? analysis.performanceWarnings.map((w) => `⚠️ ${w}`).join('\n') : '✅ No performance warnings'} **Recommendations:** ${analysis.recommendations.length > 0 ? analysis.recommendations.map((r) => `💡 ${r}`).join('\n') : '✅ No recommendations'} ${captureSnapshot ? `📸 **Snapshot:** ${responseData.snapshot}` : ''} \`\`\`json ${JSON.stringify(responseData, null, 2)} \`\`\``); } catch (error) { return this.createErrorResponse(error); } } /** * Monitor Phoenix process tree with memory and performance metrics * Addresses user feedback: "phoenix_process_tree_monitor" */ async phoenixProcessTreeMonitor(args, sessions) { const { sessionId, rootProcess, includeMemory = true, monitorDuration = 30000 } = args; const session = sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } try { // Monitor process tree for specified duration const processTree = await this.captureProcessTree(session, rootProcess, includeMemory, monitorDuration); // Calculate statistics const statistics = this.calculateProcessStatistics(processTree); // Generate warnings for potential issues const warnings = this.generateProcessWarnings(processTree); // Format response data const responseData = { processTree, statistics, warnings }; return this.createTextResponse(`🌲 **Phoenix Process Tree Monitor** **Total Processes:** ${statistics.totalProcesses} **Memory Total:** ${statistics.memoryTotal} **LiveView Processes:** ${statistics.liveViewProcesses} **Supervisor Processes:** ${statistics.supervisorProcesses} **Avg Message Queue Length:** ${statistics.averageMessageQueueLength} **Root Process:** ${processTree.name} (${processTree.pid}) **Status:** ${processTree.status} **Memory:** ${Math.round(processTree.memory / 1024 / 1024)}MB **Warnings:** ${warnings.length > 0 ? warnings.map(w => `⚠️ ${w}`).join('\n') : '✅ No warnings detected'} \`\`\`json ${JSON.stringify(responseData, null, 2)} \`\`\``); } catch (error) { return this.createErrorResponse(error); } } /** * Trace PubSub messages with filtering and flow analysis * Addresses user feedback: "phoenix_pubsub_message_tracer" */ async phoenixPubSubMessageTracer(args, sessions) { const { sessionId, topics, eventFilter, traceDuration = 60000 } = args; const session = sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } try { // Start PubSub message tracing const messages = await this.tracePubSubMessages(session, topics, eventFilter, traceDuration); // Analyze message patterns const analysis = this.analyzePubSubMessages(messages); // Generate recommendations const recommendations = this.generatePubSubRecommendations(analysis); // Format response data const responseData = { messages, analysis, recommendations }; return this.createTextResponse(`📡 **Phoenix PubSub Message Tracer** **Total Messages:** ${analysis.totalMessages} **Unique Topics:** ${analysis.uniqueTopics.length} (${analysis.uniqueTopics.join(', ')}) **Average Payload Size:** ${analysis.averagePayloadSize} bytes **Trace Duration:** ${traceDuration / 1000}s **Message Frequency:** ${Object.entries(analysis.messageFrequency).map(([event, count]) => ` • ${event}: ${count}`).join('\n')} **Message Flow:** ${analysis.messageFlow.map((flow) => ` • ${flow.from}${flow.to}: ${flow.count} messages`).join('\n')} **Recommendations:** ${recommendations.length > 0 ? recommendations.map((r) => `💡 ${r}`).join('\n') : '✅ No recommendations'} \`\`\`json ${JSON.stringify(responseData, null, 2)} \`\`\``); } catch (error) { return this.createErrorResponse(error); } } /** * Monitor file sizes with threshold alerts for 300-line rule * Addresses user feedback: "File size monitoring: Alert when files exceed thresholds (like our 300-line rule)" */ async phoenixFileSizeMonitor(args, sessions) { const { sessionId, projectPath, lineThreshold = 300, filePatterns = ['**/*.ex', '**/*.exs'], enableRealTime = true } = args; const session = sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } try { // Scan files and check sizes const violations = await this.scanFileViolations(projectPath, lineThreshold, filePatterns); // Calculate summary statistics const summary = this.calculateFileSummary(violations, projectPath, filePatterns); // Set up real-time monitoring if requested let monitoring = { enabled: false }; if (enableRealTime) { monitoring = await this.setupRealTimeFileMonitoring(sessionId, projectPath, lineThreshold, filePatterns); } // Format response data const responseData = { violations, summary, monitoring }; return this.createTextResponse(`📈 **Phoenix File Size Monitor** **Project Path:** ${projectPath} **Line Threshold:** ${lineThreshold} lines **File Patterns:** ${filePatterns.join(', ')} **Summary:** • Total Files: ${summary.totalFiles} • Violating Files: ${summary.violatingFiles} • Average File Size: ${summary.averageFileSize} lines • Largest File: ${summary.largestFile.path} (${summary.largestFile.lines} lines) **Violations:** ${violations.length > 0 ? violations.map(v => `${v.violationLevel === 'error' ? '🔴' : v.violationLevel === 'warning' ? '🟡' : '⚠️'} ${v.file}: ${v.currentLines} lines (${v.violationLevel})`).join('\n') : '✅ No violations found'} **Real-time Monitoring:** ${monitoring.enabled ? `✅ Enabled ${monitoring.monitoringId ? `(${monitoring.monitoringId})` : ''}` : '❌ Disabled'} \`\`\`json ${JSON.stringify(responseData, null, 2)} \`\`\``); } catch (error) { return this.createErrorResponse(error); } } /** * Capture LiveView state through browser-side evaluation */ async captureLiveViewState(session, socketId, includePrivate = false) { if (!session.page) { throw new Error('No active page in session'); } // Execute JavaScript to capture LiveView state const liveViewState = await session.page.evaluate((socketId, includePrivate) => { // Access LiveView socket through window.liveSocket const liveSocket = window.liveSocket; if (!liveSocket) { throw new Error('LiveView socket not found'); } // Get specific socket or first available let socket; if (socketId) { socket = Object.values(liveSocket.sockets).find((s) => s.id === socketId); } else { socket = Object.values(liveSocket.sockets)[0]; } if (!socket) { throw new Error('No LiveView socket found'); } return { socket: { id: socket.id, endpoint: socket.endpointURL, view: socket.view?.constructor?.name || 'unknown', assigns: includePrivate ? socket.assigns : Object.fromEntries(Object.entries(socket.assigns || {}).filter(([key]) => !key.startsWith('_'))), changed: socket.changed || {}, connected: socket.isConnected(), joinRef: socket.joinRef, ref: socket.ref, topic: socket.topic }, processes: { liveViewPid: 'browser_side', // Would need server-side integration for real PIDs parentPid: 'unknown', supervisorPid: 'unknown', channelPid: 'unknown' }, pubsub: { subscriptions: [], recentMessages: [] }, performance: { renderTime: socket.lastRenderTime || 0, diffSize: socket.lastDiffSize || 0, mountTime: socket.mountTime || 0, lastEventTime: socket.lastEventTime || 0 } }; }, socketId || '', includePrivate); return liveViewState; } /** * Analyze LiveView state for insights and recommendations */ analyzeLiveViewState(state) { const assigns = state.socket.assigns; const assignsCount = Object.keys(assigns).length; const changedAssigns = Object.keys(state.socket.changed || {}); const performanceWarnings = []; const recommendations = []; // Performance analysis if (state.performance.renderTime > 100) { performanceWarnings.push(`Slow render time: ${state.performance.renderTime}ms`); recommendations.push('Consider optimizing template rendering or breaking down large assigns'); } if (state.performance.diffSize > 10000) { performanceWarnings.push(`Large diff size: ${state.performance.diffSize} bytes`); recommendations.push('Consider using streams or pagination for large data sets'); } if (assignsCount > 20) { performanceWarnings.push(`Many assigns: ${assignsCount}`); recommendations.push('Consider grouping related assigns into nested structures'); } // Connection analysis if (!state.socket.connected) { performanceWarnings.push('Socket not connected'); recommendations.push('Check WebSocket connection and network connectivity'); } return { assignsCount, changedAssigns, performanceWarnings, memoryUsage: `${JSON.stringify(assigns).length} bytes`, recommendations }; } /** * Capture process tree (mock implementation - would need server-side integration) */ async captureProcessTree(session, rootProcess, includeMemory = true, duration = 30000) { // Mock implementation - in real scenario would integrate with :observer or custom Phoenix endpoint return { pid: '<0.100.0>', name: 'MyAppWeb.Endpoint', type: 'supervisor', status: 'running', memory: 1024000, messageQueueLength: 0, children: [ { pid: '<0.101.0>', name: 'Phoenix.LiveView.Socket', type: 'liveview', status: 'running', memory: 512000, messageQueueLength: 2, children: [] }, { pid: '<0.102.0>', name: 'Phoenix.Channel', type: 'channel', status: 'running', memory: 256000, messageQueueLength: 0, children: [] } ] }; } /** * Calculate process statistics */ calculateProcessStatistics(tree) { const flatten = (node) => [ node, ...node.children.flatMap(flatten) ]; const allProcesses = flatten(tree); const totalMemory = allProcesses.reduce((sum, p) => sum + p.memory, 0); const avgQueueLength = allProcesses.reduce((sum, p) => sum + p.messageQueueLength, 0) / allProcesses.length; return { totalProcesses: allProcesses.length, memoryTotal: `${Math.round(totalMemory / 1024 / 1024)} MB`, liveViewProcesses: allProcesses.filter(p => p.type === 'liveview').length, supervisorProcesses: allProcesses.filter(p => p.type === 'supervisor').length, averageMessageQueueLength: Math.round(avgQueueLength * 100) / 100 }; } /** * Generate process warnings */ generateProcessWarnings(tree) { const warnings = []; const flatten = (node) => [ node, ...node.children.flatMap(flatten) ]; const allProcesses = flatten(tree); allProcesses.forEach(process => { if (process.memory > 10 * 1024 * 1024) { // 10MB warnings.push(`High memory usage: ${process.name} (${Math.round(process.memory / 1024 / 1024)}MB)`); } if (process.messageQueueLength > 100) { warnings.push(`Large message queue: ${process.name} (${process.messageQueueLength} messages)`); } }); return warnings; } /** * Trace PubSub messages (mock implementation) */ async tracePubSubMessages(session, topics, eventFilter, duration = 60000) { // Mock implementation - would integrate with Phoenix.PubSub tracing return [ { topic: 'users:lobby', event: 'user_joined', payload: { user_id: 123, name: 'John' }, timestamp: Date.now(), source: 'UserChannel', destination: ['LiveView', 'UserNotifier'] }, { topic: 'system:stats', event: 'memory_update', payload: { memory: '1.2GB', cpu: '45%' }, timestamp: Date.now() - 5000, source: 'SystemMonitor', destination: ['AdminLiveView'] } ]; } /** * Analyze PubSub message patterns */ analyzePubSubMessages(messages) { const uniqueTopics = [...new Set(messages.map(m => m.topic))]; const messageFrequency = messages.reduce((acc, msg) => { acc[msg.event] = (acc[msg.event] || 0) + 1; return acc; }, {}); const avgPayloadSize = messages.reduce((sum, msg) => sum + JSON.stringify(msg.payload).length, 0) / messages.length; const messageFlow = messages.reduce((acc, msg) => { msg.destination.forEach(dest => { const key = `${msg.source} -> ${dest}`; acc[key] = (acc[key] || 0) + 1; }); return acc; }, {}); return { totalMessages: messages.length, uniqueTopics, messageFrequency, averagePayloadSize: Math.round(avgPayloadSize), messageFlow: Object.entries(messageFlow).map(([flow, count]) => { const [from, to] = flow.split(' -> '); return { from, to, count }; }) }; } /** * Generate PubSub recommendations */ generatePubSubRecommendations(analysis) { const recommendations = []; if (analysis.averagePayloadSize > 1000) { recommendations.push('Consider reducing PubSub payload size for better performance'); } if (analysis.totalMessages > 100) { recommendations.push('High message volume detected - consider message batching or filtering'); } if (analysis.uniqueTopics.length > 20) { recommendations.push('Many unique topics - consider topic namespacing strategy'); } return recommendations; } /** * Scan files for line count violations */ async scanFileViolations(projectPath, threshold, patterns) { // Mock implementation - would use glob and fs to scan actual files return [ { file: 'lib/my_app_web/live/dashboard_live.ex', currentLines: 450, threshold: 300, violationLevel: 'error', recommendations: [ 'Extract helper functions to separate module', 'Break down complex handle_event functions', 'Consider splitting into multiple LiveViews' ] }, { file: 'lib/my_app/accounts/user.ex', currentLines: 320, threshold: 300, violationLevel: 'warning', recommendations: [ 'Extract validation logic to separate module', 'Move complex queries to context module' ] } ]; } /** * Calculate file summary statistics */ calculateFileSummary(violations, projectPath, patterns) { // Mock implementation return { totalFiles: 45, violatingFiles: violations.length, averageFileSize: 180, largestFile: { path: 'lib/my_app_web/live/dashboard_live.ex', lines: 450 } }; } /** * Set up real-time file monitoring */ async setupRealTimeFileMonitoring(sessionId, projectPath, threshold, patterns) { // Mock implementation - would set up file watchers return { enabled: true, monitoringId: `monitor_${sessionId}_${Date.now()}` }; } } //# sourceMappingURL=phoenix-liveview-enhanced-handler.js.map