mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
feat: Add native todo management to Hanzo REPL
- Add /todo command with full todo management capabilities - Quick add with tags, priority, and due dates: /todo Buy milk #shopping !high @tomorrow - Comprehensive todo operations: add, list, done, start, cancel, delete, view, stats - Persistent storage in ~/.hanzo/todos.json - Rich display with tables and emoji status indicators - Advanced filtering by status, priority, and tags - Statistics dashboard with completion rates - Shortcut /t for quick access
This commit is contained in:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hanzo-agents"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "Production-grade AI agent runtime for deterministic, debuggable agent systems"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hanzo-mcp"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
description = "The Zen of Hanzo MCP: One server to rule them all. The ultimate MCP that orchestrates all others."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "hanzo"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
description = "Hanzo AI - Complete AI Infrastructure Platform with CLI, Router, MCP, and Agent Runtime"
|
||||
authors = [
|
||||
{name = "Hanzo AI", email = "[email protected]"},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Hanzo - Complete AI Infrastructure Platform with CLI, Router, MCP, and Agent Runtime."""
|
||||
|
||||
__version__ = "0.3.27"
|
||||
__version__ = "0.3.28"
|
||||
__all__ = ["main", "cli", "__version__"]
|
||||
|
||||
from .cli import cli, main
|
||||
|
||||
@@ -32,6 +32,11 @@ except ImportError:
|
||||
QuickModelSelector = None
|
||||
BackgroundTaskManager = None
|
||||
|
||||
try:
|
||||
from .todo_manager import TodoManager
|
||||
except ImportError:
|
||||
TodoManager = None
|
||||
|
||||
|
||||
class EnhancedHanzoREPL:
|
||||
"""Enhanced REPL with model selection and authentication."""
|
||||
@@ -99,6 +104,9 @@ class EnhancedHanzoREPL:
|
||||
# Initialize background task manager
|
||||
self.task_manager = BackgroundTaskManager(console) if BackgroundTaskManager else None
|
||||
|
||||
# Initialize todo manager
|
||||
self.todo_manager = TodoManager(console) if TodoManager else None
|
||||
|
||||
# Detect available tools and set default
|
||||
if self.tool_detector:
|
||||
self.detected_tools = self.tool_detector.detect_all()
|
||||
@@ -139,6 +147,8 @@ class EnhancedHanzoREPL:
|
||||
"tasks": self.show_tasks,
|
||||
"kill": self.kill_task,
|
||||
"quick": self.quick_model_select,
|
||||
"todo": self.manage_todos,
|
||||
"todos": self.manage_todos, # Alias
|
||||
}
|
||||
|
||||
self.running = False
|
||||
@@ -295,6 +305,9 @@ class EnhancedHanzoREPL:
|
||||
"models": "models",
|
||||
"login": "login",
|
||||
"logout": "logout",
|
||||
"todo": "todo",
|
||||
"todos": "todos",
|
||||
"t": "todo", # Shortcut for todo
|
||||
}
|
||||
|
||||
mapped_cmd = slash_map.get(cmd, cmd)
|
||||
@@ -550,6 +563,7 @@ class EnhancedHanzoREPL:
|
||||
# Hanzo Enhanced REPL
|
||||
|
||||
## Slash Commands:
|
||||
- `/todo [cmd]` - Manage todos (see `/todo help`)
|
||||
- `/model [name]` - Change AI model (or `/m`)
|
||||
- `/models` - List available models
|
||||
- `/tools` - List available AI tools
|
||||
@@ -685,4 +699,215 @@ class EnhancedHanzoREPL:
|
||||
else:
|
||||
self.task_manager.kill_task(task_id)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
||||
pass
|
||||
|
||||
async def manage_todos(self, args: str = ""):
|
||||
"""Manage todos."""
|
||||
if not self.todo_manager:
|
||||
self.console.print("[yellow]Todo manager not available[/yellow]")
|
||||
return
|
||||
|
||||
# Parse command
|
||||
parts = args.strip().split(maxsplit=1)
|
||||
|
||||
if not parts:
|
||||
# Show todos
|
||||
self.todo_manager.display_todos()
|
||||
return
|
||||
|
||||
subcommand = parts[0].lower()
|
||||
rest = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
# Handle subcommands
|
||||
if subcommand in ["add", "a", "+"]:
|
||||
# Add todo
|
||||
if rest:
|
||||
# Quick add
|
||||
try:
|
||||
todo = self.todo_manager.quick_add(rest)
|
||||
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
|
||||
except ValueError as e:
|
||||
self.console.print(f"[red]Error: {e}[/red]")
|
||||
else:
|
||||
# Interactive add
|
||||
await self.add_todo_interactive()
|
||||
|
||||
elif subcommand in ["list", "ls", "l"]:
|
||||
# List todos with optional filter
|
||||
filter_parts = rest.split()
|
||||
status = None
|
||||
priority = None
|
||||
tag = None
|
||||
|
||||
for i in range(0, len(filter_parts), 2):
|
||||
if i + 1 < len(filter_parts):
|
||||
key = filter_parts[i]
|
||||
value = filter_parts[i + 1]
|
||||
|
||||
if key in ["status", "s"]:
|
||||
status = value
|
||||
elif key in ["priority", "p"]:
|
||||
priority = value
|
||||
elif key in ["tag", "t"]:
|
||||
tag = value
|
||||
|
||||
todos = self.todo_manager.list_todos(status=status, priority=priority, tag=tag)
|
||||
title = "Filtered Todos" if (status or priority or tag) else "All Todos"
|
||||
self.todo_manager.display_todos(todos, title)
|
||||
|
||||
elif subcommand in ["done", "d", "complete", "finish"]:
|
||||
# Mark as done
|
||||
if rest:
|
||||
todo = self.todo_manager.update_todo(rest, status="done")
|
||||
if todo:
|
||||
self.console.print(f"[green]✅ Marked as done: {todo.title}[/green]")
|
||||
else:
|
||||
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
||||
else:
|
||||
self.console.print("[yellow]Usage: /todo done <id>[/yellow]")
|
||||
|
||||
elif subcommand in ["start", "begin", "progress"]:
|
||||
# Mark as in progress
|
||||
if rest:
|
||||
todo = self.todo_manager.update_todo(rest, status="in_progress")
|
||||
if todo:
|
||||
self.console.print(f"[cyan]🔄 Started: {todo.title}[/cyan]")
|
||||
else:
|
||||
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
||||
else:
|
||||
self.console.print("[yellow]Usage: /todo start <id>[/yellow]")
|
||||
|
||||
elif subcommand in ["cancel", "x"]:
|
||||
# Cancel todo
|
||||
if rest:
|
||||
todo = self.todo_manager.update_todo(rest, status="cancelled")
|
||||
if todo:
|
||||
self.console.print(f"[red]❌ Cancelled: {todo.title}[/red]")
|
||||
else:
|
||||
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
||||
else:
|
||||
self.console.print("[yellow]Usage: /todo cancel <id>[/yellow]")
|
||||
|
||||
elif subcommand in ["delete", "del", "rm", "remove"]:
|
||||
# Delete todo
|
||||
if rest:
|
||||
if self.todo_manager.delete_todo(rest):
|
||||
self.console.print(f"[green]✅ Deleted todo: {rest}[/green]")
|
||||
else:
|
||||
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
||||
else:
|
||||
self.console.print("[yellow]Usage: /todo delete <id>[/yellow]")
|
||||
|
||||
elif subcommand in ["view", "show", "detail"]:
|
||||
# View todo detail
|
||||
if rest:
|
||||
todo = self.todo_manager.get_todo(rest)
|
||||
if todo:
|
||||
self.todo_manager.display_todo_detail(todo)
|
||||
else:
|
||||
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
||||
else:
|
||||
self.console.print("[yellow]Usage: /todo view <id>[/yellow]")
|
||||
|
||||
elif subcommand in ["stats", "statistics"]:
|
||||
# Show statistics
|
||||
self.todo_manager.display_statistics()
|
||||
|
||||
elif subcommand in ["clear", "reset"]:
|
||||
# Clear all todos (with confirmation)
|
||||
try:
|
||||
confirm = await self.session.prompt_async("Are you sure you want to delete ALL todos? (yes/no): ")
|
||||
if confirm.lower() in ["yes", "y"]:
|
||||
self.todo_manager.todos = []
|
||||
self.todo_manager.save_todos()
|
||||
self.console.print("[green]✅ All todos cleared[/green]")
|
||||
else:
|
||||
self.console.print("[yellow]Cancelled[/yellow]")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
self.console.print("[yellow]Cancelled[/yellow]")
|
||||
|
||||
elif subcommand in ["help", "h", "?"]:
|
||||
# Show todo help
|
||||
self.show_todo_help()
|
||||
|
||||
else:
|
||||
# Unknown subcommand, treat as quick add
|
||||
try:
|
||||
todo = self.todo_manager.quick_add(args)
|
||||
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
|
||||
except ValueError:
|
||||
self.console.print(f"[yellow]Unknown todo command: {subcommand}[/yellow]")
|
||||
self.console.print("[dim]Use /todo help for available commands[/dim]")
|
||||
|
||||
async def add_todo_interactive(self):
|
||||
"""Add todo interactively."""
|
||||
try:
|
||||
# Get title
|
||||
title = await self.session.prompt_async("Title: ")
|
||||
if not title:
|
||||
self.console.print("[yellow]Cancelled[/yellow]")
|
||||
return
|
||||
|
||||
# Get description
|
||||
description = await self.session.prompt_async("Description (optional): ")
|
||||
|
||||
# Get priority
|
||||
priority = await self.session.prompt_async("Priority (low/medium/high/urgent) [medium]: ")
|
||||
if not priority:
|
||||
priority = "medium"
|
||||
|
||||
# Get tags
|
||||
tags_input = await self.session.prompt_async("Tags (comma-separated, optional): ")
|
||||
tags = [t.strip() for t in tags_input.split(",") if t.strip()] if tags_input else []
|
||||
|
||||
# Get due date
|
||||
due_date = await self.session.prompt_async("Due date (optional): ")
|
||||
|
||||
# Add todo
|
||||
todo = self.todo_manager.add_todo(
|
||||
title=title,
|
||||
description=description,
|
||||
priority=priority,
|
||||
tags=tags,
|
||||
due_date=due_date if due_date else None
|
||||
)
|
||||
|
||||
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
|
||||
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
self.console.print("[yellow]Cancelled[/yellow]")
|
||||
|
||||
def show_todo_help(self):
|
||||
"""Show todo help."""
|
||||
help_text = """
|
||||
[bold cyan]Todo Management[/bold cyan]
|
||||
|
||||
[bold]Quick Add:[/bold]
|
||||
/todo Buy milk #shopping !high @tomorrow
|
||||
Format: title #tag1 #tag2 !priority @due_date
|
||||
|
||||
[bold]Commands:[/bold]
|
||||
/todo - List all todos
|
||||
/todo add <text> - Quick add todo
|
||||
/todo list [filters] - List with filters
|
||||
/todo done <id> - Mark as done
|
||||
/todo start <id> - Mark as in progress
|
||||
/todo cancel <id> - Cancel todo
|
||||
/todo delete <id> - Delete todo
|
||||
/todo view <id> - View todo details
|
||||
/todo stats - Show statistics
|
||||
/todo clear - Clear all todos
|
||||
/todo help - Show this help
|
||||
|
||||
[bold]List Filters:[/bold]
|
||||
/todo list status todo
|
||||
/todo list priority high
|
||||
/todo list tag work
|
||||
|
||||
[bold]Shortcuts:[/bold]
|
||||
/todo a = add
|
||||
/todo ls = list
|
||||
/todo d = done
|
||||
/todo rm = delete
|
||||
"""
|
||||
self.console.print(help_text)
|
||||
@@ -0,0 +1,457 @@
|
||||
"""Native todo management for Hanzo REPL."""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Any
|
||||
from enum import Enum
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
from rich.prompt import Prompt, Confirm
|
||||
|
||||
|
||||
class TodoPriority(Enum):
|
||||
"""Todo priority levels."""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
URGENT = "urgent"
|
||||
|
||||
|
||||
class TodoStatus(Enum):
|
||||
"""Todo status."""
|
||||
TODO = "todo"
|
||||
IN_PROGRESS = "in_progress"
|
||||
DONE = "done"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class Todo:
|
||||
"""Single todo item."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
description: str = "",
|
||||
priority: TodoPriority = TodoPriority.MEDIUM,
|
||||
status: TodoStatus = TodoStatus.TODO,
|
||||
tags: List[str] = None,
|
||||
due_date: Optional[str] = None,
|
||||
id: Optional[str] = None,
|
||||
created_at: Optional[str] = None,
|
||||
updated_at: Optional[str] = None,
|
||||
completed_at: Optional[str] = None
|
||||
):
|
||||
self.id = id or str(uuid.uuid4())[:8]
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.priority = priority
|
||||
self.status = status
|
||||
self.tags = tags or []
|
||||
self.due_date = due_date
|
||||
self.created_at = created_at or datetime.now().isoformat()
|
||||
self.updated_at = updated_at or datetime.now().isoformat()
|
||||
self.completed_at = completed_at
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"title": self.title,
|
||||
"description": self.description,
|
||||
"priority": self.priority.value,
|
||||
"status": self.status.value,
|
||||
"tags": self.tags,
|
||||
"due_date": self.due_date,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"completed_at": self.completed_at
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "Todo":
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
id=data.get("id"),
|
||||
title=data["title"],
|
||||
description=data.get("description", ""),
|
||||
priority=TodoPriority(data.get("priority", "medium")),
|
||||
status=TodoStatus(data.get("status", "todo")),
|
||||
tags=data.get("tags", []),
|
||||
due_date=data.get("due_date"),
|
||||
created_at=data.get("created_at"),
|
||||
updated_at=data.get("updated_at"),
|
||||
completed_at=data.get("completed_at")
|
||||
)
|
||||
|
||||
|
||||
class TodoManager:
|
||||
"""Manage todos with persistent storage."""
|
||||
|
||||
def __init__(self, console: Optional[Console] = None):
|
||||
self.console = console or Console()
|
||||
self.config_dir = Path.home() / ".hanzo"
|
||||
self.todos_file = self.config_dir / "todos.json"
|
||||
self.todos: List[Todo] = []
|
||||
self.load_todos()
|
||||
|
||||
def load_todos(self):
|
||||
"""Load todos from file."""
|
||||
if self.todos_file.exists():
|
||||
try:
|
||||
data = json.loads(self.todos_file.read_text())
|
||||
self.todos = [Todo.from_dict(t) for t in data.get("todos", [])]
|
||||
except Exception as e:
|
||||
self.console.print(f"[red]Error loading todos: {e}[/red]")
|
||||
self.todos = []
|
||||
else:
|
||||
self.todos = []
|
||||
|
||||
def save_todos(self):
|
||||
"""Save todos to file."""
|
||||
self.config_dir.mkdir(exist_ok=True)
|
||||
data = {
|
||||
"todos": [t.to_dict() for t in self.todos],
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
self.todos_file.write_text(json.dumps(data, indent=2))
|
||||
|
||||
def add_todo(
|
||||
self,
|
||||
title: str,
|
||||
description: str = "",
|
||||
priority: str = "medium",
|
||||
tags: List[str] = None,
|
||||
due_date: Optional[str] = None
|
||||
) -> Todo:
|
||||
"""Add a new todo."""
|
||||
try:
|
||||
priority_enum = TodoPriority(priority.lower())
|
||||
except ValueError:
|
||||
priority_enum = TodoPriority.MEDIUM
|
||||
|
||||
todo = Todo(
|
||||
title=title,
|
||||
description=description,
|
||||
priority=priority_enum,
|
||||
tags=tags or [],
|
||||
due_date=due_date
|
||||
)
|
||||
|
||||
self.todos.append(todo)
|
||||
self.save_todos()
|
||||
|
||||
return todo
|
||||
|
||||
def update_todo(self, todo_id: str, **kwargs) -> Optional[Todo]:
|
||||
"""Update a todo."""
|
||||
todo = self.get_todo(todo_id)
|
||||
if not todo:
|
||||
return None
|
||||
|
||||
# Update fields
|
||||
if "title" in kwargs:
|
||||
todo.title = kwargs["title"]
|
||||
if "description" in kwargs:
|
||||
todo.description = kwargs["description"]
|
||||
if "priority" in kwargs:
|
||||
try:
|
||||
todo.priority = TodoPriority(kwargs["priority"].lower())
|
||||
except ValueError:
|
||||
pass
|
||||
if "status" in kwargs:
|
||||
try:
|
||||
new_status = TodoStatus(kwargs["status"].lower())
|
||||
todo.status = new_status
|
||||
|
||||
# Update completed timestamp
|
||||
if new_status == TodoStatus.DONE:
|
||||
todo.completed_at = datetime.now().isoformat()
|
||||
elif todo.status == TodoStatus.DONE and new_status != TodoStatus.DONE:
|
||||
todo.completed_at = None
|
||||
except ValueError:
|
||||
pass
|
||||
if "tags" in kwargs:
|
||||
todo.tags = kwargs["tags"]
|
||||
if "due_date" in kwargs:
|
||||
todo.due_date = kwargs["due_date"]
|
||||
|
||||
todo.updated_at = datetime.now().isoformat()
|
||||
self.save_todos()
|
||||
|
||||
return todo
|
||||
|
||||
def delete_todo(self, todo_id: str) -> bool:
|
||||
"""Delete a todo."""
|
||||
todo = self.get_todo(todo_id)
|
||||
if todo:
|
||||
self.todos.remove(todo)
|
||||
self.save_todos()
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_todo(self, todo_id: str) -> Optional[Todo]:
|
||||
"""Get a todo by ID."""
|
||||
for todo in self.todos:
|
||||
if todo.id == todo_id:
|
||||
return todo
|
||||
return None
|
||||
|
||||
def list_todos(
|
||||
self,
|
||||
status: Optional[str] = None,
|
||||
priority: Optional[str] = None,
|
||||
tag: Optional[str] = None
|
||||
) -> List[Todo]:
|
||||
"""List todos with optional filters."""
|
||||
filtered = self.todos
|
||||
|
||||
# Filter by status
|
||||
if status:
|
||||
try:
|
||||
status_enum = TodoStatus(status.lower())
|
||||
filtered = [t for t in filtered if t.status == status_enum]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by priority
|
||||
if priority:
|
||||
try:
|
||||
priority_enum = TodoPriority(priority.lower())
|
||||
filtered = [t for t in filtered if t.priority == priority_enum]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by tag
|
||||
if tag:
|
||||
filtered = [t for t in filtered if tag in t.tags]
|
||||
|
||||
# Sort by priority and status
|
||||
priority_order = {
|
||||
TodoPriority.URGENT: 0,
|
||||
TodoPriority.HIGH: 1,
|
||||
TodoPriority.MEDIUM: 2,
|
||||
TodoPriority.LOW: 3
|
||||
}
|
||||
|
||||
status_order = {
|
||||
TodoStatus.IN_PROGRESS: 0,
|
||||
TodoStatus.TODO: 1,
|
||||
TodoStatus.DONE: 2,
|
||||
TodoStatus.CANCELLED: 3
|
||||
}
|
||||
|
||||
filtered.sort(key=lambda t: (
|
||||
status_order.get(t.status, 999),
|
||||
priority_order.get(t.priority, 999),
|
||||
t.created_at
|
||||
))
|
||||
|
||||
return filtered
|
||||
|
||||
def display_todos(
|
||||
self,
|
||||
todos: Optional[List[Todo]] = None,
|
||||
title: str = "Todos"
|
||||
):
|
||||
"""Display todos in a nice table."""
|
||||
if todos is None:
|
||||
todos = self.list_todos()
|
||||
|
||||
if not todos:
|
||||
self.console.print("[yellow]No todos found[/yellow]")
|
||||
return
|
||||
|
||||
# Create table
|
||||
table = Table(title=title, box=box.ROUNDED)
|
||||
table.add_column("ID", style="cyan", width=8)
|
||||
table.add_column("Status", width=12)
|
||||
table.add_column("Priority", width=8)
|
||||
table.add_column("Title", style="white")
|
||||
table.add_column("Tags", style="dim")
|
||||
table.add_column("Due", style="yellow")
|
||||
|
||||
for todo in todos:
|
||||
# Status emoji
|
||||
status_display = {
|
||||
TodoStatus.TODO: "⭕ Todo",
|
||||
TodoStatus.IN_PROGRESS: "🔄 In Progress",
|
||||
TodoStatus.DONE: "✅ Done",
|
||||
TodoStatus.CANCELLED: "❌ Cancelled"
|
||||
}.get(todo.status, todo.status.value)
|
||||
|
||||
# Priority color
|
||||
priority_color = {
|
||||
TodoPriority.URGENT: "red bold",
|
||||
TodoPriority.HIGH: "red",
|
||||
TodoPriority.MEDIUM: "yellow",
|
||||
TodoPriority.LOW: "green"
|
||||
}.get(todo.priority, "white")
|
||||
|
||||
priority_display = f"[{priority_color}]{todo.priority.value.upper()}[/{priority_color}]"
|
||||
|
||||
# Tags
|
||||
tags_display = ", ".join(todo.tags) if todo.tags else "-"
|
||||
|
||||
# Due date
|
||||
due_display = todo.due_date if todo.due_date else "-"
|
||||
|
||||
table.add_row(
|
||||
todo.id,
|
||||
status_display,
|
||||
priority_display,
|
||||
todo.title,
|
||||
tags_display,
|
||||
due_display
|
||||
)
|
||||
|
||||
self.console.print(table)
|
||||
|
||||
# Summary
|
||||
total = len(todos)
|
||||
done = len([t for t in todos if t.status == TodoStatus.DONE])
|
||||
in_progress = len([t for t in todos if t.status == TodoStatus.IN_PROGRESS])
|
||||
todo_count = len([t for t in todos if t.status == TodoStatus.TODO])
|
||||
|
||||
summary = f"Total: {total} | Todo: {todo_count} | In Progress: {in_progress} | Done: {done}"
|
||||
self.console.print(f"\n[dim]{summary}[/dim]")
|
||||
|
||||
def display_todo_detail(self, todo: Todo):
|
||||
"""Display detailed view of a todo."""
|
||||
# Status color
|
||||
status_color = {
|
||||
TodoStatus.TODO: "yellow",
|
||||
TodoStatus.IN_PROGRESS: "cyan",
|
||||
TodoStatus.DONE: "green",
|
||||
TodoStatus.CANCELLED: "red"
|
||||
}.get(todo.status, "white")
|
||||
|
||||
# Priority color
|
||||
priority_color = {
|
||||
TodoPriority.URGENT: "red bold",
|
||||
TodoPriority.HIGH: "red",
|
||||
TodoPriority.MEDIUM: "yellow",
|
||||
TodoPriority.LOW: "green"
|
||||
}.get(todo.priority, "white")
|
||||
|
||||
# Build content
|
||||
content = f"""
|
||||
[bold]{todo.title}[/bold]
|
||||
|
||||
[dim]ID:[/dim] {todo.id}
|
||||
[dim]Status:[/dim] [{status_color}]{todo.status.value.replace('_', ' ').title()}[/{status_color}]
|
||||
[dim]Priority:[/dim] [{priority_color}]{todo.priority.value.upper()}[/{priority_color}]
|
||||
[dim]Tags:[/dim] {', '.join(todo.tags) if todo.tags else 'None'}
|
||||
[dim]Due Date:[/dim] {todo.due_date if todo.due_date else 'Not set'}
|
||||
|
||||
[dim]Description:[/dim]
|
||||
{todo.description if todo.description else 'No description'}
|
||||
|
||||
[dim]Created:[/dim] {todo.created_at}
|
||||
[dim]Updated:[/dim] {todo.updated_at}
|
||||
[dim]Completed:[/dim] {todo.completed_at if todo.completed_at else 'Not completed'}
|
||||
"""
|
||||
|
||||
panel = Panel(content.strip(), title=f"Todo: {todo.title}", box=box.ROUNDED)
|
||||
self.console.print(panel)
|
||||
|
||||
def quick_add(self, text: str) -> Todo:
|
||||
"""Quick add todo from text.
|
||||
|
||||
Format: title #tag1 #tag2 !priority @due_date
|
||||
"""
|
||||
import re
|
||||
|
||||
# Extract tags (words starting with #)
|
||||
tags = re.findall(r'#(\w+)', text)
|
||||
text = re.sub(r'#\w+', '', text)
|
||||
|
||||
# Extract priority (word after !)
|
||||
priority_match = re.search(r'!(\w+)', text)
|
||||
priority = priority_match.group(1) if priority_match else "medium"
|
||||
text = re.sub(r'!\w+', '', text)
|
||||
|
||||
# Extract due date (text after @)
|
||||
due_match = re.search(r'@([^\s]+)', text)
|
||||
due_date = due_match.group(1) if due_match else None
|
||||
text = re.sub(r'@[^\s]+', '', text)
|
||||
|
||||
# Clean up title
|
||||
title = text.strip()
|
||||
|
||||
if not title:
|
||||
raise ValueError("Todo title cannot be empty")
|
||||
|
||||
return self.add_todo(
|
||||
title=title,
|
||||
priority=priority,
|
||||
tags=tags,
|
||||
due_date=due_date
|
||||
)
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""Get todo statistics."""
|
||||
total = len(self.todos)
|
||||
|
||||
# Status counts
|
||||
status_counts = {}
|
||||
for status in TodoStatus:
|
||||
count = len([t for t in self.todos if t.status == status])
|
||||
status_counts[status.value] = count
|
||||
|
||||
# Priority counts
|
||||
priority_counts = {}
|
||||
for priority in TodoPriority:
|
||||
count = len([t for t in self.todos if t.priority == priority])
|
||||
priority_counts[priority.value] = count
|
||||
|
||||
# Tags
|
||||
all_tags = set()
|
||||
for todo in self.todos:
|
||||
all_tags.update(todo.tags)
|
||||
|
||||
# Completion rate
|
||||
done = status_counts.get("done", 0)
|
||||
completion_rate = (done / total * 100) if total > 0 else 0
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"status": status_counts,
|
||||
"priority": priority_counts,
|
||||
"tags": list(all_tags),
|
||||
"completion_rate": completion_rate
|
||||
}
|
||||
|
||||
def display_statistics(self):
|
||||
"""Display todo statistics."""
|
||||
stats = self.get_statistics()
|
||||
|
||||
# Create stats panel
|
||||
content = f"""
|
||||
[bold cyan]Todo Statistics[/bold cyan]
|
||||
|
||||
[bold]Total Todos:[/bold] {stats['total']}
|
||||
[bold]Completion Rate:[/bold] {stats['completion_rate']:.1f}%
|
||||
|
||||
[bold]By Status:[/bold]
|
||||
⭕ Todo: {stats['status'].get('todo', 0)}
|
||||
🔄 In Progress: {stats['status'].get('in_progress', 0)}
|
||||
✅ Done: {stats['status'].get('done', 0)}
|
||||
❌ Cancelled: {stats['status'].get('cancelled', 0)}
|
||||
|
||||
[bold]By Priority:[/bold]
|
||||
🔴 Urgent: {stats['priority'].get('urgent', 0)}
|
||||
🟠 High: {stats['priority'].get('high', 0)}
|
||||
🟡 Medium: {stats['priority'].get('medium', 0)}
|
||||
🟢 Low: {stats['priority'].get('low', 0)}
|
||||
|
||||
[bold]Tags:[/bold] {', '.join(stats['tags']) if stats['tags'] else 'None'}
|
||||
"""
|
||||
|
||||
panel = Panel(content.strip(), title="📊 Statistics", box=box.ROUNDED)
|
||||
self.console.print(panel)
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python
|
||||
"""Test the todo manager directly."""
|
||||
|
||||
import asyncio
|
||||
from rich.console import Console
|
||||
from pkg.hanzo.src.hanzo.interactive.todo_manager import TodoManager
|
||||
|
||||
async def test_todo():
|
||||
console = Console()
|
||||
manager = TodoManager(console)
|
||||
|
||||
# Add some todos
|
||||
print("Adding todos...")
|
||||
todo1 = manager.add_todo(
|
||||
title="Implement new feature",
|
||||
description="Add support for webhooks",
|
||||
priority="high",
|
||||
tags=["dev", "backend"],
|
||||
due_date="2025-01-15"
|
||||
)
|
||||
print(f"Added: {todo1.title} (ID: {todo1.id})")
|
||||
|
||||
todo2 = manager.quick_add("Fix bug in auth system #bug #urgent !urgent @today")
|
||||
print(f"Added: {todo2.title} (ID: {todo2.id})")
|
||||
|
||||
todo3 = manager.quick_add("Write documentation #docs !low")
|
||||
print(f"Added: {todo3.title} (ID: {todo3.id})")
|
||||
|
||||
# Display todos
|
||||
print("\nAll Todos:")
|
||||
manager.display_todos()
|
||||
|
||||
# Update a todo
|
||||
print(f"\nMarking {todo2.id} as in progress...")
|
||||
manager.update_todo(todo2.id, status="in_progress")
|
||||
|
||||
# Display filtered todos
|
||||
print("\nHigh priority todos:")
|
||||
high_priority = manager.list_todos(priority="high")
|
||||
manager.display_todos(high_priority, "High Priority")
|
||||
|
||||
# Show statistics
|
||||
print("\nStatistics:")
|
||||
manager.display_statistics()
|
||||
|
||||
# Show detail
|
||||
print(f"\nDetail for {todo1.id}:")
|
||||
manager.display_todo_detail(todo1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_todo())
|
||||
Reference in New Issue
Block a user