feat: Add tool categories for organization

- Add category field to Tool dataclass (Text, Developer, Data, Other)
- Display tools grouped by category in main UI with section headers
- Add category dropdown selector in tool builder dialog
- Update example tools with appropriate categories
- Document category feature in README

Categories help organize tools in the UI for easier navigation.
Tools without a category default to "Other".

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rob 2025-12-05 04:33:58 -04:00
parent cff3b674ac
commit 541d49b7f9
3 changed files with 97 additions and 8 deletions

View File

@ -204,6 +204,7 @@ A tool is a YAML config file in `~/.smarttools/<name>/config.yaml`:
```yaml ```yaml
name: summarize name: summarize
description: Condense documents to key points description: Condense documents to key points
category: Text # Optional: Text, Developer, Data, or Other
arguments: arguments:
- flag: --length - flag: --length
variable: length variable: length
@ -219,6 +220,17 @@ steps:
output: "{response}" output: "{response}"
``` ```
### Categories
Tools can be organized into categories for easier navigation in the UI:
| Category | Description |
|----------|-------------|
| `Text` | Text processing (summarize, translate, grammar) |
| `Developer` | Dev tools (explain-error, gen-tests, commit-msg) |
| `Data` | Data extraction and conversion (json-extract, csv) |
| `Other` | Default for uncategorized tools |
### Multi-Step Tools ### Multi-Step Tools
Chain AI prompts with Python code for powerful workflows: Chain AI prompts with Python code for powerful workflows:

View File

@ -100,11 +100,16 @@ class CodeStep:
Step = PromptStep | CodeStep Step = PromptStep | CodeStep
# Default categories for organizing tools
DEFAULT_CATEGORIES = ["Text", "Developer", "Data", "Other"]
@dataclass @dataclass
class Tool: class Tool:
"""A SmartTools tool definition.""" """A SmartTools tool definition."""
name: str name: str
description: str = "" description: str = ""
category: str = "Other" # Tool category for organization
arguments: List[ToolArgument] = field(default_factory=list) arguments: List[ToolArgument] = field(default_factory=list)
steps: List[Step] = field(default_factory=list) steps: List[Step] = field(default_factory=list)
output: str = "{input}" # Output template output: str = "{input}" # Output template
@ -125,19 +130,24 @@ class Tool:
return cls( return cls(
name=data["name"], name=data["name"],
description=data.get("description", ""), description=data.get("description", ""),
category=data.get("category", "Other"),
arguments=arguments, arguments=arguments,
steps=steps, steps=steps,
output=data.get("output", "{input}") output=data.get("output", "{input}")
) )
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { d = {
"name": self.name, "name": self.name,
"description": self.description, "description": self.description,
"arguments": [arg.to_dict() for arg in self.arguments],
"steps": [step.to_dict() for step in self.steps],
"output": self.output
} }
# Only include category if it's not the default
if self.category and self.category != "Other":
d["category"] = self.category
d["arguments"] = [arg.to_dict() for arg in self.arguments]
d["steps"] = [step.to_dict() for step in self.steps]
d["output"] = self.output
return d
def get_available_variables(self) -> List[str]: def get_available_variables(self) -> List[str]:
"""Get all variables available for use in templates.""" """Get all variables available for use in templates."""

View File

@ -863,12 +863,39 @@ class SmartToolsUI:
def _refresh_main_menu(self): def _refresh_main_menu(self):
"""Refresh the main menu display.""" """Refresh the main menu display."""
from collections import defaultdict
from .tool import DEFAULT_CATEGORIES
tools = list_tools() tools = list_tools()
self._tools_list = tools self._tools_list = tools
# Tool list - arrows navigate within, doesn't pass focus out # Group tools by category
tool_items = [] tools_by_category = defaultdict(list)
for name in tools: for name in tools:
tool = load_tool(name)
category = tool.category if tool else "Other"
tools_by_category[category].append(name)
# Build tool list with category headers
tool_items = []
# Show categories in defined order, then any custom ones
all_categories = list(DEFAULT_CATEGORIES)
for cat in tools_by_category:
if cat not in all_categories:
all_categories.append(cat)
for category in all_categories:
if category in tools_by_category and tools_by_category[category]:
# Category header (non-selectable)
header = urwid.AttrMap(
urwid.Text(f"─── {category} ───"),
'label'
)
tool_items.append(header)
# Tools in this category
for name in sorted(tools_by_category[category]):
item = SelectableToolItem(name, on_select=self._on_tool_select) item = SelectableToolItem(name, on_select=self._on_tool_select)
tool_items.append(item) tool_items.append(item)
@ -1089,6 +1116,8 @@ class SmartToolsUI:
def _show_tool_builder(self): def _show_tool_builder(self):
"""Render the tool builder screen.""" """Render the tool builder screen."""
from .tool import DEFAULT_CATEGORIES
tool = self._current_tool tool = self._current_tool
# Create edit widgets # Create edit widgets
@ -1101,12 +1130,50 @@ class SmartToolsUI:
self._desc_edit = urwid.AttrMap(urwid.Edit(('label', "Desc: "), tool.description), 'edit', 'edit_focus') self._desc_edit = urwid.AttrMap(urwid.Edit(('label', "Desc: "), tool.description), 'edit', 'edit_focus')
self._output_edit = urwid.AttrMap(urwid.Edit(('label', "Output: "), tool.output), 'edit', 'edit_focus') self._output_edit = urwid.AttrMap(urwid.Edit(('label', "Output: "), tool.output), 'edit', 'edit_focus')
# Category selector
self._selected_category = [tool.category or "Other"]
category_btn_text = urwid.Text(self._selected_category[0])
category_btn = urwid.AttrMap(
urwid.Padding(category_btn_text, left=1, right=1),
'edit', 'edit_focus'
)
def show_category_dropdown(_):
"""Show category selection popup."""
def select_category(cat):
def callback(_):
self._selected_category[0] = cat
category_btn_text.set_text(cat)
tool.category = cat
self.close_overlay()
return callback
items = []
for cat in DEFAULT_CATEGORIES:
btn = urwid.Button(cat, on_press=select_category(cat))
items.append(urwid.AttrMap(btn, 'button', 'button_focus'))
listbox = urwid.ListBox(urwid.SimpleFocusListWalker(items))
popup = Dialog("Select Category", listbox, [])
self.show_overlay(popup, width=30, height=len(DEFAULT_CATEGORIES) + 4)
category_select_btn = Button3DCompact("", on_press=show_category_dropdown)
category_row = urwid.Columns([
('pack', urwid.Text(('label', "Category: "))),
('weight', 1, category_btn),
('pack', urwid.Text(" ")),
('pack', category_select_btn),
])
# Left column - fields # Left column - fields
left_pile = urwid.Pile([ left_pile = urwid.Pile([
('pack', name_widget), ('pack', name_widget),
('pack', urwid.Divider()), ('pack', urwid.Divider()),
('pack', self._desc_edit), ('pack', self._desc_edit),
('pack', urwid.Divider()), ('pack', urwid.Divider()),
('pack', category_row),
('pack', urwid.Divider()),
('pack', self._output_edit), ('pack', self._output_edit),
]) ])
left_box = urwid.LineBox(urwid.Filler(left_pile, valign='top'), title='Tool Info') left_box = urwid.LineBox(urwid.Filler(left_pile, valign='top'), title='Tool Info')