Phase 2: Enhanced TUI/CLI with search, export/import, and custom tools for Proxmox and Ollama
This commit is contained in:
1 parent
f5026965f7
commit
9f3298fa13
7 files changed
+1353
-16
No files matched your search
+289
-10
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user