/** * 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, type AgentConfig } from '@michaelnkomo/core'; import { simpleFileTagging as fileTagging, simpleAutocompleteManager as autocompleteManager, detectFileTagging, renderAutocompletePopup } from './utils/simple-file-tagging.js'; interface AppProps { args: string[]; } const App: React.FC = ({ 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: 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 as Error).message}`); setIsLoading(false); } }; init(); }, []); // Handle input changes for autocomplete const handleInputChange = async (value: string) => { 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: string) => { 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 as Error).message}` }, ]); } finally { setIsProcessing(false); } }; // Render loading state if (isLoading) { return ( BroCode v{version} Initializing... ); } // Render error state if (error) { return ( BroCode v{version} {error} ); } // Render main chat interface return ( BroCode v{version} Press ESC or Ctrl+C to exit {/* Message history */} {messages.map((msg, idx) => ( {msg.role === 'system' && ( {msg.content} )} {msg.role === 'user' && ( You: {msg.content} )} {msg.role === 'assistant' && ( BroCode: {msg.content} )} {msg.role === 'error' && ( {msg.content} )} ))} {/* Input field */} {isProcessing ? ( BroCode is thinking... ) : ( <> Type your message (use @ to tag files, Ctrl+C to exit): {/* Autocomplete popup */} {showAutocomplete && autocompleteResults.length > 0 && ( {renderAutocompletePopup(autocompleteResults, selectedAutocompleteIndex)} )} You: {/* File tag hint */} {detectFileTagging(input) && !showAutocomplete && ( 💡 Tip: Use @ to tag files for context (e.g., @src/App.tsx) )} )} ); }; export default App;