""" Text User Interface for MaxClaw Agent configuration management """ import curses import sys from typing import Optional, List, Dict, Any from config_manager import config_manager def draw_menu(stdscr, selected_row_idx, options): """Draw a menu with options""" stdscr.clear() h, w = stdscr.getmaxyx() # Title title = "MaxClaw Agent Configuration TUI" stdscr.attron(curses.A_BOLD) stdscr.addstr(0, (w - len(title)) // 2, title) stdscr.attroff(curses.A_BOLD) # Instructions instructions = "Use arrow keys to navigate, Enter to select, 'q' to quit" stdscr.addstr(2, (w - len(instructions)) // 2, instructions) # Menu options for idx, option in enumerate(options): x = w // 2 - len(option) // 2 y = h // 2 - len(options) // 2 + idx if idx == selected_row_idx: stdscr.attron(curses.A_REVERSE) stdscr.addstr(y, x, option) stdscr.attroff(curses.A_REVERSE) else: stdscr.addstr(y, x, option) stdscr.refresh() def get_string_input(stdscr, prompt, y, x, max_length=50): """Get string input from user""" curses.echo() stdscr.addstr(y, x, prompt) stdscr.refresh() input_win = curses.newwin(1, max_length, y, x + len(prompt)) input_win.box() input_win.refresh() curses.curs_set(1) result = input_win.getstr(0, 0, max_length).decode('utf-8') curses.curs_set(0) curses.noecho() return result def get_password_input(stdscr, prompt, y, x, max_length=50): """Get password input from user (hidden)""" curses.noecho() stdscr.addstr(y, x, prompt) stdscr.refresh() input_win = curses.newwin(1, max_length, y, x + len(prompt)) input_win.box() input_win.refresh() curses.curs_set(1) result = "" while True: ch = input_win.getch() if ch in [curses.KEY_ENTER, 10, 13]: # Enter key break elif ch in [curses.KEY_BACKSPACE, 127, 8]: # Backspace if len(result) > 0: result = result[:-1] input_win.addstr(0, len(result), " ") input_win.move(0, len(result)) elif ch >= 32 and ch <= 126 and len(result) < max_length: # Printable characters result += chr(ch) input_win.addstr(0, len(result)-1, "*") input_win.refresh() curses.curs_set(0) return result def show_message(stdscr, message, wait_for_key=True): """Show a message to the user""" h, w = stdscr.getmaxyx() stdscr.addstr(h//2, (w - len(message)) // 2, message) stdscr.refresh() if wait_for_key: stdscr.getch() def main_tui(stdscr): """Main TUI function""" # Initialize colors if available if curses.has_colors(): curses.start_color() curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) # Main menu options main_options = [ "View Configurations", "Add Configuration", "Remove Configuration", "Set Active Configuration", "View Active Configuration", "Exit" ] current_selection = 0 while True: draw_menu(stdscr, current_selection, main_options) key = stdscr.getch() if key == curses.KEY_UP and current_selection > 0: current_selection -= 1 elif key == curses.KEY_DOWN and current_selection < len(main_options) - 1: current_selection += 1 elif key == curses.KEY_ENTER or key in [10, 13]: # Enter key pressed if current_selection == 0: # View Configurations view_configurations_tui(stdscr) elif current_selection == 1: # Add Configuration add_configuration_tui(stdscr) elif current_selection == 2: # Remove Configuration remove_configuration_tui(stdscr) elif current_selection == 3: # Set Active Configuration set_active_configuration_tui(stdscr) elif current_selection == 4: # View Active Configuration view_active_configuration_tui(stdscr) elif current_selection == 5: # Exit break elif key == ord('q') or key == ord('Q'): break def view_configurations_tui(stdscr): """View all configurations in TUI""" configs = config_manager.get_configurations() active = config_manager.get_active_configuration() stdscr.clear() h, w = stdscr.getmaxyx() # Title title = "Configurations" stdscr.attron(curses.A_BOLD) stdscr.addstr(0, (w - len(title)) // 2, title) stdscr.attroff(curses.A_BOLD) if not configs: stdscr.addstr(2, 0, "No configurations found.") else: # Header header = f"{'Name':<20} {'Provider':<12} {'Model':<20} {'Active'}" stdscr.addstr(2, 0, header) stdscr.addstr(3, 0, "-" * len(header)) # Configurations for idx, (name, config) in enumerate(configs.items()): y = 4 + idx if y >= h - 2: # Prevent overflow break marker = "*" if name == active else " " provider = config.get("provider", "unknown")[:11] model = config.get("model_name", "unknown")[:19] line = f"{marker} {name:<19} {provider:<12} {model:<20}" stdscr.addstr(y, 0, line) # Instructions instructions = "Press any key to return to main menu" stdscr.addstr(h-2, (w - len(instructions)) // 2, instructions) stdscr.refresh() stdscr.getch() def add_configuration_tui(stdscr): """Add a new configuration via TUI""" stdscr.clear() h, w = stdscr.getmaxyx() # Title title = "Add New Configuration" stdscr.attron(curses.A_BOLD) stdscr.addstr(0, (w - len(title)) // 2, title) stdscr.attroff(curses.A_BOLD) # Get configuration name name = get_string_input(stdscr, "Configuration name: ", 2, 0) if not name: show_message(stdscr, "Error: Configuration name cannot be empty") return if config_manager.configuration_exists(name): overwrite_prompt = f"Configuration '{name}' already exists. Overwrite? (y/n): " overwrite = get_string_input(stdscr, overwrite_prompt, 3, 0, 1) if overwrite.lower() != 'y': show_message(stdscr, "Cancelled.") return # Select provider providers = ["OpenAI", "Ollama", "OpenRouter"] provider_selection = 0 while True: stdscr.clear() stdscr.attron(curses.A_BOLD) stdscr.addstr(0, (w - len(title)) // 2, title) stdscr.attroff(curses.A_BOLD) stdscr.addstr(2, 0, f"Name: {name}") stdscr.addstr(3, 0, "Select provider:") for idx, provider in enumerate(providers): y = 4 + idx if idx == provider_selection: stdscr.attron(curses.A_REVERSE) stdscr.addstr(y, 2, f"> {provider}") stdscr.attroff(curses.A_REVERSE) else: stdscr.addstr(y, 2, f" {provider}") stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm") stdscr.refresh() key = stdscr.getch() if key == curses.KEY_UP and provider_selection > 0: provider_selection -= 1 elif key == curses.KEY_DOWN and provider_selection < len(providers) - 1: provider_selection += 1 elif key == curses.KEY_ENTER or key in [10, 13]: break provider_map = {"OpenAI": "openai", "Ollama": "ollama", "OpenRouter": "openrouter"} provider = provider_map[providers[provider_selection]] config = {"provider": provider} # Get model name model_name = get_string_input(stdscr, f"Model name for {provider} (e.g., gpt-4o, gemma2:9b): ", 5, 0) if not model_name: show_message(stdscr, "Error: Model name cannot be empty") return config["model_name"] = model_name # Get provider-specific settings y_pos = 6 if provider == "openai": api_key = get_password_input(stdscr, "OpenAI API key: ", y_pos, 0) config["api_key"] = api_key y_pos += 1 elif provider == "ollama": base_url = get_string_input(stdscr, f"Ollama base URL (default: http://localhost:11434): ", y_pos, 0) if not base_url: base_url = "http://localhost:11434" config["base_url"] = base_url y_pos += 1 elif provider == "openrouter": api_key = get_password_input(stdscr, "OpenRouter API key: ", y_pos, 0) config["api_key"] = api_key y_pos += 1 base_url = get_string_input(stdscr, f"OpenRouter base URL (default: https://openrouter.ai/api/v1): ", y_pos, 0) if not base_url: base_url = "https://openrouter.ai/api/v1" config["base_url"] = base_url y_pos += 1 # Ask if this should be the active configuration set_active_prompt = "Set as active configuration? (y/n): " set_active = get_string_input(stdscr, set_active_prompt, y_pos, 0, 1) # Save the configuration try: config_manager.add_configuration(name, config) message = f"Configuration '{name}' saved successfully." if set_active.lower() == 'y': config_manager.set_active_configuration(name) message += f" Configuration '{name}' set as active." show_message(stdscr, message) except Exception as e: show_message(stdscr, f"Error saving configuration: {e}") def remove_configuration_tui(stdscr): """Remove a configuration via TUI""" configs = config_manager.get_configurations() if not configs: show_message(stdscr, "No configurations to remove.") return stdscr.clear() h, w = stdscr.getmaxyx() # Title title = "Remove Configuration" stdscr.attron(curses.A_BOLD) stdscr.addstr(0, (w - len(title)) // 2, title) stdscr.attroff(curses.A_BOLD) # List configurations config_names = list(configs.keys()) selection = 0 while True: stdscr.clear() stdscr.attron(curses.A_BOLD) stdscr.addstr(0, (w - len(title)) // 2, title) stdscr.attroff(curses.A_BOLD) stdscr.addstr(2, 0, "Select configuration to remove:") for idx, name in enumerate(config_names): y = 3 + idx if y >= h - 4: # Prevent overflow break active_marker = " *" if name == config_manager.get_active_configuration(): active_marker = " (active)" if idx == selection: stdscr.attron(curses.A_REVERSE) stdscr.addstr(y, 2, f"> {name}{active_marker}") stdscr.attroff(curses.A_REVERSE) else: stdscr.addstr(y, 2, f" {name}{active_marker}") stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm, 'c' to cancel") stdscr.refresh() key = stdscr.getch() if key == curses.KEY_UP and selection > 0: selection -= 1 elif key == curses.KEY_DOWN and selection < len(config_names) - 1: selection += 1 elif key == curses.KEY_ENTER or key in [10, 13]: # Confirm removal name_to_remove = config_names[selection] active = config_manager.get_active_configuration() if name_to_remove == active: confirm_prompt = f"'{name_to_remove}' is the active configuration. Remove it anyway? (y/n): " confirm = get_string_input(stdscr, confirm_prompt, h-3, 0, 1) if confirm.lower() != 'y': show_message(stdscr, "Cancelled.") return confirm_prompt = f"Are you sure you want to remove configuration '{name_to_remove}'? (y/n): " confirm = get_string_input(stdscr, confirm_prompt, h-3, 0, 1) if confirm.lower() == 'y': if config_manager.remove_configuration(name_to_remove): show_message(stdscr, f"Configuration '{name_to_remove}' removed successfully.") if name_to_remove == active: show_message(stdscr, "Active configuration has been unset.") else: show_message(stdscr, f"Error: Failed to remove configuration '{name_to_remove}'.") else: show_message(stdscr, "Cancelled.") return elif key == ord('c') or key == ord('C'): show_message(stdscr, "Cancelled.") return def set_active_configuration_tui(stdscr): """Set active configuration via TUI""" configs = config_manager.get_configurations() if not configs: show_message(stdscr, "No configurations available.") return stdscr.clear() h, w = stdscr.getmaxyx() # Title title = "Set Active Configuration" stdscr.attron(curses.A_BOLD) stdscr.addstr(0, (w - len(title)) // 2, title) stdscr.attroff(curses.A_BOLD) # List configurations config_names = list(configs.keys()) selection = 0 while True: stdscr.clear() stdscr.attron(curses.A_BOLD) stdscr.addstr(0, (w - len(title)) // 2, title) stdscr.attroff(curses.A_BOLD) stdscr.addstr(2, 0, "Select configuration to set as active:") for idx, name in enumerate(config_names): y = 3 + idx if y >= h - 4: # Prevent overflow break current_active = config_manager.get_active_configuration() active_marker = " (current)" if name == current_active else "" if idx == selection: stdscr.attron(curses.A_REVERSE) stdscr.addstr(y, 2, f"> {name}{active_marker}") stdscr.attroff(curses.A_REVERSE) else: stdscr.addstr(y, 2, f" {name}{active_marker}") stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm, 'c' to cancel") stdscr.refresh() key = stdscr.getch() if key == curses.KEY_UP and selection > 0: selection -= 1 elif key == curses.KEY_DOWN and selection < len(config_names) - 1: selection += 1 elif key == curses.KEY_ENTER or key in [10, 13]: # Set as active name_to_set = config_names[selection] if config_manager.set_active_configuration(name_to_set): show_message(stdscr, f"Configuration '{name_to_set}' set as active.") else: show_message(stdscr, f"Error: Failed to set configuration '{name_to_set}' as active.") return elif key == ord('c') or key == ord('C'): show_message(stdscr, "Cancelled.") return def view_active_configuration_tui(stdscr): """View active configuration via TUI""" active_name = config_manager.get_active_configuration() if not active_name: show_message(stdscr, "No active configuration set.") return config = config_manager.get_configuration(active_name) if not config: show_message(stdscr, f"Error: Active configuration '{active_name}' not found.") return stdscr.clear() h, w = stdscr.getmaxyx() # Title title = "Active Configuration" stdscr.attron(curses.A_BOLD) stdscr.addstr(0, (w - len(title)) // 2, title) stdscr.attroff(curses.A_BOLD) # Display configuration details details = [ f"Name: {active_name}", f"Provider: {config.get('provider', 'unknown')}", f"Model: {config.get('model_name', 'unknown')}" ] # Add provider-specific details provider = config.get('provider') if provider == "openai": details.append(f"API Key: {'Set' if config.get('api_key') else 'Not set'}") elif provider == "ollama": base_url = config.get('base_url', 'http://localhost:11434') details.append(f"Base URL: {base_url}") elif provider == "openrouter": details.append(f"API Key: {'Set' if config.get('api_key') else 'Not set'}") base_url = config.get('base_url', 'https://openrouter.ai/api/v1') details.append(f"Base URL: {base_url}") # Display details for idx, detail in enumerate(details): y = 2 + idx if y >= h - 2: # Prevent overflow break stdscr.addstr(y, 0, detail) # Instructions instructions = "Press any key to return to main menu" stdscr.addstr(h-2, (w - len(instructions)) // 2, instructions) stdscr.refresh() stdscr.getch() def main(): """Main entry point for the TUI""" curses.wrapper(main_tui) if __name__ == "__main__": main()