Implement core SmartTools functionality
- providers.py: Provider abstraction with mock support - tool.py: Tool loading, saving, and wrapper script generation - runner.py: Tool execution engine with prompt building - ui.py: Dialog-based UI for tool management - cli.py: CLI entry point with list/create/edit/delete/test commands ~430 lines of Python as designed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
"""Dialog-based UI for managing SmartTools."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from .tool import Tool, ToolInput, list_tools, load_tool, save_tool, delete_tool, tool_exists
|
||||
|
||||
|
||||
def check_dialog() -> str:
|
||||
"""Check for available dialog program. Returns 'dialog', 'whiptail', or None."""
|
||||
for prog in ["dialog", "whiptail"]:
|
||||
try:
|
||||
subprocess.run([prog, "--version"], capture_output=True, check=False)
|
||||
return prog
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def run_dialog(args: list[str], dialog_prog: str = "dialog") -> Tuple[int, str]:
|
||||
"""
|
||||
Run a dialog command and return (exit_code, output).
|
||||
|
||||
Args:
|
||||
args: Dialog arguments
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
Tuple of (exit_code, captured_output)
|
||||
"""
|
||||
cmd = [dialog_prog] + args
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
# dialog outputs to stderr
|
||||
return result.returncode, result.stderr.strip()
|
||||
|
||||
|
||||
def show_menu(title: str, choices: list[tuple[str, str]], dialog_prog: str) -> Optional[str]:
|
||||
"""
|
||||
Show a menu and return the selected item.
|
||||
|
||||
Args:
|
||||
title: Menu title
|
||||
choices: List of (tag, description) tuples
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
Selected tag or None if cancelled
|
||||
"""
|
||||
args = ["--title", title, "--menu", "", "15", "60", str(len(choices))]
|
||||
for tag, desc in choices:
|
||||
args.extend([tag, desc])
|
||||
|
||||
code, output = run_dialog(args, dialog_prog)
|
||||
return output if code == 0 else None
|
||||
|
||||
|
||||
def show_input(title: str, prompt: str, initial: str = "", dialog_prog: str = "dialog") -> Optional[str]:
|
||||
"""
|
||||
Show an input box and return the entered text.
|
||||
|
||||
Args:
|
||||
title: Dialog title
|
||||
prompt: Prompt text
|
||||
initial: Initial value
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
Entered text or None if cancelled
|
||||
"""
|
||||
args = ["--title", title, "--inputbox", prompt, "10", "60", initial]
|
||||
code, output = run_dialog(args, dialog_prog)
|
||||
return output if code == 0 else None
|
||||
|
||||
|
||||
def show_textbox(title: str, text: str, dialog_prog: str = "dialog") -> Optional[str]:
|
||||
"""
|
||||
Show a text editor for multi-line input.
|
||||
|
||||
Args:
|
||||
title: Dialog title
|
||||
text: Initial text
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
Edited text or None if cancelled
|
||||
"""
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||
f.write(text)
|
||||
temp_path = f.name
|
||||
|
||||
args = ["--title", title, "--editbox", temp_path, "20", "70"]
|
||||
code, output = run_dialog(args, dialog_prog)
|
||||
|
||||
import os
|
||||
os.unlink(temp_path)
|
||||
|
||||
return output if code == 0 else None
|
||||
|
||||
|
||||
def show_yesno(title: str, text: str, dialog_prog: str = "dialog") -> bool:
|
||||
"""
|
||||
Show a yes/no dialog.
|
||||
|
||||
Args:
|
||||
title: Dialog title
|
||||
text: Question text
|
||||
dialog_prog: Dialog program to use
|
||||
|
||||
Returns:
|
||||
True if yes, False if no
|
||||
"""
|
||||
args = ["--title", title, "--yesno", text, "10", "60"]
|
||||
code, _ = run_dialog(args, dialog_prog)
|
||||
return code == 0
|
||||
|
||||
|
||||
def show_message(title: str, text: str, dialog_prog: str = "dialog"):
|
||||
"""Show a message box."""
|
||||
args = ["--title", title, "--msgbox", text, "10", "60"]
|
||||
run_dialog(args, dialog_prog)
|
||||
|
||||
|
||||
def create_tool_form(dialog_prog: str, existing: Optional[Tool] = None) -> Optional[Tool]:
|
||||
"""
|
||||
Show form for creating/editing a tool.
|
||||
|
||||
Args:
|
||||
dialog_prog: Dialog program to use
|
||||
existing: Existing tool to edit (None for new)
|
||||
|
||||
Returns:
|
||||
Tool object or None if cancelled
|
||||
"""
|
||||
# Get basic info
|
||||
name = show_input(
|
||||
"Tool Name",
|
||||
"Enter tool name (used as command):",
|
||||
existing.name if existing else "",
|
||||
dialog_prog
|
||||
)
|
||||
if not name:
|
||||
return None
|
||||
|
||||
# Check if name already exists (for new tools)
|
||||
if not existing and tool_exists(name):
|
||||
if not show_yesno("Overwrite?", f"Tool '{name}' already exists. Overwrite?", dialog_prog):
|
||||
return None
|
||||
|
||||
description = show_input(
|
||||
"Description",
|
||||
"Enter tool description:",
|
||||
existing.description if existing else "",
|
||||
dialog_prog
|
||||
)
|
||||
if description is None:
|
||||
return None
|
||||
|
||||
provider = show_input(
|
||||
"Provider",
|
||||
"Enter AI provider command (e.g., codex, claude, gemini):",
|
||||
existing.provider if existing else "codex",
|
||||
dialog_prog
|
||||
)
|
||||
if not provider:
|
||||
return None
|
||||
|
||||
provider_args = show_input(
|
||||
"Provider Args",
|
||||
"Enter provider arguments (e.g., -p):",
|
||||
existing.provider_args if existing else "-p",
|
||||
dialog_prog
|
||||
)
|
||||
if provider_args is None:
|
||||
return None
|
||||
|
||||
# Edit prompt
|
||||
default_prompt = existing.prompt if existing else "Process the following input:\n\n{input}"
|
||||
prompt = show_textbox("Prompt Template", default_prompt, dialog_prog)
|
||||
if not prompt:
|
||||
return None
|
||||
|
||||
# For simplicity, skip custom inputs in the UI for now
|
||||
# They can be added by editing config.yaml directly
|
||||
inputs = existing.inputs if existing else []
|
||||
|
||||
return Tool(
|
||||
name=name,
|
||||
description=description,
|
||||
prompt=prompt,
|
||||
provider=provider,
|
||||
provider_args=provider_args,
|
||||
inputs=inputs,
|
||||
post_process=existing.post_process if existing else None
|
||||
)
|
||||
|
||||
|
||||
def ui_list_tools(dialog_prog: str):
|
||||
"""Show list of tools."""
|
||||
tools = list_tools()
|
||||
if not tools:
|
||||
show_message("Tools", "No tools found.\n\nCreate your first tool from the main menu.", dialog_prog)
|
||||
return
|
||||
|
||||
text = "Available tools:\n\n"
|
||||
for name in tools:
|
||||
tool = load_tool(name)
|
||||
if tool:
|
||||
text += f" {name}: {tool.description or 'No description'}\n"
|
||||
text += f" Provider: {tool.provider}\n\n"
|
||||
|
||||
show_message("Tools", text, dialog_prog)
|
||||
|
||||
|
||||
def ui_create_tool(dialog_prog: str):
|
||||
"""Create a new tool."""
|
||||
tool = create_tool_form(dialog_prog)
|
||||
if tool:
|
||||
path = save_tool(tool)
|
||||
show_message("Success", f"Tool '{tool.name}' created!\n\nConfig: {path}\n\nRun it with: {tool.name} -i <file>", dialog_prog)
|
||||
|
||||
|
||||
def ui_edit_tool(dialog_prog: str):
|
||||
"""Edit an existing tool."""
|
||||
tools = list_tools()
|
||||
if not tools:
|
||||
show_message("Edit Tool", "No tools found.", dialog_prog)
|
||||
return
|
||||
|
||||
choices = [(name, load_tool(name).description or "No description") for name in tools]
|
||||
selected = show_menu("Select Tool to Edit", choices, dialog_prog)
|
||||
|
||||
if selected:
|
||||
existing = load_tool(selected)
|
||||
if existing:
|
||||
tool = create_tool_form(dialog_prog, existing)
|
||||
if tool:
|
||||
save_tool(tool)
|
||||
show_message("Success", f"Tool '{tool.name}' updated!", dialog_prog)
|
||||
|
||||
|
||||
def ui_delete_tool(dialog_prog: str):
|
||||
"""Delete a tool."""
|
||||
tools = list_tools()
|
||||
if not tools:
|
||||
show_message("Delete Tool", "No tools found.", dialog_prog)
|
||||
return
|
||||
|
||||
choices = [(name, load_tool(name).description or "No description") for name in tools]
|
||||
selected = show_menu("Select Tool to Delete", choices, dialog_prog)
|
||||
|
||||
if selected:
|
||||
if show_yesno("Confirm Delete", f"Delete tool '{selected}'?\n\nThis cannot be undone.", dialog_prog):
|
||||
if delete_tool(selected):
|
||||
show_message("Deleted", f"Tool '{selected}' deleted.", dialog_prog)
|
||||
else:
|
||||
show_message("Error", f"Failed to delete '{selected}'.", dialog_prog)
|
||||
|
||||
|
||||
def ui_test_tool(dialog_prog: str):
|
||||
"""Test a tool with mock provider."""
|
||||
tools = list_tools()
|
||||
if not tools:
|
||||
show_message("Test Tool", "No tools found.", dialog_prog)
|
||||
return
|
||||
|
||||
choices = [(name, load_tool(name).description or "No description") for name in tools]
|
||||
selected = show_menu("Select Tool to Test", choices, dialog_prog)
|
||||
|
||||
if selected:
|
||||
tool = load_tool(selected)
|
||||
if tool:
|
||||
# Get test input
|
||||
test_input = show_textbox("Test Input", "Enter test input here...", dialog_prog)
|
||||
if test_input:
|
||||
from .runner import run_tool
|
||||
output, code = run_tool(
|
||||
tool=tool,
|
||||
input_text=test_input,
|
||||
custom_args={},
|
||||
provider_override="mock",
|
||||
dry_run=False,
|
||||
show_prompt=False,
|
||||
verbose=False
|
||||
)
|
||||
show_message(
|
||||
"Test Result",
|
||||
f"Exit code: {code}\n\n--- Output ---\n{output[:500]}",
|
||||
dialog_prog
|
||||
)
|
||||
|
||||
|
||||
def main_menu(dialog_prog: str):
|
||||
"""Show the main menu."""
|
||||
while True:
|
||||
choice = show_menu(
|
||||
"SmartTools Manager",
|
||||
[
|
||||
("list", "List all tools"),
|
||||
("create", "Create new tool"),
|
||||
("edit", "Edit existing tool"),
|
||||
("delete", "Delete tool"),
|
||||
("test", "Test tool (mock provider)"),
|
||||
("exit", "Exit"),
|
||||
],
|
||||
dialog_prog
|
||||
)
|
||||
|
||||
if choice is None or choice == "exit":
|
||||
break
|
||||
elif choice == "list":
|
||||
ui_list_tools(dialog_prog)
|
||||
elif choice == "create":
|
||||
ui_create_tool(dialog_prog)
|
||||
elif choice == "edit":
|
||||
ui_edit_tool(dialog_prog)
|
||||
elif choice == "delete":
|
||||
ui_delete_tool(dialog_prog)
|
||||
elif choice == "test":
|
||||
ui_test_tool(dialog_prog)
|
||||
|
||||
|
||||
def run_ui():
|
||||
"""Entry point for the UI."""
|
||||
dialog_prog = check_dialog()
|
||||
|
||||
if not dialog_prog:
|
||||
print("Error: Neither 'dialog' nor 'whiptail' found.", file=sys.stderr)
|
||||
print("Install with: sudo apt install dialog", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
main_menu(dialog_prog)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
# Clear screen on exit
|
||||
subprocess.run(["clear"], check=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_ui()
|
||||
Reference in New Issue
Block a user