@autifyhq/muon
Version:
Muon - AI-Powered Playwright Test Coding Agent with Advanced Test Fixing Capabilities
331 lines (330 loc) • 21.3 kB
JavaScript
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)))] }));
};