Refactor CLI and UI into modular packages, add registry scrutiny

Major refactoring:
- Split cli.py (1465 lines) into cli/ package with 6 modules
- Split ui_urwid.py (2313 lines) into ui_urwid/ package with 4 modules
- Maintain backwards compatibility via thin wrapper modules

New features:
- Add tool scrutiny system for registry publishing (honesty, transparency, scope, efficiency checks)
- Add optimization suggestions for AI calls that could be pure code

Bug fixes:
- Fix variable substitution escaping ({{literal}} now works)
- Fix provider command parsing with shlex for quoted paths
- Add error logging even without --verbose flag

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-01 02:44:39 -04:00
co-authored by Claude Opus 4.5
parent 4fb229858f
commit 97f9de52e6
27 changed files with 5195 additions and 3781 deletions
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
"""Allow running the UI as a module."""
from . import run_ui
if __name__ == "__main__":
run_ui()
+30
View File
@@ -0,0 +1,30 @@
"""Color palette for the BIOS-style TUI."""
# Color palette - BIOS style with 3D button effects
PALETTE = [
('body', 'white', 'dark blue'),
('header', 'white', 'dark red', 'bold'),
('footer', 'black', 'light gray'),
# Button colors - raised 3D effect
('button', 'black', 'light gray'),
('button_focus', 'white', 'dark red', 'bold'),
('button_highlight', 'white', 'light gray'), # Top/left edge (light)
('button_shadow', 'dark gray', 'light gray'), # Bottom/right edge (dark)
('button_pressed', 'black', 'dark gray'), # Pressed state
# Edit fields
('edit', 'black', 'light gray'),
('edit_focus', 'black', 'yellow'),
# List items
('listbox', 'black', 'light gray'),
('listbox_focus', 'white', 'dark red'),
# Dialog
('dialog', 'black', 'light gray'),
('dialog_border', 'white', 'dark blue'),
# Text styles
('label', 'yellow', 'dark blue', 'bold'),
('error', 'white', 'dark red', 'bold'),
('success', 'light green', 'dark blue', 'bold'),
# 3D shadow elements
('shadow', 'black', 'black'),
('shadow_edge', 'dark gray', 'dark blue'),
]
+715
View File
@@ -0,0 +1,715 @@
"""Custom widgets for the BIOS-style TUI."""
import urwid
class SelectableText(urwid.WidgetWrap):
"""A selectable text widget for list items."""
def __init__(self, text, value=None, on_select=None):
self.value = value
self.on_select = on_select
self.text_widget = urwid.Text(text)
display = urwid.AttrMap(self.text_widget, 'listbox', 'listbox_focus')
super().__init__(display)
def selectable(self):
return True
def keypress(self, size, key):
if key == 'enter' and self.on_select:
self.on_select(self.value)
return None
return key
def mouse_event(self, size, event, button, col, row, focus):
if event == 'mouse press' and button == 1 and self.on_select:
self.on_select(self.value)
return True
return False
class Button3D(urwid.WidgetWrap):
"""A 3D-style button using box-drawing characters for depth.
Creates a raised button effect like DOS/BIOS interfaces:
┌──────────┐
│ Label │▄
└──────────┘█
When focused, colors change to show selection.
"""
signals = ['click']
def __init__(self, label, on_press=None, user_data=None):
self.label = label
self.on_press = on_press
self.user_data = user_data
self._pressed = False
# Build the 3D button structure
self._build_widget()
super().__init__(self._widget)
def _build_widget(self):
"""Build the 3D button widget structure."""
label = self.label
width = len(label) + 4 # Padding inside button
# Button face with border
# Top edge: ┌────┐
top = '┌' + '─' * (width - 2) + '┐'
# Middle: │ Label │ with shadow
middle_text = '│ ' + label + ' │'
# Bottom edge: └────┘ with shadow
bottom = '└' + '─' * (width - 2) + '┘'
# Shadow characters (right and bottom)
shadow_right = '▄'
shadow_bottom = '█'
# Create the rows
top_row = urwid.Text(top + ' ') # Space for shadow alignment
middle_row = urwid.Columns([
('pack', urwid.Text(middle_text)),
('pack', urwid.Text(('shadow_edge', shadow_right))),
])
bottom_row = urwid.Columns([
('pack', urwid.Text(bottom)),
('pack', urwid.Text(('shadow_edge', shadow_right))),
])
shadow_row = urwid.Text(('shadow_edge', ' ' + shadow_bottom * (width - 1)))
# Stack them
pile = urwid.Pile([
top_row,
middle_row,
bottom_row,
shadow_row,
])
self._widget = urwid.AttrMap(pile, 'button', 'button_focus')
def selectable(self):
return True
def keypress(self, size, key):
if key == 'enter':
self._activate()
return None
return key
def mouse_event(self, size, event, button, col, row, focus):
if button == 1:
if event == 'mouse press':
self._pressed = True
return True
elif event == 'mouse release' and self._pressed:
self._pressed = False
self._activate()
return True
return False
def _activate(self):
"""Trigger the button callback."""
if self.on_press:
self.on_press(self.user_data)
self._emit('click')
class Button3DCompact(urwid.WidgetWrap):
"""A compact 3D button that fits on a single line with shadow effect.
Creates a subtle 3D effect: [ Label ]▌
Better for inline use where vertical space is limited.
"""
signals = ['click']
def __init__(self, label, on_press=None, user_data=None):
self.label = label
self.on_press = on_press
self.user_data = user_data
# Build compact button: ▐ Label ▌ with shadow
# Using block characters for edges
button_text = urwid.Text([
('button_highlight', '▐'),
('button', f' {label} '),
('button_shadow', '▌'),
('shadow_edge', '▄'),
])
self._widget = urwid.AttrMap(button_text, None, {
'button': 'button_focus',
'button_highlight': 'button_focus',
'button_shadow': 'button_focus',
})
super().__init__(self._widget)
def selectable(self):
return True
def keypress(self, size, key):
if key == 'enter':
self._activate()
return None
return key
def mouse_event(self, size, event, button, col, row, focus):
if button == 1 and event == 'mouse release':
self._activate()
return True
return False
def _activate(self):
if self.on_press:
self.on_press(self.user_data)
self._emit('click')
class ClickableButton(urwid.WidgetWrap):
"""A button that responds to mouse clicks (legacy wrapper)."""
def __init__(self, label, on_press=None, user_data=None):
self.on_press = on_press
self.user_data = user_data
button = urwid.Button(label)
if on_press:
urwid.connect_signal(button, 'click', self._handle_click)
display = urwid.AttrMap(button, 'button', 'button_focus')
super().__init__(display)
def _handle_click(self, button):
if self.on_press:
self.on_press(self.user_data)
class SelectableToolItem(urwid.WidgetWrap):
"""A selectable tool item that maintains selection state."""
def __init__(self, name, on_select=None):
self.name = name
self.on_select = on_select
self._selected = False
self.text_widget = urwid.Text(f" {name} ")
self.attr_map = urwid.AttrMap(self.text_widget, 'listbox', 'listbox_focus')
super().__init__(self.attr_map)
def selectable(self):
return True
def set_selected(self, selected):
"""Set whether this item is the selected tool."""
self._selected = selected
if self._selected:
self.attr_map.set_attr_map({None: 'listbox_focus'})
else:
self.attr_map.set_attr_map({None: 'listbox'})
def keypress(self, size, key):
if key == 'enter' and self.on_select:
self.on_select(self.name)
return None
return key
def mouse_event(self, size, event, button, col, row, focus):
if event == 'mouse press' and button == 1:
# Single click just selects/focuses - don't call on_select
# on_select is only called on Enter key (to edit)
return True
return False
class ToolListBox(urwid.ListBox):
"""A ListBox that keeps arrow keys internal and passes Tab out."""
def __init__(self, body, on_focus_change=None):
super().__init__(body)
self.on_focus_change = on_focus_change
self._last_focus = None
def keypress(self, size, key):
if key in ('up', 'down'):
# Handle arrow keys internally - navigate within list
result = super().keypress(size, key)
# Check if focus changed
self._check_focus_change()
return result
elif key == 'tab':
# Pass tab out to parent for focus cycling
return key
elif key == 'shift tab':
return key
else:
return super().keypress(size, key)
def _check_focus_change(self):
"""Check if focus changed and notify callback."""
try:
current = self.focus
if current is not self._last_focus:
self._last_focus = current
if self.on_focus_change and isinstance(current, SelectableToolItem):
self.on_focus_change(current.name)
except (IndexError, TypeError):
pass
def render(self, size, focus=False):
# Check focus on render too (for initial display)
if focus:
self._check_focus_change()
return super().render(size, focus)
class TabCyclePile(urwid.Pile):
"""A Pile that uses Tab/Shift-Tab to cycle between specific positions.
Args:
widget_list: List of widgets (same as urwid.Pile)
tab_positions: List of indices in the pile that Tab should cycle between.
Default is [0] (only first position).
"""
def __init__(self, widget_list, tab_positions=None):
super().__init__(widget_list)
# Positions in the pile that Tab should cycle between
self.tab_positions = tab_positions or [0]
self._current_tab_idx = 0
def keypress(self, size, key):
if key == 'tab':
# Move to next tab position
self._current_tab_idx = (self._current_tab_idx + 1) % len(self.tab_positions)
self.focus_position = self.tab_positions[self._current_tab_idx]
return None
elif key == 'shift tab':
# Move to previous tab position
self._current_tab_idx = (self._current_tab_idx - 1) % len(self.tab_positions)
self.focus_position = self.tab_positions[self._current_tab_idx]
return None
else:
return super().keypress(size, key)
class TabPassEdit(urwid.Edit):
"""A multiline Edit that passes Tab through for focus cycling instead of inserting tabs."""
def keypress(self, size, key):
if key in ('tab', 'shift tab'):
# Pass Tab through to parent for focus cycling
return key
return super().keypress(size, key)
class UndoableEdit(urwid.Edit):
"""A multiline Edit with undo/redo support.
Features:
- Undo with Alt+U (up to 50 states)
- Redo with Alt+R
- Tab passes through for focus cycling
"""
MAX_UNDO = 50 # Maximum undo history size
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._undo_stack = [] # List of (text, cursor_pos) tuples
self._redo_stack = []
def keypress(self, size, key):
if key in ('tab', 'shift tab'):
return key
# Handle undo (Alt+U or meta u)
if key in ('meta u', 'alt u'):
self._undo()
return None
# Handle redo (Alt+R or meta r)
if key in ('meta r', 'alt r'):
self._redo()
return None
# Save current state BEFORE the edit for undo
old_text = self.edit_text
old_pos = self.edit_pos
# Let the parent handle the keypress
result = super().keypress(size, key)
# If text changed, save the old state to undo stack
if self.edit_text != old_text:
self._save_undo_state(old_text, old_pos)
self._redo_stack.clear() # Clear redo on new edit
return result
def _save_undo_state(self, text, pos):
"""Save state to undo stack."""
# Don't save duplicate states
if self._undo_stack and self._undo_stack[-1][0] == text:
return
if len(self._undo_stack) >= self.MAX_UNDO:
self._undo_stack.pop(0)
self._undo_stack.append((text, pos))
def _undo(self):
"""Restore previous state from undo stack."""
if not self._undo_stack:
return
# Save current state to redo stack
self._redo_stack.append((self.edit_text, self.edit_pos))
# Restore previous state
text, pos = self._undo_stack.pop()
self.set_edit_text(text)
self.set_edit_pos(min(pos, len(text)))
def _redo(self):
"""Restore state from redo stack."""
if not self._redo_stack:
return
# Save current state to undo stack
self._undo_stack.append((self.edit_text, self.edit_pos))
# Restore redo state
text, pos = self._redo_stack.pop()
self.set_edit_text(text)
self.set_edit_pos(min(pos, len(text)))
class DOSScrollBar(urwid.WidgetWrap):
"""A DOS-style scrollbar with arrow buttons at top and bottom.
Renders a scrollbar on the right side of the wrapped widget with:
- ▲ arrow at top (click to scroll up)
- ░ track with █ thumb showing scroll position
- ▼ arrow at bottom (click to scroll down)
Click zones (expanded to last 2 columns for easier clicking):
- Top 25%: scroll up 3 lines
- Bottom 25%: scroll down 3 lines
- Middle: page up/down based on which half clicked
"""
def __init__(self, widget):
self._wrapped = widget
# Create a columns layout: content on left, scrollbar on right
super().__init__(widget)
def render(self, size, focus=False):
maxcol, maxrow = size
# Render the wrapped widget with one less column for scrollbar
content_size = (maxcol - 1, maxrow)
content_canvas = self._wrapped.render(content_size, focus)
# Build the scrollbar column
scrollbar_chars = []
# Up arrow at top
scrollbar_chars.append('▲')
# Calculate thumb position
if maxrow > 2:
track_height = maxrow - 2 # Minus the two arrow buttons
# Get scroll position info from wrapped widget
try:
if hasattr(self._wrapped, 'rows_max'):
rows_max = self._wrapped.rows_max(content_size)
scroll_pos = self._wrapped.get_scrollpos(content_size)
else:
rows_max = maxrow
scroll_pos = 0
if rows_max > maxrow:
# Calculate thumb position within track
thumb_pos = int((scroll_pos / (rows_max - maxrow)) * (track_height - 1))
thumb_pos = max(0, min(thumb_pos, track_height - 1))
else:
thumb_pos = 0
except (AttributeError, TypeError, ZeroDivisionError):
thumb_pos = 0
# Build track with thumb
for i in range(track_height):
if i == thumb_pos:
scrollbar_chars.append('█') # Thumb
else:
scrollbar_chars.append('░') # Track
# Down arrow at bottom
scrollbar_chars.append('▼')
# Create scrollbar canvas
scrollbar_text = '\n'.join(scrollbar_chars[:maxrow])
scrollbar_canvas = urwid.Text(scrollbar_text).render((1,))
# Combine canvases
combined = urwid.CanvasJoin([
(content_canvas, None, focus, content_size[0]),
(scrollbar_canvas, None, False, 1),
])
return combined
def keypress(self, size, key):
maxcol, maxrow = size
content_size = (maxcol - 1, maxrow)
return self._wrapped.keypress(content_size, key)
def mouse_event(self, size, event, button, col, row, focus):
maxcol, maxrow = size
content_size = (maxcol - 1, maxrow)
# Expand clickable area - last 2 columns count as scrollbar
if col >= maxcol - 2:
if button == 1 and event == 'mouse press':
# Top 25% of scrollbar = scroll up
if row < maxrow // 4:
for _ in range(3):
self._wrapped.keypress(content_size, 'up')
self._invalidate()
return True
# Bottom 25% of scrollbar = scroll down
elif row >= maxrow - (maxrow // 4):
for _ in range(3):
self._wrapped.keypress(content_size, 'down')
self._invalidate()
return True
# Middle = page up/down based on which half
elif row < maxrow // 2:
for _ in range(maxrow // 2):
self._wrapped.keypress(content_size, 'up')
self._invalidate()
return True
else:
for _ in range(maxrow // 2):
self._wrapped.keypress(content_size, 'down')
self._invalidate()
return True
# Handle mouse wheel on scrollbar
if button == 4: # Scroll up
for _ in range(3):
self._wrapped.keypress(content_size, 'up')
self._invalidate()
return True
elif button == 5: # Scroll down
for _ in range(3):
self._wrapped.keypress(content_size, 'down')
self._invalidate()
return True
return True # Consume other scrollbar clicks
# Pass to wrapped widget
return self._wrapped.mouse_event(content_size, event, button, col, row, focus)
def selectable(self):
return self._wrapped.selectable()
def sizing(self):
return frozenset([urwid.Sizing.BOX])
class ToolBuilderLayout(urwid.WidgetWrap):
"""Custom layout for tool builder that handles Tab cycling across all sections."""
def __init__(self, left_box, args_box, steps_box, args_section, steps_section, bottom_buttons, on_cancel=None):
self._current_section = 0
self.on_cancel = on_cancel
# Store references to LineBoxes for title highlighting
self.left_box = left_box
self.args_box = args_box
self.steps_box = steps_box
# Build visual layout: left column and right column side by side
right_pile = urwid.Pile([
('weight', 1, args_section),
('pack', urwid.Divider()),
('weight', 1, steps_section),
])
columns = urwid.Columns([
('weight', 1, left_box),
('weight', 1, right_pile),
], dividechars=1)
main_pile = urwid.Pile([
('weight', 1, columns),
('pack', urwid.Divider()),
('pack', bottom_buttons),
])
super().__init__(main_pile)
# Set initial highlight
self._update_section_titles()
def keypress(self, size, key):
if key == 'tab':
self._current_section = (self._current_section + 1) % 4
self._focus_section(self._current_section)
self._update_section_titles()
return None
elif key == 'shift tab':
self._current_section = (self._current_section - 1) % 4
self._focus_section(self._current_section)
self._update_section_titles()
return None
elif key == 'esc':
# Go back to main menu instead of exiting
if self.on_cancel:
self.on_cancel(None)
return None
else:
return super().keypress(size, key)
def mouse_event(self, size, event, button, col, row, focus):
# Let the parent handle the mouse event first
result = super().mouse_event(size, event, button, col, row, focus)
# After mouse click, detect which section has focus and update titles
if event == 'mouse press':
self._detect_current_section()
self._update_section_titles()
return result
def _detect_current_section(self):
"""Detect which section currently has focus based on widget hierarchy."""
main_pile = self._w
# Check if bottom buttons have focus (position 2)
if main_pile.focus_position == 2:
self._current_section = 3
return
# Focus is on columns (position 0)
columns = main_pile.contents[0][0]
if columns.focus_position == 0:
# Left box (Tool Info)
self._current_section = 0
else:
# Right pile
right_pile = columns.contents[1][0]
if right_pile.focus_position == 0:
# Args section
self._current_section = 1
else:
# Steps section
self._current_section = 2
def _update_section_titles(self):
"""Update section titles to highlight the current one with markers."""
# Section 0 = Tool Info, Section 1 = Arguments, Section 2 = Steps, Section 3 = buttons
if self._current_section == 0:
self.left_box.set_title('[ Tool Info ]')
self.args_box.set_title('Arguments')
self.steps_box.set_title('Execution Steps')
elif self._current_section == 1:
self.left_box.set_title('Tool Info')
self.args_box.set_title('[ Arguments ]')
self.steps_box.set_title('Execution Steps')
elif self._current_section == 2:
self.left_box.set_title('Tool Info')
self.args_box.set_title('Arguments')
self.steps_box.set_title('[ Execution Steps ]')
else:
# Buttons focused - no section highlighted
self.left_box.set_title('Tool Info')
self.args_box.set_title('Arguments')
self.steps_box.set_title('Execution Steps')
def _focus_section(self, section_idx):
"""Set focus to the specified section."""
# Get the main pile
main_pile = self._w
if section_idx == 0:
# Tool Info (left box) - focus columns, then left
main_pile.focus_position = 0 # columns
columns = main_pile.contents[0][0]
columns.focus_position = 0 # left box
elif section_idx == 1:
# Arguments section - focus columns, then right, then args
main_pile.focus_position = 0 # columns
columns = main_pile.contents[0][0]
columns.focus_position = 1 # right pile
right_pile = columns.contents[1][0]
right_pile.focus_position = 0 # args section
elif section_idx == 2:
# Steps section - focus columns, then right, then steps
main_pile.focus_position = 0 # columns
columns = main_pile.contents[0][0]
columns.focus_position = 1 # right pile
right_pile = columns.contents[1][0]
right_pile.focus_position = 2 # steps section (after divider)
elif section_idx == 3:
# Save/Cancel buttons
main_pile.focus_position = 2 # bottom buttons (after divider)
class Dialog(urwid.WidgetWrap):
"""A dialog box overlay with 3D-style buttons."""
def __init__(self, title, body, buttons, width=60, height=None):
# Title
title_widget = urwid.Text(('header', f' {title} '), align='center')
# Buttons row - use 3D compact buttons for dialog actions
button_widgets = []
for label, callback in buttons:
btn = Button3DCompact(label, callback)
button_widgets.append(btn)
buttons_row = urwid.Columns([('pack', b) for b in button_widgets], dividechars=2)
buttons_centered = urwid.Padding(buttons_row, align='center', width='pack')
# Check if body is a box widget
# ListBox is always a box widget. For Piles with weighted items,
# check if it ONLY supports BOX sizing (not FLOW).
is_box_widget = isinstance(body, (urwid.ListBox, urwid.Scrollable, urwid.ScrollBar))
if not is_box_widget:
try:
sizing = body.sizing()
# Box widget if it ONLY supports BOX sizing
is_box_widget = sizing == frozenset({urwid.Sizing.BOX})
except (AttributeError, TypeError):
pass
if is_box_widget:
# Box widget - use directly with weight
pile = urwid.Pile([
('pack', title_widget),
('pack', urwid.Divider('─')),
('weight', 1, body),
('pack', urwid.Divider('─')),
('pack', buttons_centered),
])
else:
# Flow widget - wrap in Filler
body_padded = urwid.Padding(body, left=1, right=1)
body_filled = urwid.Filler(body_padded, valign='top')
pile = urwid.Pile([
('pack', title_widget),
('pack', urwid.Divider('─')),
body_filled,
('pack', urwid.Divider('─')),
('pack', buttons_centered),
])
# Box it
box = urwid.LineBox(pile, title='', title_align='center')
box = urwid.AttrMap(box, 'dialog')
super().__init__(box)