UNPKG

@autifyhq/muon

Version:

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

178 lines (177 loc) 7.5 kB
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 }) })] })); };