""" Enhanced CLI for LangCode Agent Provides modern terminal UI with colors, history, and autocomplete """ import sys import os from typing import Optional, List, Dict, Any from datetime import datetime # prompt_toolkit imports from prompt_toolkit import PromptSession, HTML from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.styles import Style from prompt_toolkit.completion import WordCompleter, NestedCompleter from prompt_toolkit.history import FileHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.formatted_text import FormattedText # Color palette class Colors: """ANSI color codes for terminal output""" # Styles BOLD = "\033[1m" DIM = "\033[2m" ITALIC = "\033[3m" RESET = "\033[0m" # Colors BLACK = "\033[30m" RED = "\033[31m" GREEN = "\033[32m" YELLOW = "\033[33m" BLUE = "\033[34m" MAGENTA = "\033[35m" CYAN = "\033[36m" WHITE = "\033[37m" # Background BG_BLACK = "\033[40m" BG_BLUE = "\033[44m" BG_CYAN = "\033[46m" # Bright colors BRIGHT_BLACK = "\033[90m" BRIGHT_RED = "\033[91m" BRIGHT_GREEN = "\033[92m" BRIGHT_YELLOW = "\033[93m" BRIGHT_BLUE = "\033[94m" BRIGHT_MAGENTA = "\033[95m" BRIGHT_CYAN = "\033[96m" BRIGHT_WHITE = "\033[97m" class Theme: """Color themes for the CLI""" DEFAULT = { "user": "ansicyan", # Cyan for user input "agent": "ansigreen", # Green for agent response "system": "ansiyellow", # Yellow for system messages "error": "ansired", # Red for errors "info": "ansiblue", # Blue for info "muted": "ansiblack", # Black for muted text "prompt": "ansimagenta", # Magenta for prompt } DARK = { "user": "cyan", "agent": "lightgreen", "system": "yellow", "error": "lightred", "info": "lightblue", "muted": "gray", "prompt": "magenta", } LIGHT = { "user": "blue", "agent": "darkgreen", "system": "darkyellow", "error": "darkred", "info": "darkblue", "muted": "gray", "prompt": "darkmagenta", } def create_style(theme_name: str = "default") -> Style: """Create prompt_toolkit style from theme""" theme = getattr(Theme, theme_name.upper(), Theme.DEFAULT) style_dict = { "user": theme["user"], "agent": theme["agent"], "system": theme["system"], "error": theme["error"], "info": theme["info"], "muted": theme["muted"], "prompt": theme["prompt"], } return Style.from_dict(style_dict) class EnhancedCLI: """ Enhanced Command Line Interface for LangCode Agent. Features: - Colored input/output - Command history (persistent) - Autocomplete for commands - Multi-line input support - Themes (light/dark) """ def __init__( self, history_file: str = "./.langcode/.cli_history", theme: str = "default", welcome_message: Optional[str] = None ): """ Initialize Enhanced CLI. Args: history_file: Path to persist command history theme: Color theme (default, dark, light) welcome_message: Initial message to show """ self.history_file = history_file self.theme = theme self.welcome_message = welcome_message or self._default_welcome() # Ensure history directory exists os.makedirs(os.path.dirname(history_file) or ".", exist_ok=True) # Command definitions self.commands = { "/help": "Mostra aiuto", "/status": "Stato sistema", "/clear": "Pulisci schermo", "/theme": "Cambia tema (default/dark/light)", "/history": "Mostra cronologia", "/exit": "Esci", "/quit": "Esci", } # Create completer for autocomplete self.completer = NestedCompleter.from_nested_dict({ cmd: None for cmd in self.commands.keys() }) # Create key bindings self.key_bindings = self._create_key_bindings() # Create prompt session self.session = self._create_session() # Current theme name self.current_theme = theme # Output welcome message msg = welcome_message if welcome_message else self._default_welcome() self._print_system(msg) def _default_welcome(self) -> str: """Generate default welcome message""" return f""" {Colors.CYAN}{Colors.BOLD}╔═══════════════════════════════════════════════════════════╗{Colors.RESET} ║ {Colors.BRIGHT_GREEN}{Colors.BOLD}LangCode Agent - Enhanced CLI{Colors.RESET} ║ ║ ║ ║ {Colors.YELLOW}Comandi:{Colors.RESET} ║ ║ {Colors.CYAN}/help{Colors.RESET} - Mostra aiuto {Colors.CYAN}/clear{Colors.RESET} - Pulisci schermo ║ ║ {Colors.CYAN}/status{Colors.RESET} - Stato sistema {Colors.CYAN}/theme{Colors.RESET} - Cambia tema ║ ║ {Colors.CYAN}/exit{Colors.RESET} - Esci ║ ║ ║ ╚═══════════════════════════════════════════════════════════════════╝{Colors.RESET} """ def _create_key_bindings(self) -> KeyBindings: """Create custom key bindings""" kb = KeyBindings() @kb.add("c-c") def _(event): """Handle Ctrl+C - cancel input""" event.app.exit(exception=KeyboardInterrupt) return kb def _create_session(self) -> PromptSession: """Create prompt session with history and autocomplete""" return PromptSession( history=FileHistory(self.history_file), completer=self.completer, key_bindings=self.key_bindings, auto_suggest=AutoSuggestFromHistory(), multiline=True, ) def _get_bottom_bar(self, cli) -> List: """Bottom toolbar showing theme and shortcuts""" return [ ("class:muted", f" Theme: {self.current_theme} "), ("class:muted", " | "), ("class:muted", "Ctrl+C: exit | Tab: autocomplete | ↑↓: history "), ] def _create_prompt_tokens(self) -> FormattedText: """Create prompt tokens with colors""" timestamp = datetime.now().strftime("%H:%M") return FormattedText([ ("class:prompt", f"❯ "), ("class:user", f"TUO "), ("class:muted", f"[{timestamp}] "), ]) def _print_system(self, message: str) -> None: """Print system message (in yellow)""" print(f"{Colors.YELLOW}{message}{Colors.RESET}") def _print_user(self, message: str) -> None: """Print user input (in cyan)""" print(f"{Colors.CYAN}👤 TUO:{Colors.RESET} {message}") def _print_agent(self, message: str) -> None: """Print agent response (in green)""" # Handle multi-line messages lines = message.split("\n") for i, line in enumerate(lines): if i == 0: print(f"{Colors.GREEN}🤖 AGENTE:{Colors.RESET} {line}") else: print(f"{Colors.GREEN} {Colors.RESET} {line}") def _print_error(self, message: str) -> None: """Print error message (in red)""" print(f"{Colors.RED}❌ ERRORE:{Colors.RESET} {message}") def _print_info(self, message: str) -> None: """Print info message (in blue)""" print(f"{Colors.BLUE}ℹ️ INFO:{Colors.RESET} {message}") def _handle_command(self, command: str) -> Optional[bool]: """ Handle special commands (/command). Returns: True if command was handled and we should exit False if command was handled but we should continue None if command was not recognized """ cmd_lower = command.lower().strip() if cmd_lower == "/help": self._print_system(""" ╔═══════════════════════════════════════════════════════════════╗ ║ COMANDI DISPONIBILI ║ ╠═══════════════════════════════════════════════════════════════╣ ║ /help - Mostra questo messaggio ║ ║ /status - Mostra stato del sistema ║ ║ /clear - Pulisci lo schermo ║ ║ /theme - Cicla tra i temi (default/dark/light) ║ ║ /history - Mostra cronologia comandi ║ ║ /exit - Esci dall'agente ║ ║ ║ ║ Per messaggi normali, scrivi direttamente la tua domanda. ║ ╚═══════════════════════════════════════════════════════════════╝ """) return None elif cmd_lower == "/status": self._print_info(f"Theme: {self.current_theme}") self._print_info(f"History file: {self.history_file}") return None elif cmd_lower == "/clear": os.system("clear" if os.name != "nt" else "cls") return None elif cmd_lower == "/theme": themes = ["default", "dark", "light"] current_idx = themes.index(self.current_theme) if self.current_theme in themes else 0 next_theme = themes[(current_idx + 1) % len(themes)] self.current_theme = next_theme self._print_info(f"Tema cambiato a: {next_theme}") return None elif cmd_lower in ["/exit", "/quit"]: return True elif cmd_lower == "/history": try: with open(self.history_file, "r") as f: lines = f.readlines() if lines: self._print_system(f"Ultimi {min(10, len(lines))} comandi:") for i, line in enumerate(lines[-10:], 1): print(f" {i}. {line.strip()}") else: self._print_info("Nessuna cronologia") except FileNotFoundError: self._print_info("Nessuna cronologia") return None return None # Command not recognized def prompt(self) -> str: """Get input from user with enhanced UI""" try: text = self.session.prompt( self._create_prompt_tokens(), style=create_style(self.current_theme) ) return text except KeyboardInterrupt: return "/exit" except EOFError: return "/exit" def run(self, agent_callback): """ Run the interactive CLI loop. Args: agent_callback: Function that takes user input and returns agent response """ self._print_system(self.welcome_message) while True: try: # Get user input user_input = self.prompt().strip() if not user_input: continue # Show what user typed self._print_user(user_input) # Check for commands if user_input.startswith("/"): should_exit = self._handle_command(user_input) if should_exit: break continue # Call agent and show response try: response = agent_callback(user_input) self._print_agent(response) except Exception as e: self._print_error(f"Errore: {str(e)}") except KeyboardInterrupt: self._print_system("\n👋 Arrivederci!") break except EOFError: break self._print_system("Sessione terminata.") # Factory function for easy use def create_enhanced_cli( history_file: str = "./.langcode/.cli_history", theme: str = "default" ) -> EnhancedCLI: """Create an EnhancedCLI instance""" return EnhancedCLI(history_file=history_file, theme=theme) if __name__ == "__main__": # Test the CLI def test_callback(text): return f"Hai detto: {text}" cli = create_enhanced_cli() cli.run(test_callback)