Files
strix/strix/interface/tool_components/todo_renderer.py
T
0xallam 37c8cffbe3 feat(tools): add bulk operations support to todo tools
- update_todo: add `updates` param for bulk updates in one call
- mark_todo_done: add `todo_ids` param to mark multiple todos done
- mark_todo_pending: add `todo_ids` param to mark multiple pending
- delete_todo: add `todo_ids` param to delete multiple todos
- Increase todo renderer display limit from 10 to 25
- Maintains backward compatibility with single-ID usage
- Update prompts to keep todos short-horizon and dynamic
2025-12-14 20:31:33 -08:00

205 lines
6.9 KiB
Python

from typing import Any, ClassVar
from textual.widgets import Static
from .base_renderer import BaseToolRenderer
from .registry import register_tool_renderer
STATUS_MARKERS = {
"pending": "[ ]",
"in_progress": "[~]",
"done": "[•]",
}
def _truncate(text: str, length: int = 80) -> str:
if len(text) <= length:
return text
return text[: length - 3] + "..."
def _format_todo_lines(
cls: type[BaseToolRenderer], result: dict[str, Any], limit: int = 25
) -> list[str]:
todos = result.get("todos")
if not isinstance(todos, list) or not todos:
return [" [dim]No todos[/]"]
lines: list[str] = []
total = len(todos)
for index, todo in enumerate(todos):
if index >= limit:
remaining = total - limit
if remaining > 0:
lines.append(f" [dim]... +{remaining} more[/]")
break
status = todo.get("status", "pending")
marker = STATUS_MARKERS.get(status, STATUS_MARKERS["pending"])
title = todo.get("title", "").strip() or "(untitled)"
title = cls.escape_markup(_truncate(title, 90))
if status == "done":
title_markup = f"[dim strike]{title}[/]"
elif status == "in_progress":
title_markup = f"[italic]{title}[/]"
else:
title_markup = title
lines.append(f" {marker} {title_markup}")
return lines
@register_tool_renderer
class CreateTodoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_todo"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
header = "📋 [bold #a78bfa]Todo[/]"
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to create todo")
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
else:
content_text = f"{header}\n [dim]Creating...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class ListTodosRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "list_todos"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
header = "📋 [bold #a78bfa]Todos[/]"
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Unable to list todos")
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
else:
content_text = f"{header}\n [dim]Loading...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class UpdateTodoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "update_todo"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
header = "📋 [bold #a78bfa]Todo Updated[/]"
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to update todo")
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
else:
content_text = f"{header}\n [dim]Updating...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class MarkTodoDoneRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "mark_todo_done"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
header = "📋 [bold #a78bfa]Todo Completed[/]"
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to mark todo done")
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
else:
content_text = f"{header}\n [dim]Marking done...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class MarkTodoPendingRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "mark_todo_pending"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
header = "📋 [bold #f59e0b]Todo Reopened[/]"
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to reopen todo")
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
else:
content_text = f"{header}\n [dim]Reopening...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)
@register_tool_renderer
class DeleteTodoRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "delete_todo"
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static:
result = tool_data.get("result")
header = "📋 [bold #94a3b8]Todo Removed[/]"
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to remove todo")
content_text = f"{header}\n [#ef4444]{cls.escape_markup(error)}[/]"
else:
content_text = f"{header}\n [dim]Removing...[/]"
css_classes = cls.get_css_classes("completed")
return Static(content_text, classes=css_classes)