@autifyhq/muon
Version:
Muon - AI-Powered Playwright Test Coding Agent with Advanced Test Fixing Capabilities
178 lines (177 loc) • 7.5 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Box, useApp, useInput } from 'ink';
import { useCallback, useEffect, useRef, useState } from 'react';
import { StreamingMuonAgent } from '../streaming-agent.js';
import { ChatContainer, InputBox, StatusBar, TipsPanel, WelcomePanel } from './components.js';
export const MuonApp = ({ serverUrl, agentType, projectPath, apiKey, accessToken, auth, nlstepMode = false, }) => {
const [messages, setMessages] = useState([]);
const [isProcessing, setIsProcessing] = useState(false);
const [currentAgent, setCurrentAgent] = useState(null);
const [sessionId, setSessionId] = useState('');
const [streamingContent, setStreamingContent] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [hasStartedChat, setHasStartedChat] = useState(false);
const [showToolResults, setShowToolResults] = useState(false);
// Throttle streaming updates to reduce flashing
const streamingBuffer = useRef('');
const streamingTimeoutRef = useRef(null);
const { exit } = useApp();
useInput((input, key) => {
if (key.ctrl && input === 'c') {
exit();
}
if (key.ctrl && input === 'r') {
setShowToolResults((prev) => !prev);
}
if (key.escape && isProcessing) {
handleInterrupt();
}
}, { isActive: true } // Always active for Ctrl shortcuts, but ESC only handled when processing
);
const addMessage = useCallback((message) => {
if (message.session_id && message.session_id !== sessionId) {
setSessionId(message.session_id);
}
if (message.type === 'assistant_start') {
// Clear buffer and streaming content
streamingBuffer.current = '';
setStreamingContent('');
setIsStreaming(true);
return;
}
if (message.type === 'assistant_delta') {
// Buffer streaming content and update immediately
streamingBuffer.current += message.content || '';
setStreamingContent(streamingBuffer.current);
return;
}
if (message.type === 'assistant_complete') {
// Clear any pending timeout and use final buffer content
if (streamingTimeoutRef.current) {
clearTimeout(streamingTimeoutRef.current);
}
const finalContent = streamingBuffer.current;
if (finalContent.trim()) {
setMessages((prev) => [
...prev,
{
type: 'assistant',
content: finalContent,
session_id: message.session_id,
timestamp: new Date(),
},
]);
}
// Clear streaming state and buffer
streamingBuffer.current = '';
setStreamingContent('');
setIsStreaming(false);
return;
}
// Prevent duplicate user messages by checking if the last message has the same content and type
if (message.type === 'user') {
setMessages((prev) => {
const lastMessage = prev[prev.length - 1];
if (lastMessage &&
lastMessage.type === 'user' &&
lastMessage.content === message.content) {
return prev; // Skip duplicate user message
}
return [...prev, message];
});
return;
}
setMessages((prev) => [...prev, message]);
}, [sessionId]);
const handleSubmit = useCallback(async (input) => {
if (!input.trim() || isProcessing)
return;
setHasStartedChat(true);
const userMessage = {
type: 'user',
content: input,
session_id: sessionId || 'new',
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setIsProcessing(true);
try {
let agent = currentAgent;
if (!agent) {
console.log('🔧 Server URL:', serverUrl);
agent = new StreamingMuonAgent({
serverUrl,
agentType: agentType,
projectPath,
apiKey,
accessToken,
nlstepMode,
auth,
});
setCurrentAgent(agent);
}
const messageGenerator = agent.query(input);
for await (const message of messageGenerator) {
addMessage(message);
}
}
catch (error) {
addMessage({
type: 'error',
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
session_id: sessionId,
timestamp: new Date(),
});
}
finally {
setIsProcessing(false);
}
}, [
currentAgent,
sessionId,
isProcessing,
addMessage,
serverUrl,
agentType,
projectPath,
apiKey,
accessToken,
auth,
nlstepMode,
]);
const handleInterrupt = useCallback(() => {
if (currentAgent && isProcessing) {
currentAgent.interrupt();
// Clean up streaming state
if (streamingTimeoutRef.current) {
clearTimeout(streamingTimeoutRef.current);
streamingTimeoutRef.current = null;
}
// Clear streaming content and stop processing
streamingBuffer.current = '';
setStreamingContent('');
setIsStreaming(false);
setIsProcessing(false);
// Add interruption message
addMessage({
type: 'system',
content: '⚠️ Chat interrupted. You can continue the conversation.',
session_id: sessionId,
timestamp: new Date(),
});
}
}, [currentAgent, isProcessing, sessionId, addMessage]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (streamingTimeoutRef.current) {
clearTimeout(streamingTimeoutRef.current);
}
};
}, []);
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, children: !hasStartedChat ? (_jsxs(Box, { flexDirection: "column", overflow: "hidden", children: [_jsx(WelcomePanel, { projectPath: projectPath }), _jsx(TipsPanel, {})] })) : (_jsx(Box, { flexGrow: 1, minHeight: 0, children: _jsx(ChatContainer, { messages: messages, showTimestamps: false, isProcessing: isStreaming, streamingContent: streamingContent, showToolResults: showToolResults }) })) }), _jsx(Box, { flexShrink: 0, children: _jsx(InputBox, { placeholder: hasStartedChat
? isProcessing
? 'Processing... (ESC to interrupt)'
: 'Type your message...'
: 'Try "fix failing test in auth.spec.ts"', onSubmit: handleSubmit, isProcessing: isProcessing }) }), _jsx(Box, { flexShrink: 0, children: _jsx(StatusBar, { error: undefined, showToolResults: showToolResults, isProcessing: isStreaming }) })] }));
};