Phase 2: Enhanced TUI/CLI with search, export/import, and custom tools for Proxmox and Ollama

This commit is contained in:
truetype committed 2026-03-20 16:08:26 +01:00
1 parent f5026965f7
commit 9f3298fa13
7 files changed
+1353 -16

No files matched your search

+289 -10
View File
@@ -1,19 +1,22 @@
"""
Text User Interface for MaxClaw Agent configuration management
Enhanced with search/filter, export/import, and connection testing
"""
import curses
import sys
import json
import yaml
from pathlib import Path
from typing import Optional, List, Dict, Any
from config_manager import config_manager
def draw_menu(stdscr, selected_row_idx, options):
def draw_menu(stdscr, selected_row_idx, options, title="MaxClaw Agent Configuration TUI"):
"""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)
@@ -87,20 +90,61 @@ def show_message(stdscr, message, wait_for_key=True):
stdscr.getch()
def filter_configurations(configs: Dict[str, Any], search_term: str) -> Dict[str, Any]:
"""Filter configurations based on search term"""
if not search_term:
return configs
search_term = search_term.lower()
filtered = {}
for name, config in configs.items():
# Search in name
if search_term in name.lower():
filtered[name] = config
continue
# Search in provider
provider = config.get("provider", "").lower()
if search_term in provider:
filtered[name] = config
continue
# Search in model name
model_name = config.get("model_name", "").lower()
if search_term in model_name:
filtered[name] = config
continue
# Search in base URL
base_url = config.get("base_url", "").lower()
if search_term in base_url:
filtered[name] = config
continue
return filtered
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)
curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
# Main menu options
main_options = [
"View Configurations",
"Search Configurations",
"Add Configuration",
"Remove Configuration",
"Set Active Configuration",
"View Active Configuration",
"Test Configuration",
"Export Configurations",
"Import Configurations",
"Exit"
]
@@ -119,30 +163,38 @@ def main_tui(stdscr):
# Enter key pressed
if current_selection == 0: # View Configurations
view_configurations_tui(stdscr)
elif current_selection == 1: # Add Configuration
elif current_selection == 1: # Search Configurations
search_configurations_tui(stdscr)
elif current_selection == 2: # Add Configuration
add_configuration_tui(stdscr)
elif current_selection == 2: # Remove Configuration
elif current_selection == 3: # Remove Configuration
remove_configuration_tui(stdscr)
elif current_selection == 3: # Set Active Configuration
elif current_selection == 4: # Set Active Configuration
set_active_configuration_tui(stdscr)
elif current_selection == 4: # View Active Configuration
elif current_selection == 5: # View Active Configuration
view_active_configuration_tui(stdscr)
elif current_selection == 5: # Exit
elif current_selection == 6: # Test Configuration
test_configuration_tui(stdscr)
elif current_selection == 7: # Export Configurations
export_configurations_tui(stdscr)
elif current_selection == 8: # Import Configurations
import_configurations_tui(stdscr)
elif current_selection == 9: # Exit
break
elif key == ord('q') or key == ord('Q'):
break
def view_configurations_tui(stdscr):
def view_configurations_tui(stdscr, configs=None, title="Configurations"):
"""View all configurations in TUI"""
configs = config_manager.get_configurations()
if configs is None:
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)
@@ -174,6 +226,35 @@ def view_configurations_tui(stdscr):
stdscr.getch()
def search_configurations_tui(stdscr):
"""Search/filter configurations"""
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "Search Configurations"
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
# Get search term
search_term = get_string_input(stdscr, "Search term (name, provider, model, URL): ", 2, 0, 50)
if not search_term:
show_message(stdscr, "Search cancelled.")
return
# Get all configurations and filter
all_configs = config_manager.get_configurations()
filtered_configs = filter_configurations(all_configs, search_term)
# Show filtered results
if not filtered_configs:
show_message(stdscr, f"No configurations found matching '{search_term}'.")
else:
view_configurations_tui(stdscr, filtered_configs, f"Search Results for '{search_term}'")
def add_configuration_tui(stdscr):
"""Add a new configuration via TUI"""
stdscr.clear()
@@ -480,6 +561,204 @@ def view_active_configuration_tui(stdscr):
stdscr.getch()
def test_configuration_tui(stdscr):
"""Test connection to a configuration"""
configs = config_manager.get_configurations()
if not configs:
show_message(stdscr, "No configurations to test.")
return
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "Test Configuration Connection"
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 test:")
for idx, name in enumerate(config_names):
y = 3 + idx
if y >= h - 4: # Prevent overflow
break
if idx == selection:
stdscr.attron(curses.A_REVERSE)
stdscr.addstr(y, 2, f"> {name}")
stdscr.attroff(curses.A_REVERSE)
else:
stdscr.addstr(y, 2, f" {name}")
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to test, '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]:
# Test configuration
name_to_test = config_names[selection]
show_message(stdscr, f"Testing connection to '{name_to_test}'...", False)
success, message = config_manager.test_configuration_connection(name_to_test)
if success:
show_message(stdscr, f"✅ {message}")
else:
show_message(stdscr, f"❌ {message}")
return
elif key == ord('c') or key == ord('C'):
show_message(stdscr, "Cancelled.")
return
def export_configurations_tui(stdscr):
"""Export configurations to JSON or YAML"""
configs = config_manager.get_configurations()
if not configs:
show_message(stdscr, "No configurations to export.")
return
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "Export Configurations"
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
# Select format
formats = ["JSON", "YAML"]
format_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 export format:")
for idx, fmt in enumerate(formats):
y = 3 + idx
if idx == format_selection:
stdscr.attron(curses.A_REVERSE)
stdscr.addstr(y, 2, f"> {fmt}")
stdscr.attroff(curses.A_REVERSE)
else:
stdscr.addstr(y, 2, f" {fmt}")
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 format_selection > 0:
format_selection -= 1
elif key == curses.KEY_DOWN and format_selection < len(formats) - 1:
format_selection += 1
elif key == curses.KEY_ENTER or key in [10, 13]:
break
elif key == ord('c') or key == ord('C'):
show_message(stdscr, "Cancelled.")
return
# Get filename
filename = get_string_input(stdscr, "Filename (without extension): ", 5, 0, 30)
if not filename:
show_message(stdscr, "Cancelled.")
return
# Export configurations
try:
if format_selection == 0: # JSON
export_file = Path(f"{filename}.json")
with open(export_file, 'w') as f:
json.dump(configs, f, indent=2)
else: # YAML
export_file = Path(f"{filename}.yaml")
with open(export_file, 'w') as f:
yaml.dump(configs, f, default_flow_style=False)
show_message(stdscr, f"Configurations exported to {export_file}")
except Exception as e:
show_message(stdscr, f"Error exporting configurations: {e}")
def import_configurations_tui(stdscr):
"""Import configurations from JSON or YAML"""
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "Import Configurations"
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
# Get filename
filename = get_string_input(stdscr, "Filename (with extension .json or .yaml): ", 2, 0, 50)
if not filename:
show_message(stdscr, "Cancelled.")
return
# Check if file exists
file_path = Path(filename)
if not file_path.exists():
show_message(stdscr, f"Error: File '{filename}' not found.")
return
# Import configurations
try:
with open(file_path, 'r') as f:
if filename.endswith('.json'):
imported_configs = json.load(f)
elif filename.endswith(('.yaml', '.yml')):
imported_configs = yaml.safe_load(f)
else:
show_message(stdscr, "Error: Unsupported file format. Use .json or .yaml")
return
# Validate imported configurations
if not isinstance(imported_configs, dict):
show_message(stdscr, "Error: Invalid configuration format.")
return
# Ask for confirmation
confirm_prompt = f"Import {len(imported_configs)} configurations? (y/n): "
confirm = get_string_input(stdscr, confirm_prompt, 4, 0, 1)
if confirm.lower() != 'y':
show_message(stdscr, "Cancelled.")
return
# Import configurations
imported_count = 0
for name, config in imported_configs.items():
try:
config_manager.add_configuration(name, config)
imported_count += 1
except Exception as e:
show_message(stdscr, f"Warning: Could not import '{name}': {e}")
show_message(stdscr, f"Successfully imported {imported_count} configurations.")
except Exception as e:
show_message(stdscr, f"Error importing configurations: {e}")
def main():
"""Main entry point for the TUI"""
curses.wrapper(main_tui)