UNPKG

@autifyhq/muon

Version:

Muon - AI-Powered Playwright Test Coding Agent with Advanced Test Fixing Capabilities

331 lines (330 loc) 21.3 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Box, Newline, Spacer, Text, useFocus, useInput } from 'ink'; import { marked } from 'marked'; import { markedTerminal } from 'marked-terminal'; import React, { useCallback, useMemo, useState } from 'react'; marked.use(markedTerminal()); const MarkdownComponent = React.memo(({ children }) => { return (_jsx(Box, { children: _jsx(Text, { children: marked.parse(children) }) })); }); // Simplified streaming cursor - no blinking to prevent flashing const StreamingCursor = React.memo(({ isVisible }) => { if (!isVisible) return null; return _jsx(Text, { color: "cyan", children: "\u258B" }); }); export const WelcomePanel = ({ projectPath }) => { return (_jsx(Box, { borderStyle: "round", borderColor: "gray", padding: 1, marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "\uD83E\uDDEA Welcome to Muon - AI Test Agent!" }), _jsx(Text, { dimColor: true, children: "Advanced Playwright test automation with AI-powered failure fixing" }), _jsxs(Text, { dimColor: true, children: ["cwd: ", projectPath] })] }) })); }; export const TipsPanel = () => { return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { children: "\uD83E\uDDEA Test Automation & Fixing Examples:" }), _jsx(Text, {}), _jsx(Text, { children: "\u2022 Create tests: \"Write a Playwright test for the login page\"" }), _jsx(Text, { children: "\u2022 Fix failing tests: \"Fix the broken test in auth.spec.ts\"" }), _jsx(Text, { children: "\u2022 Test analysis: \"Analyze why tests/checkout.spec.ts is failing\"" }), _jsx(Text, { children: "\u2022 Update selectors: \"Update selectors in the dashboard test\"" }), _jsx(Text, { children: "\u2022 Generate from interactions: \"Record test by navigating to /signup\"" }), _jsx(Text, {}), _jsx(Text, { dimColor: true, children: "\uD83D\uDD27 Advanced Test Fixing: AI-powered failure analysis, browser simulation" }), _jsx(Text, { dimColor: true, children: "\uD83D\uDCCA Smart Analysis: Root cause detection, selector updates, timing fixes" }), _jsx(Text, { dimColor: true, children: "\u2328\uFE0F Press Ctrl+R to toggle tool details, ESC to interrupt, Ctrl+C to exit" })] })); }; export const InputBox = ({ placeholder = 'Type your message... (ESC to clear)', onSubmit, onCancel, disabled = false, isProcessing = false, }) => { const [input, setInput] = useState(''); const { isFocused } = useFocus({ autoFocus: !disabled }); useInput((inputStr, key) => { if (disabled) return; // Handle Ctrl+C to exit if (key.ctrl && inputStr === 'c') { process.exit(0); return; } // Handle Enter key presses first if (key.return) { // Plain Enter submits if (input.trim()) { onSubmit(input.trim()); setInput(''); } return; } // Check for newline characters (from paste) if (inputStr && (inputStr.includes('\n') || inputStr.includes('\r'))) { // Normalize line endings: convert \r\n and \r to \n const normalizedInput = inputStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); setInput((prev) => prev + normalizedInput); return; } // Handle Escape to cancel/clear (only when not disabled and not processing) // Global handler takes care of interrupt when processing if (key.escape && !disabled && !isProcessing) { if (onCancel) { onCancel(); } else { setInput(''); } return; } // Handle backspace and delete if (key.backspace || key.delete) { setInput((prev) => prev.slice(0, -1)); return; } // Handle regular character input (avoid control characters but preserve newlines) if (inputStr && !key.ctrl && !key.meta && !key.return) { // For pasted content, preserve newlines but filter other control chars if (inputStr.length > 1) { // Multi-character input (paste) - preserve newlines const cleanedInput = inputStr.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, ''); if (cleanedInput) { setInput((prev) => prev + cleanedInput); } } else { // Single character input - filter all control chars except newlines const printableChars = inputStr.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); if (printableChars) { setInput((prev) => prev + printableChars); } } } }, { isActive: !disabled } // Active when not disabled ); const showingPlaceholder = !input && !isFocused; return (_jsx(Box, { borderStyle: "round", borderColor: isFocused ? 'cyan' : 'gray', paddingX: 1, paddingY: 0, marginBottom: 1, width: "100%", children: _jsx(Box, { flexDirection: "column", width: "100%", children: showingPlaceholder ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "cyan", children: '> ' }), isFocused && !disabled ? (_jsx(Text, { backgroundColor: "cyan", color: "black", children: "\u2588" })) : (_jsx(Text, { dimColor: true, children: placeholder }))] })) : (_jsx(Box, { flexDirection: "column", width: "100%", children: input.split('\n').map((line, index, lines) => { const isFirstLine = index === 0; const isLastLine = index === lines.length - 1; return (_jsxs(Box, { flexDirection: "row", width: "100%", children: [_jsx(Text, { color: "cyan", children: isFirstLine ? '> ' : ' ' }), _jsxs(Box, { flexShrink: 1, flexGrow: 1, flexDirection: "row", children: [_jsx(Text, { wrap: "wrap", children: line }), isFocused && !disabled && isLastLine && _jsx(Text, { children: "\u2588" })] })] }, index)); }) })) }) })); }; export const StatusBar = ({ error, showToolResults = false, isProcessing = false, }) => { return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Ctrl+R to toggle tool calls & results: " }), _jsx(Spacer, {}), _jsx(Text, { dimColor: true, children: showToolResults ? _jsx(Text, { color: "green", children: "full" }) : _jsx(Text, { color: "yellow", children: "minimal" }) }), isProcessing && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " \u2022 " }), _jsx(Text, { color: "cyan", children: "Processing ..." })] })), error && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "red", children: ["\u2717 ", error] }), _jsx(Text, { dimColor: true, children: " \u2022 Try " }), _jsx(Text, { color: "cyan", children: "muon doctor" }), _jsx(Text, { dimColor: true, children: " or " }), _jsx(Text, { color: "cyan", children: "npm i -g @autifyhq/muon" })] }))] })); }; export const ProgressBar = ({ current, total, width = 20, showPercent = true, }) => { const percentage = Math.round((current / total) * 100); const filledWidth = Math.round((current / total) * width); const emptyWidth = width - filledWidth; const filled = '█'.repeat(filledWidth); const empty = '░'.repeat(emptyWidth); return (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: filled }), _jsx(Text, { color: "gray", children: empty }), showPercent && _jsxs(Text, { children: [" ", percentage, "%"] })] })); }; export const StatusBadge = ({ status, children }) => { const styles = { success: { color: 'green', icon: '✅' }, error: { color: 'red', icon: '❌' }, warning: { color: 'yellow', icon: '⚠️' }, info: { color: 'blue', icon: 'ℹ️' }, processing: { color: 'cyan', icon: '⚡' }, }; return (_jsx(Box, { children: _jsxs(Text, { color: styles[status].color, children: [styles[status].icon, " ", children] }) })); }; export const MessageBox = ({ type, content, showToolResults = true, tool_calls, }) => { const getMessageStyle = (messageType) => { switch (messageType) { case 'user': return { color: 'cyan', icon: '👤', prefix: 'You', bgColor: undefined, bold: true, }; case 'assistant': case 'assistant_start': case 'assistant_delta': case 'assistant_complete': return { color: 'green', icon: '🤖', prefix: 'Muon', bgColor: undefined, bold: false, }; case 'system': return { color: 'blue', icon: 'ℹ️', prefix: 'System', bgColor: undefined, bold: false, }; case 'tool_call': return { color: 'yellow', icon: '⚡', prefix: 'Tool', bgColor: undefined, bold: false, }; case 'tool_result': return { color: 'gray', icon: '✅', prefix: 'Result', bgColor: undefined, bold: false, }; case 'error': return { color: 'red', icon: '❌', prefix: 'Error', bgColor: undefined, bold: true, }; default: return { color: 'white', icon: '', prefix: '', bgColor: undefined, bold: false, }; } }; const style = getMessageStyle(type); const getToolDisplayInfo = useCallback((toolCalls) => { if (!toolCalls || toolCalls.length === 0) return null; const toolCall = toolCalls[0]; const toolName = toolCall.function?.name; if (!toolCall.function?.arguments) { return `${toolName}...`; } let args; try { args = JSON.parse(toolCall.function?.arguments || '{}'); } catch (_e) { return `${toolName}...`; } // Specialized messages for each tool switch (toolName) { case 'readFile': return `Reading file ${args.filePath || 'unknown'}...`; case 'writeFile': return `Writing file ${args.filePath || 'unknown'}...`; case 'listDirectory': return `Listing directory ${args.dirPath || '.'}...`; case 'searchFileContent': { const searchPattern = args.text || args.pattern || args.query; const filePattern = args.filePattern || 'all files'; return `Searching "${searchPattern}" in ${filePattern}...`; } case 'executeCommand': { const cmd = args.command; return `Executing \`${cmd.length > 100 ? `${cmd.substring(0, 100)}...` : cmd}\`...`; } case 'initializeBrowser': return `Initializing browser ...`; case 'navigate': return `Navigating to ${args.url || 'URL'}...`; case 'clickElement': return `Clicking element ${args.selector || 'unknown'}...`; case 'inputText': { const text = args.text; return `Typing "${text.length > 20 ? `${text.substring(0, 20)}...` : text}" into ${args.selector || 'element'}...`; } case 'selectOption': return `Selecting option in ${args.selector || 'dropdown'}...`; case 'checkCheckbox': return `${args.checked ? 'Checking' : 'Unchecking'} ${args.selector || 'checkbox'}...`; case 'assertText': { const expectedText = args.text; return `Asserting text "${expectedText.length > 20 ? `${expectedText.substring(0, 20)}...` : expectedText}" in ${args.selector || 'element'}...`; } case 'assertUrl': return `Asserting URL ${args.url || args.expectedUrl || 'matches'}...`; case 'sleep': return `Waiting ${args.seconds || args.duration || 1} seconds...`; case 'getBrowserStatus': return `Getting browser status...`; case 'clearRecordedSteps': return `Clearing recorded steps...`; case 'closeBrowser': return `Closing browser...`; case 'killBackgroundProcesses': return `Killing background processes...`; case 'getPageDOM': return `Getting page DOM...`; case 'getMainFrameDOM': return `Getting main frame DOM...`; case 'getIframeDOM': return `Getting iframe DOM...`; case 'listIframes': return `Listing iframes...`; case 'conclude': return `Concluding ${args.success ? 'successfully' : 'with error'}...`; default: return `${toolName}...`; } }, []); // Strip tool content when showToolResults is false const displayContent = useMemo(() => { if (!showToolResults && (type === 'tool_call' || type === 'tool_result')) { if (type === 'tool_call') { if (tool_calls && tool_calls.length > 0) { return getToolDisplayInfo(tool_calls); } const toolMatch = content.match(/Executing (\w+)/); return toolMatch ? `${toolMatch[1]}...` : 'Running tool...'; } else { // Show minimal result info return content.length > 100 ? `${content.slice(0, 100)}...` : content; } } return content; }, [content, type, showToolResults, tool_calls, getToolDisplayInfo]); if (!displayContent || !displayContent.trim()) return null; return (_jsx(Box, { marginY: type === 'user' || type === 'assistant' ? 1 : 0, children: _jsxs(Box, { minWidth: 0, children: [style.icon && (_jsxs(Text, { color: style.color, bold: style.bold, children: [style.icon, style.prefix ? ` ${style.prefix}: ` : ' '] })), _jsx(MarkdownComponent, { children: displayContent }), type === 'assistant' && displayContent.endsWith('streaming') && (_jsx(StreamingCursor, { isVisible: true }))] }) })); }; // Special component for streaming messages to reduce re-renders const StreamingMessageBox = React.memo(({ content, isProcessing }) => { return (_jsx(Box, { marginY: 1, children: _jsxs(Box, { minWidth: 0, children: [_jsxs(Text, { color: "green", bold: false, children: ["\uD83E\uDD16 Muon:", ' '] }), _jsx(MarkdownComponent, { children: content }), _jsx(StreamingCursor, { isVisible: isProcessing })] }) })); }); export const ChatContainer = ({ messages, isProcessing = false, streamingContent = '', showToolResults = false, }) => { return (_jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [messages.map((message, index) => (_jsx(MessageBox, { type: message.type, content: message.content, showToolResults: showToolResults, tool_calls: message.tool_calls }, `${message.session_id}-${index}`))), streamingContent.trim() && (_jsx(StreamingMessageBox, { content: streamingContent, isProcessing: isProcessing }))] }) })); }; export const StatusPanel = ({ isProcessing, sessionId, serverUrl, agentType, }) => { const displaySessionId = sessionId === 'Not connected' ? 'Not connected' : sessionId.slice(-8); return (_jsx(Box, { borderStyle: "round", borderColor: "blue", padding: 1, marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "blue", children: "\uD83E\uDDEA Muon 2.0 - Terminal UI" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Status: " }), isProcessing ? (_jsx(Box, { flexDirection: "row", children: _jsx(Text, { color: "yellow", children: " Processing..." }) })) : (_jsx(Text, { color: "green", children: "Ready" }))] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Server: " }), _jsx(Text, { children: serverUrl })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Agent: " }), _jsx(Text, { color: "cyan", children: agentType })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Session: " }), _jsx(Text, { dimColor: true, children: displaySessionId })] })] }) })); }; export const HelpPanel = ({ shortcuts = [] }) => { const defaultShortcuts = [ { key: 'Enter', description: 'Send message' }, { key: 'Escape', description: 'Clear input' }, { key: 'Ctrl+C', description: 'Exit application' }, { key: 'Ctrl+H', description: 'Toggle help panel' }, ]; const displayShortcuts = shortcuts.length > 0 ? shortcuts : defaultShortcuts; return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "\uD83E\uDDED Help & Shortcuts" }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Keyboard Shortcuts:" }), displayShortcuts.map((shortcut, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "yellow", bold: true, children: shortcut.key.padEnd(12) }), _jsx(Text, { children: shortcut.description })] }, index)))] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Commands:" }), _jsx(Text, { children: "Type any natural language instructions to interact with Muon." }), _jsx(Text, { children: "Muon will understand your intent and execute appropriate tools." })] })] })); }; export const TrustConfirmation = ({ projectPath, onConfirm, onExit, }) => { const [selectedOption, setSelectedOption] = useState(0); const { isFocused } = useFocus({ autoFocus: true }); useInput((input, key) => { if (key.upArrow || (key.shift && key.tab)) { setSelectedOption((prev) => (prev > 0 ? prev - 1 : 1)); } else if (key.downArrow || key.tab) { setSelectedOption((prev) => (prev < 1 ? prev + 1 : 0)); } else if (key.return) { if (selectedOption === 0) { onConfirm(); } else { onExit(); } } else if (key.escape) { onExit(); } else if (input === '1') { setSelectedOption(0); onConfirm(); } else if (input === '2') { setSelectedOption(1); onExit(); } }, { isActive: isFocused }); return (_jsx(Box, { justifyContent: "center", alignItems: "center", height: "100%", children: _jsxs(Box, { borderStyle: "round", borderColor: "gray", padding: 2, width: 110, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Do you trust the files in this folder?" }), _jsx(Newline, {}), _jsx(Text, { color: "cyan", children: projectPath }), _jsx(Newline, {}), _jsx(Text, { children: "Muon CLI may read files in this folder. Reading untrusted files may lead Muon CLI to behave in unexpected ways." }), _jsx(Newline, {}), _jsx(Text, { children: "With your permission Muon CLI may read/write/execute files in this folder. Executing untrusted code is unsafe." }), _jsx(Newline, {})] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: selectedOption === 0 ? 'cyan' : 'white', children: [selectedOption === 0 ? '❯ ' : ' ', "1. Yes, proceed"] }) }), _jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: selectedOption === 1 ? 'cyan' : 'white', children: [selectedOption === 1 ? '❯ ' : ' ', "2. No, exit"] }) })] }), _jsx(Newline, {}), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to exit" })] }) })); }; export const TaskProgress = ({ tasks }) => { return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "blue", children: "Task Progress" }), _jsx(Newline, {}), tasks.map((task, index) => (_jsxs(Box, { marginBottom: index === tasks.length - 1 ? 0 : 1, children: [_jsx(Box, { minWidth: 20, children: _jsxs(Text, { children: [task.status === 'completed' && '✅ ', task.status === 'failed' && '❌ ', task.status === 'in_progress' && '⚡ ', task.status === 'pending' && '⏳ ', task.title] }) }), task.status === 'in_progress' && task.progress !== undefined && (_jsx(Box, { marginLeft: 2, children: _jsx(ProgressBar, { current: task.progress, total: 100, width: 15 }) }))] }, task.id)))] })); };