orchestrated-discussions/.venv/lib/python3.12/site-packages/cmdforge/ui_snack.py

707 lines
28 KiB
Python

"""BIOS-style TUI for CmdForge using snack (python3-newt)."""
import sys
# Ensure system packages are accessible
if '/usr/lib/python3/dist-packages' not in sys.path:
sys.path.insert(0, '/usr/lib/python3/dist-packages')
import snack
from typing import Optional, List, Tuple
from .tool import (
Tool, ToolArgument, PromptStep, CodeStep, Step,
list_tools, load_tool, save_tool, delete_tool, tool_exists
)
from .providers import Provider, load_providers, add_provider, delete_provider, get_provider
class CmdForgeUI:
"""BIOS-style UI for CmdForge."""
def __init__(self):
self.screen = None
def run(self):
"""Run the UI."""
self.screen = snack.SnackScreen()
# Enable mouse support
self.screen.pushHelpLine(" Tab/Arrow:Navigate | Enter:Select | Mouse:Click | Esc:Back ")
try:
# Enable mouse - newt supports GPM and xterm mouse
import os
os.environ.setdefault('NEWT_MONO', '0')
self.main_menu()
finally:
self.screen.finish()
def main_menu(self):
"""Show the main menu."""
while True:
items = [
("Create New Tool", "create"),
("Edit Tool", "edit"),
("Delete Tool", "delete"),
("List Tools", "list"),
("Test Tool", "test"),
("Manage Providers", "providers"),
("Exit", "exit"),
]
listbox = snack.Listbox(height=7, width=30, returnExit=1)
for label, value in items:
listbox.append(label, value)
grid = snack.GridForm(self.screen, "CmdForge Manager", 1, 1)
grid.add(listbox, 0, 0)
result = grid.runOnce()
selected = listbox.current()
if selected == "exit" or result == "ESC":
break
elif selected == "create":
self.tool_builder(None)
elif selected == "edit":
self.select_and_edit_tool()
elif selected == "delete":
self.select_and_delete_tool()
elif selected == "list":
self.show_tools_list()
elif selected == "test":
self.select_and_test_tool()
elif selected == "providers":
self.manage_providers()
def message_box(self, title: str, message: str):
"""Show a message box."""
snack.ButtonChoiceWindow(self.screen, title, message, ["OK"])
def yes_no(self, title: str, message: str) -> bool:
"""Show a yes/no dialog."""
result = snack.ButtonChoiceWindow(self.screen, title, message, ["Yes", "No"])
return result == "yes"
def input_box(self, title: str, prompt: str, initial: str = "", width: int = 40) -> Optional[str]:
"""Show an input dialog."""
entry = snack.Entry(width, initial)
grid = snack.GridForm(self.screen, title, 1, 3)
grid.add(snack.Label(prompt), 0, 0)
grid.add(entry, 0, 1, padding=(0, 1, 0, 1))
buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 0, 2)
result = grid.runOnce()
if buttons.buttonPressed(result) == "ok":
return entry.value()
return None
def text_edit(self, title: str, initial: str = "", width: int = 60, height: int = 10) -> Optional[str]:
"""Show a multi-line text editor."""
text = snack.Textbox(width, height, initial, scroll=1, wrap=1)
# snack doesn't have a true multi-line editor, so we use Entry for now
# For multi-line, we'll use a workaround with a simple entry
entry = snack.Entry(width, initial, scroll=1)
grid = snack.GridForm(self.screen, title, 1, 2)
grid.add(entry, 0, 0, padding=(0, 0, 0, 1))
buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 0, 1)
result = grid.runOnce()
if buttons.buttonPressed(result) == "ok":
return entry.value()
return None
def select_provider(self) -> Optional[str]:
"""Show provider selection dialog."""
providers = load_providers()
listbox = snack.Listbox(height=min(len(providers) + 1, 8), width=50, returnExit=1)
for p in providers:
listbox.append(f"{p.name}: {p.command}", p.name)
listbox.append("[ + Add New Provider ]", "__new__")
grid = snack.GridForm(self.screen, "Select Provider", 1, 2)
grid.add(listbox, 0, 0)
buttons = snack.ButtonBar(self.screen, [("Select", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 0, 1)
result = grid.runOnce()
if buttons.buttonPressed(result) == "cancel" or result == "ESC":
return None
selected = listbox.current()
if selected == "__new__":
provider = self.add_provider_dialog()
if provider:
add_provider(provider)
return provider.name
return None
return selected
def add_provider_dialog(self) -> Optional[Provider]:
"""Dialog to add a new provider."""
name_entry = snack.Entry(30, "")
cmd_entry = snack.Entry(40, "")
desc_entry = snack.Entry(40, "")
grid = snack.GridForm(self.screen, "Add Provider", 2, 4)
grid.add(snack.Label("Name:"), 0, 0, anchorLeft=1)
grid.add(name_entry, 1, 0, padding=(1, 0, 0, 0))
grid.add(snack.Label("Command:"), 0, 1, anchorLeft=1)
grid.add(cmd_entry, 1, 1, padding=(1, 0, 0, 0))
grid.add(snack.Label("Description:"), 0, 2, anchorLeft=1)
grid.add(desc_entry, 1, 2, padding=(1, 0, 0, 0))
buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 0, 3, growx=1, growy=1)
result = grid.runOnce()
if buttons.buttonPressed(result) == "ok":
name = name_entry.value().strip()
cmd = cmd_entry.value().strip()
if name and cmd:
return Provider(name=name, command=cmd, description=desc_entry.value().strip())
self.message_box("Error", "Name and command are required.")
return None
def add_argument_dialog(self, existing: Optional[ToolArgument] = None) -> Optional[ToolArgument]:
"""Dialog to add/edit an argument."""
flag_entry = snack.Entry(20, existing.flag if existing else "--")
var_entry = snack.Entry(20, existing.variable if existing else "")
default_entry = snack.Entry(20, existing.default or "" if existing else "")
desc_entry = snack.Entry(40, existing.description if existing else "")
title = "Edit Argument" if existing else "Add Argument"
grid = snack.GridForm(self.screen, title, 2, 5)
grid.add(snack.Label("Flag:"), 0, 0, anchorLeft=1)
grid.add(flag_entry, 1, 0, padding=(1, 0, 0, 0))
grid.add(snack.Label("Variable:"), 0, 1, anchorLeft=1)
grid.add(var_entry, 1, 1, padding=(1, 0, 0, 0))
grid.add(snack.Label("Default:"), 0, 2, anchorLeft=1)
grid.add(default_entry, 1, 2, padding=(1, 0, 0, 0))
grid.add(snack.Label("Description:"), 0, 3, anchorLeft=1)
grid.add(desc_entry, 1, 3, padding=(1, 0, 0, 0))
buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 0, 4, growx=1)
result = grid.runOnce()
if buttons.buttonPressed(result) == "ok":
flag = flag_entry.value().strip()
var = var_entry.value().strip()
if not flag:
self.message_box("Error", "Flag is required.")
return None
if not var:
var = flag.lstrip("-").replace("-", "_")
return ToolArgument(
flag=flag,
variable=var,
default=default_entry.value().strip() or None,
description=desc_entry.value().strip()
)
return None
def add_step_dialog(self, tool: Tool, existing_step: Optional[Step] = None, step_idx: int = -1) -> Optional[Step]:
"""Dialog to choose and add a step."""
if existing_step:
# Edit existing step
if isinstance(existing_step, PromptStep):
return self.add_prompt_dialog(tool, existing_step, step_idx)
else:
return self.add_code_dialog(tool, existing_step, step_idx)
# Choose step type
listbox = snack.Listbox(height=2, width=30, returnExit=1)
listbox.append("Prompt (AI call)", "prompt")
listbox.append("Code (Python)", "code")
grid = snack.GridForm(self.screen, "Add Step", 1, 2)
grid.add(listbox, 0, 0)
buttons = snack.ButtonBar(self.screen, [("Select", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 0, 1)
result = grid.runOnce()
if buttons.buttonPressed(result) == "cancel" or result == "ESC":
return None
step_type = listbox.current()
if step_type == "prompt":
return self.add_prompt_dialog(tool, None, step_idx)
else:
return self.add_code_dialog(tool, None, step_idx)
def get_available_variables(self, tool: Tool, up_to_step: int = -1) -> List[str]:
"""Get available variables at a point in the tool."""
variables = ["input"]
for arg in tool.arguments:
variables.append(arg.variable)
if up_to_step == -1:
up_to_step = len(tool.steps)
for i, step in enumerate(tool.steps):
if i >= up_to_step:
break
variables.append(step.output_var)
return variables
def add_prompt_dialog(self, tool: Tool, existing: Optional[PromptStep] = None, step_idx: int = -1) -> Optional[PromptStep]:
"""Dialog to add/edit a prompt step."""
available = self.get_available_variables(tool, step_idx if step_idx >= 0 else -1)
var_help = "Variables: " + ", ".join(f"{{{v}}}" for v in available)
# Provider selection first
provider = self.select_provider()
if not provider:
provider = existing.provider if existing else "mock"
prompt_entry = snack.Entry(60, existing.prompt if existing else f"Process this:\n\n{{input}}", scroll=1)
output_entry = snack.Entry(20, existing.output_var if existing else "result")
title = "Edit Prompt Step" if existing else "Add Prompt Step"
grid = snack.GridForm(self.screen, title, 2, 4)
grid.add(snack.Label(f"Provider: {provider}"), 0, 0, anchorLeft=1, growx=1)
grid.add(snack.Label(""), 1, 0)
grid.add(snack.Label(f"Prompt ({var_help}):"), 0, 1, anchorLeft=1, growx=1)
grid.add(snack.Label(""), 1, 1)
grid.add(prompt_entry, 0, 2, growx=1)
grid.add(snack.Label(""), 1, 2)
sub_grid = snack.Grid(2, 1)
sub_grid.setField(snack.Label("Output var:"), 0, 0, anchorLeft=1)
sub_grid.setField(output_entry, 1, 0, padding=(1, 0, 0, 0))
grid.add(sub_grid, 0, 3, anchorLeft=1)
buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 1, 3)
result = grid.runOnce()
if buttons.buttonPressed(result) == "ok":
prompt = prompt_entry.value().strip()
output_var = output_entry.value().strip()
if not prompt:
self.message_box("Error", "Prompt is required.")
return None
if not output_var:
output_var = "result"
return PromptStep(prompt=prompt, provider=provider, output_var=output_var)
return None
def add_code_dialog(self, tool: Tool, existing: Optional[CodeStep] = None, step_idx: int = -1) -> Optional[CodeStep]:
"""Dialog to add/edit a code step."""
available = self.get_available_variables(tool, step_idx if step_idx >= 0 else -1)
var_help = "Variables: " + ", ".join(available)
default_code = existing.code if existing else "# Set 'result' for output\nresult = input.upper()"
code_entry = snack.Entry(60, default_code, scroll=1)
output_entry = snack.Entry(20, existing.output_var if existing else "processed")
title = "Edit Code Step" if existing else "Add Code Step"
grid = snack.GridForm(self.screen, title, 2, 4)
grid.add(snack.Label(f"Python Code ({var_help}):"), 0, 0, anchorLeft=1, growx=1)
grid.add(snack.Label("Set 'result' variable for output"), 1, 0)
grid.add(code_entry, 0, 1, growx=1)
grid.add(snack.Label(""), 1, 1)
sub_grid = snack.Grid(2, 1)
sub_grid.setField(snack.Label("Output var:"), 0, 0, anchorLeft=1)
sub_grid.setField(output_entry, 1, 0, padding=(1, 0, 0, 0))
grid.add(sub_grid, 0, 2, anchorLeft=1)
buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 1, 2)
result = grid.runOnce()
if buttons.buttonPressed(result) == "ok":
code = code_entry.value().strip()
output_var = output_entry.value().strip()
if not code:
self.message_box("Error", "Code is required.")
return None
if not output_var:
output_var = "processed"
return CodeStep(code=code, output_var=output_var)
return None
def tool_builder(self, existing: Optional[Tool] = None) -> Optional[Tool]:
"""Main tool builder - BIOS-style unified form."""
is_edit = existing is not None
# Initialize tool
if existing:
tool = Tool(
name=existing.name,
description=existing.description,
arguments=list(existing.arguments),
steps=list(existing.steps),
output=existing.output
)
else:
tool = Tool(name="", description="", arguments=[], steps=[], output="{input}")
while True:
# Create form elements
name_entry = snack.Entry(25, tool.name, scroll=0)
desc_entry = snack.Entry(25, tool.description, scroll=1)
output_entry = snack.Entry(25, tool.output, scroll=1)
# Arguments listbox
args_listbox = snack.Listbox(height=4, width=35, returnExit=0, scroll=1)
for i, arg in enumerate(tool.arguments):
args_listbox.append(f"{arg.flag} -> {{{arg.variable}}}", i)
args_listbox.append("[ + Add ]", "add")
# Steps listbox
steps_listbox = snack.Listbox(height=5, width=35, returnExit=0, scroll=1)
for i, step in enumerate(tool.steps):
if isinstance(step, PromptStep):
steps_listbox.append(f"P:{step.provider} -> {{{step.output_var}}}", i)
else:
steps_listbox.append(f"C: -> {{{step.output_var}}}", i)
steps_listbox.append("[ + Add ]", "add")
# Build the grid layout using nested grids for better control
title = f"Edit Tool: {tool.name}" if is_edit and tool.name else "New Tool"
# Left column grid: Name, Description, Output
left_grid = snack.Grid(1, 6)
left_grid.setField(snack.Label("Name:"), 0, 0, anchorLeft=1)
left_grid.setField(name_entry, 0, 1, anchorLeft=1)
left_grid.setField(snack.Label("Description:"), 0, 2, anchorLeft=1, padding=(0, 1, 0, 0))
left_grid.setField(desc_entry, 0, 3, anchorLeft=1)
left_grid.setField(snack.Label("Output:"), 0, 4, anchorLeft=1, padding=(0, 1, 0, 0))
left_grid.setField(output_entry, 0, 5, anchorLeft=1)
# Right column grid: Arguments and Steps
right_grid = snack.Grid(1, 4)
right_grid.setField(snack.Label("Arguments:"), 0, 0, anchorLeft=1)
right_grid.setField(args_listbox, 0, 1, anchorLeft=1)
right_grid.setField(snack.Label("Execution Steps:"), 0, 2, anchorLeft=1, padding=(0, 1, 0, 0))
right_grid.setField(steps_listbox, 0, 3, anchorLeft=1)
# Main grid
grid = snack.GridForm(self.screen, title, 2, 2)
grid.add(left_grid, 0, 0, anchorTop=1, padding=(0, 0, 2, 0))
grid.add(right_grid, 1, 0, anchorTop=1)
# Buttons at bottom
buttons = snack.ButtonBar(self.screen, [("Save", "save"), ("Cancel", "cancel")])
grid.add(buttons, 0, 1, growx=1)
# Handle hotkeys for listbox interaction
form = grid.form
while True:
result = form.run()
# Update tool from entries
if not is_edit:
tool.name = name_entry.value().strip()
tool.description = desc_entry.value().strip()
tool.output = output_entry.value().strip()
# Check what was activated
if result == args_listbox:
selected = args_listbox.current()
if selected == "add":
new_arg = self.add_argument_dialog()
if new_arg:
tool.arguments.append(new_arg)
break # Refresh form
elif isinstance(selected, int):
# Edit/delete existing argument
action = self.arg_action_menu(tool.arguments[selected])
if action == "edit":
updated = self.add_argument_dialog(tool.arguments[selected])
if updated:
tool.arguments[selected] = updated
elif action == "delete":
tool.arguments.pop(selected)
break # Refresh form
elif result == steps_listbox:
selected = steps_listbox.current()
if selected == "add":
new_step = self.add_step_dialog(tool)
if new_step:
tool.steps.append(new_step)
break # Refresh form
elif isinstance(selected, int):
# Edit/delete existing step
action = self.step_action_menu(tool.steps[selected], selected, len(tool.steps))
if action == "edit":
updated = self.add_step_dialog(tool, tool.steps[selected], selected)
if updated:
tool.steps[selected] = updated
elif action == "delete":
tool.steps.pop(selected)
elif action == "move_up" and selected > 0:
tool.steps[selected], tool.steps[selected-1] = tool.steps[selected-1], tool.steps[selected]
elif action == "move_down" and selected < len(tool.steps) - 1:
tool.steps[selected], tool.steps[selected+1] = tool.steps[selected+1], tool.steps[selected]
break # Refresh form
elif buttons.buttonPressed(result) == "save":
if not tool.name:
self.message_box("Error", "Tool name is required.")
break
if not is_edit and tool_exists(tool.name):
if not self.yes_no("Overwrite?", f"Tool '{tool.name}' exists. Overwrite?"):
break
self.screen.popWindow()
save_tool(tool)
self.message_box("Success", f"Tool '{tool.name}' saved!")
return tool
elif buttons.buttonPressed(result) == "cancel" or result == "ESC":
self.screen.popWindow()
return None
else:
# Tab between fields, continue
continue
self.screen.popWindow()
def arg_action_menu(self, arg: ToolArgument) -> Optional[str]:
"""Show action menu for an argument."""
listbox = snack.Listbox(height=3, width=20, returnExit=1)
listbox.append("Edit", "edit")
listbox.append("Delete", "delete")
listbox.append("Cancel", "cancel")
grid = snack.GridForm(self.screen, f"Argument: {arg.flag}", 1, 1)
grid.add(listbox, 0, 0)
grid.runOnce()
return listbox.current() if listbox.current() != "cancel" else None
def step_action_menu(self, step: Step, idx: int, total: int) -> Optional[str]:
"""Show action menu for a step."""
step_type = "Prompt" if isinstance(step, PromptStep) else "Code"
items = [("Edit", "edit")]
if idx > 0:
items.append(("Move Up", "move_up"))
if idx < total - 1:
items.append(("Move Down", "move_down"))
items.append(("Delete", "delete"))
items.append(("Cancel", "cancel"))
listbox = snack.Listbox(height=len(items), width=20, returnExit=1)
for label, value in items:
listbox.append(label, value)
grid = snack.GridForm(self.screen, f"Step {idx+1}: {step_type}", 1, 1)
grid.add(listbox, 0, 0)
grid.runOnce()
return listbox.current() if listbox.current() != "cancel" else None
def select_and_edit_tool(self):
"""Select a tool to edit."""
tools = list_tools()
if not tools:
self.message_box("Edit Tool", "No tools found.")
return
listbox = snack.Listbox(height=min(len(tools), 10), width=40, returnExit=1, scroll=1)
for name in tools:
tool = load_tool(name)
desc = tool.description[:30] if tool and tool.description else "No description"
listbox.append(f"{name}: {desc}", name)
grid = snack.GridForm(self.screen, "Select Tool to Edit", 1, 2)
grid.add(listbox, 0, 0)
buttons = snack.ButtonBar(self.screen, [("Select", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 0, 1)
result = grid.runOnce()
if buttons.buttonPressed(result) == "ok":
selected = listbox.current()
tool = load_tool(selected)
if tool:
self.tool_builder(tool)
def select_and_delete_tool(self):
"""Select a tool to delete."""
tools = list_tools()
if not tools:
self.message_box("Delete Tool", "No tools found.")
return
listbox = snack.Listbox(height=min(len(tools), 10), width=40, returnExit=1, scroll=1)
for name in tools:
listbox.append(name, name)
grid = snack.GridForm(self.screen, "Select Tool to Delete", 1, 2)
grid.add(listbox, 0, 0)
buttons = snack.ButtonBar(self.screen, [("Delete", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 0, 1)
result = grid.runOnce()
if buttons.buttonPressed(result) == "ok":
selected = listbox.current()
if self.yes_no("Confirm", f"Delete tool '{selected}'?"):
if delete_tool(selected):
self.message_box("Deleted", f"Tool '{selected}' deleted.")
def show_tools_list(self):
"""Show list of all tools."""
tools = list_tools()
if not tools:
self.message_box("Tools", "No tools found.\n\nCreate one from the main menu.")
return
text = ""
for name in tools:
tool = load_tool(name)
if tool:
text += f"{name}\n"
text += f" {tool.description or 'No description'}\n"
if tool.arguments:
args = ", ".join(a.flag for a in tool.arguments)
text += f" Args: {args}\n"
if tool.steps:
steps = []
for s in tool.steps:
if isinstance(s, PromptStep):
steps.append(f"P:{s.provider}")
else:
steps.append("C")
text += f" Steps: {' -> '.join(steps)}\n"
text += "\n"
snack.ButtonChoiceWindow(self.screen, "Available Tools", text.strip(), ["OK"])
def select_and_test_tool(self):
"""Select a tool to test."""
tools = list_tools()
if not tools:
self.message_box("Test Tool", "No tools found.")
return
listbox = snack.Listbox(height=min(len(tools), 10), width=40, returnExit=1, scroll=1)
for name in tools:
listbox.append(name, name)
grid = snack.GridForm(self.screen, "Select Tool to Test", 1, 2)
grid.add(listbox, 0, 0)
buttons = snack.ButtonBar(self.screen, [("Test", "ok"), ("Cancel", "cancel")])
grid.add(buttons, 0, 1)
result = grid.runOnce()
if buttons.buttonPressed(result) == "ok":
selected = listbox.current()
tool = load_tool(selected)
if tool:
test_input = self.input_box("Test Input", "Enter test input:", "Hello world")
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
)
result_text = f"Exit code: {code}\n\nOutput:\n{output[:500]}"
self.message_box("Test Result", result_text)
def manage_providers(self):
"""Manage providers menu."""
while True:
providers = load_providers()
listbox = snack.Listbox(height=min(len(providers) + 2, 10), width=50, returnExit=1, scroll=1)
for p in providers:
listbox.append(f"{p.name}: {p.command}", p.name)
listbox.append("[ + Add Provider ]", "__add__")
listbox.append("[ <- Back ]", "__back__")
grid = snack.GridForm(self.screen, "Manage Providers", 1, 1)
grid.add(listbox, 0, 0)
grid.runOnce()
selected = listbox.current()
if selected == "__back__":
break
elif selected == "__add__":
provider = self.add_provider_dialog()
if provider:
add_provider(provider)
self.message_box("Success", f"Provider '{provider.name}' added.")
else:
# Edit or delete
provider = get_provider(selected)
if provider:
action = self.provider_action_menu(provider)
if action == "edit":
updated = self.add_provider_dialog() # TODO: pass existing
if updated:
add_provider(updated)
elif action == "delete":
if self.yes_no("Confirm", f"Delete provider '{selected}'?"):
delete_provider(selected)
def provider_action_menu(self, provider: Provider) -> Optional[str]:
"""Show action menu for a provider."""
listbox = snack.Listbox(height=3, width=20, returnExit=1)
listbox.append("Edit", "edit")
listbox.append("Delete", "delete")
listbox.append("Cancel", "cancel")
grid = snack.GridForm(self.screen, f"Provider: {provider.name}", 1, 1)
grid.add(listbox, 0, 0)
grid.runOnce()
return listbox.current() if listbox.current() != "cancel" else None
def run_ui():
"""Entry point for the snack UI."""
ui = CmdForgeUI()
ui.run()
if __name__ == "__main__":
run_ui()