From 541d49b7f902d259cc069aac8d702d6003788983 Mon Sep 17 00:00:00 2001 From: rob Date: Fri, 5 Dec 2025 04:33:58 -0400 Subject: [PATCH] feat: Add tool categories for organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 12 ++++++ src/smarttools/tool.py | 18 +++++++-- src/smarttools/ui_urwid.py | 75 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 97 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4ca7ce1..83f8eda 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ A tool is a YAML config file in `~/.smarttools//config.yaml`: ```yaml name: summarize description: Condense documents to key points +category: Text # Optional: Text, Developer, Data, or Other arguments: - flag: --length variable: length @@ -219,6 +220,17 @@ steps: 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 Chain AI prompts with Python code for powerful workflows: diff --git a/src/smarttools/tool.py b/src/smarttools/tool.py index 68148a8..f8cc0cc 100644 --- a/src/smarttools/tool.py +++ b/src/smarttools/tool.py @@ -100,11 +100,16 @@ class CodeStep: Step = PromptStep | CodeStep +# Default categories for organizing tools +DEFAULT_CATEGORIES = ["Text", "Developer", "Data", "Other"] + + @dataclass class Tool: """A SmartTools tool definition.""" name: str description: str = "" + category: str = "Other" # Tool category for organization arguments: List[ToolArgument] = field(default_factory=list) steps: List[Step] = field(default_factory=list) output: str = "{input}" # Output template @@ -125,19 +130,24 @@ class Tool: return cls( name=data["name"], description=data.get("description", ""), + category=data.get("category", "Other"), arguments=arguments, steps=steps, output=data.get("output", "{input}") ) def to_dict(self) -> dict: - return { + d = { "name": self.name, "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]: """Get all variables available for use in templates.""" diff --git a/src/smarttools/ui_urwid.py b/src/smarttools/ui_urwid.py index 7c36783..c27dee2 100644 --- a/src/smarttools/ui_urwid.py +++ b/src/smarttools/ui_urwid.py @@ -863,14 +863,41 @@ class SmartToolsUI: def _refresh_main_menu(self): """Refresh the main menu display.""" + from collections import defaultdict + from .tool import DEFAULT_CATEGORIES + tools = list_tools() self._tools_list = tools - # Tool list - arrows navigate within, doesn't pass focus out - tool_items = [] + # Group tools by category + tools_by_category = defaultdict(list) for name in tools: - item = SelectableToolItem(name, on_select=self._on_tool_select) - tool_items.append(item) + 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) + tool_items.append(item) if not tools: tool_items.append(urwid.Text(('label', " (no tools - click Create to add one) "))) @@ -1089,6 +1116,8 @@ class SmartToolsUI: def _show_tool_builder(self): """Render the tool builder screen.""" + from .tool import DEFAULT_CATEGORIES + tool = self._current_tool # Create edit widgets @@ -1101,12 +1130,50 @@ class SmartToolsUI: 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') + # 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_pile = urwid.Pile([ ('pack', name_widget), ('pack', urwid.Divider()), ('pack', self._desc_edit), ('pack', urwid.Divider()), + ('pack', category_row), + ('pack', urwid.Divider()), ('pack', self._output_edit), ]) left_box = urwid.LineBox(urwid.Filler(left_pile, valign='top'), title='Tool Info')