Newer
Older
navi-1 / navi / tools / todo.py
"""Session-scoped task plan manager."""
from __future__ import annotations

from dataclasses import dataclass

from .base import Tool, ToolResult, current_session_id

_STATUS_ICON: dict[str, str] = {
    "pending":     "○",
    "in_progress": "◎",
    "done":        "✓",
    "failed":      "✗",
    "skipped":     "—",
}

@dataclass
class _Task:
    text: str
    status: str = "pending"


# In-memory store: session_id → task list (ephemeral, lives with the server process)
_plans: dict[str, list[_Task]] = {}


class TodoTool(Tool):
    name = "todo"
    description = (
        "Manage your current task plan for multi-step work. "
        "Use 'set' at the start of a complex task to record the steps, "
        "'update' to mark each step done/failed/skipped as you progress, "
        "'view' to check the current state."
    )
    parameters = {
        "type": "object",
        "properties": {
            "op": {
                "type": "string",
                "enum": ["set", "view", "update", "clear"],
                "description": (
                    "set — create/replace plan with a list of tasks; "
                    "view — show current plan; "
                    "update — change status of one task; "
                    "clear — reset plan"
                ),
            },
            "tasks": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Ordered list of task descriptions (required for 'set').",
            },
            "index": {
                "type": "integer",
                "description": "1-based task index (required for 'update').",
            },
            "status": {
                "type": "string",
                "enum": ["pending", "in_progress", "done", "failed", "skipped"],
                "description": "New status for the task (required for 'update').",
            },
        },
        "required": ["op"],
    }

    async def execute(self, params: dict) -> ToolResult:
        sid = current_session_id.get() or "__default__"
        op  = params.get("op")

        if op == "set":
            raw = params.get("tasks") or []
            if not raw:
                return ToolResult(success=False, output="", error="'tasks' list is required for 'set'")
            _plans[sid] = [_Task(text=str(t)) for t in raw]
            return ToolResult(success=True, output=self._render(sid))

        if op == "view":
            if sid not in _plans or not _plans[sid]:
                return ToolResult(success=True, output="No plan set for this session.")
            return ToolResult(success=True, output=self._render(sid))

        if op == "update":
            tasks = _plans.get(sid)
            if not tasks:
                return ToolResult(success=False, output="", error="No plan set. Use 'set' first.")
            idx    = params.get("index")
            status = params.get("status")
            if not idx or not status:
                return ToolResult(success=False, output="", error="'index' and 'status' are required for 'update'")
            if idx < 1 or idx > len(tasks):
                return ToolResult(success=False, output="", error=f"index {idx} is out of range (plan has {len(tasks)} tasks)")
            tasks[idx - 1].status = status
            return ToolResult(success=True, output=self._render(sid))

        if op == "clear":
            _plans.pop(sid, None)
            return ToolResult(success=True, output="Plan cleared.")

        return ToolResult(success=False, output="", error=f"Unknown op: {op!r}")

    def _render(self, sid: str) -> str:
        tasks = _plans.get(sid, [])
        if not tasks:
            return "Plan is empty."
        n = len(tasks)
        done = sum(1 for t in tasks if t.status == "done")
        lines = [f"Plan — {done}/{n} done:"]
        for i, t in enumerate(tasks, 1):
            icon   = _STATUS_ICON.get(t.status, "?")
            suffix = f" ({t.status})" if t.status not in ("pending", "done") else ""
            lines.append(f"  {icon} {i}. {t.text}{suffix}")
        return "\n".join(lines)