Files
langcode_agent/enhanced_cli.py
T
opencode 4a0342dcb7 feat: aggiungi EnhancedCLI con prompt_toolkit
- Nuovo modulo enhanced_cli.py con UI moderna
- Colors: user (cyan), agent (green), system (yellow), error (red)
- History persistente in .langcode/.cli_history
- Autocomplete comandi (/help, /status, /clear, /theme, /exit)
- Supporto temi: default, dark, light
- Multiline input
- Integrato in langcode.py chat mode
2026-03-24 10:15:51 +01:00

382 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)