UNPKG

@michaelnkomo/cli

Version:

BroCode CLI - AI coding assistant with @ file tagging and multi-language support

205 lines (204 loc) 9.96 kB
/** * BroCode Main Application */ import React, { useState, useEffect } from 'react'; import { Box, Text, useInput, useApp } from 'ink'; import TextInput from 'ink-text-input'; import { version, Agent, ConfigManager, Logger } from '@michaelnkomo/core'; import { simpleFileTagging as fileTagging, simpleAutocompleteManager as autocompleteManager, detectFileTagging, renderAutocompletePopup } from './utils/simple-file-tagging.js'; const App = ({ args: _args }) => { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [agent, setAgent] = useState(null); const [input, setInput] = useState(''); const [messages, setMessages] = useState([]); const [isProcessing, setIsProcessing] = useState(false); const [showAutocomplete, setShowAutocomplete] = useState(false); const [autocompleteResults, setAutocompleteResults] = useState([]); const [selectedAutocompleteIndex, setSelectedAutocompleteIndex] = useState(0); const [cursorPosition, setCursorPosition] = useState(0); const { exit } = useApp(); // Initialize config and agent useEffect(() => { const init = async () => { try { // Load configuration const configManager = await ConfigManager.load(); const config = configManager.getAll(); // Validate API key if (!config.apiKey || config.apiKey === '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') { setError('NVIDIA API key not configured.\n\n' + 'Please set NVIDIA_API_KEY in your environment or create ~/.brocode/config.json\n\n' + 'Get your API key at: https://build.nvidia.com/meta/llama-3_1-70b-instruct'); setIsLoading(false); return; } // Create logger const logger = new Logger({ verbose: config.verbose, debug: config.debug, logToFile: true, component: 'cli', }); // Create agent config const agentConfig = { apiKey: config.apiKey, apiUrl: config.apiUrl, model: config.model, temperature: config.temperature, top_p: config.top_p, max_tokens: config.max_tokens, }; // Initialize agent const agentInstance = new Agent(agentConfig, logger); setAgent(agentInstance); setIsLoading(false); // Show welcome message setMessages([ { role: 'system', content: `Welcome to BroCode v${version}! 🚀\n\nI'm your AI coding companion. I can:\n• Create complete projects (FastAPI, Express, React, Django, Go, Rust, etc.)\n• Write and edit code files\n• Execute file operations\n• And much more!\n\nType your request or 'exit' to quit.`, }, ]); } catch (err) { setError(`Failed to initialize: ${err.message}`); setIsLoading(false); } }; init(); }, []); // Handle input changes for autocomplete const handleInputChange = async (value) => { setInput(value); // Check if user is typing file tags if (detectFileTagging(value)) { const autocompleteState = await autocompleteManager.handleInput(value, value.length); setShowAutocomplete(autocompleteState.shouldShowAutocomplete); setAutocompleteResults(autocompleteState.results); setSelectedAutocompleteIndex(autocompleteState.selectedIndex); } else { setShowAutocomplete(false); setAutocompleteResults([]); } }; useInput((input, key) => { if (key.escape || (key.ctrl && input === 'c')) { if (showAutocomplete) { // Hide autocomplete first setShowAutocomplete(false); return; } exit(); } // Handle autocomplete navigation if (showAutocomplete) { if (key.upArrow) { autocompleteManager.navigateResults('up'); setSelectedAutocompleteIndex(autocompleteManager.getState().selectedIndex); return; } if (key.downArrow) { autocompleteManager.navigateResults('down'); setSelectedAutocompleteIndex(autocompleteManager.getState().selectedIndex); return; } if (key.return) { const result = autocompleteManager.selectResult(input, cursorPosition); if (result) { setInput(result.text); setCursorPosition(result.cursorPosition); } setShowAutocomplete(false); return; } } }); // Handle message submission with file context const handleSubmit = async (value) => { if (!value.trim() || !agent || isProcessing) return; // Hide autocomplete when submitting setShowAutocomplete(false); const userMessage = value.trim(); setInput(''); // Check for exit command if (userMessage.toLowerCase() === 'exit' || userMessage.toLowerCase() === 'quit') { exit(); return; } // Parse file tags and build context const { cleanInput, fileTags } = fileTagging.parseFileTags(userMessage); let contextualInput = cleanInput; if (fileTags.length > 0) { const fileContext = await fileTagging.buildFileContext(fileTags); contextualInput = fileContext + '\n\n**USER REQUEST:**\n' + cleanInput; } // Add user message to display (show original input) setMessages((prev) => [...prev, { role: 'user', content: userMessage }]); setIsProcessing(true); try { // Send contextual input (with file contents) to the agent const response = await agent.chat(contextualInput); // Add AI response to display setMessages((prev) => [...prev, { role: 'assistant', content: response }]); } catch (err) { setMessages((prev) => [ ...prev, { role: 'error', content: `Error: ${err.message}` }, ]); } finally { setIsProcessing(false); } }; // Render loading state if (isLoading) { return (React.createElement(Box, { flexDirection: "column", padding: 1 }, React.createElement(Text, { bold: true, color: "cyan" }, "BroCode v", version), React.createElement(Text, { color: "yellow" }, "Initializing..."))); } // Render error state if (error) { return (React.createElement(Box, { flexDirection: "column", padding: 1 }, React.createElement(Text, { bold: true, color: "cyan" }, "BroCode v", version), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: "red" }, error)))); } // Render main chat interface return (React.createElement(Box, { flexDirection: "column", padding: 1 }, React.createElement(Text, { bold: true, color: "cyan" }, "BroCode v", version), React.createElement(Text, { color: "gray", dimColor: true }, "Press ESC or Ctrl+C to exit"), React.createElement(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, messages.map((msg, idx) => (React.createElement(Box, { key: idx, flexDirection: "column", marginBottom: 1 }, msg.role === 'system' && (React.createElement(Box, null, React.createElement(Text, { color: "gray" }, msg.content))), msg.role === 'user' && (React.createElement(Box, null, React.createElement(Text, { bold: true, color: "green" }, "You:"), React.createElement(Text, null, " ", msg.content))), msg.role === 'assistant' && (React.createElement(Box, null, React.createElement(Text, { bold: true, color: "cyan" }, "BroCode:"), React.createElement(Text, null, " ", msg.content))), msg.role === 'error' && (React.createElement(Box, null, React.createElement(Text, { color: "red" }, msg.content))))))), React.createElement(Box, { flexDirection: "column" }, isProcessing ? (React.createElement(Text, { color: "yellow" }, " BroCode is thinking...")) : (React.createElement(React.Fragment, null, React.createElement(Text, { color: "cyan" }, "Type your message (use @ to tag files, Ctrl+C to exit):"), showAutocomplete && autocompleteResults.length > 0 && (React.createElement(Box, { marginY: 1 }, React.createElement(Text, { color: "yellow" }, renderAutocompletePopup(autocompleteResults, selectedAutocompleteIndex)))), React.createElement(Box, null, React.createElement(Text, { bold: true, color: "green" }, "You: "), React.createElement(TextInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, placeholder: "What would you like me to help you with? Use @filename to include files" })), detectFileTagging(input) && !showAutocomplete && (React.createElement(Text, { color: "gray", dimColor: true }, "\uD83D\uDCA1 Tip: Use @ to tag files for context (e.g., @src/App.tsx)"))))))); }; export default App;