@michaelnkomo/cli
Version:
BroCode CLI - AI coding assistant with @ file tagging and multi-language support
205 lines (204 loc) • 9.96 kB
JavaScript
/**
* 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;