| 121 | |
| 122 | # === SECTION: todos (s03) === |
| 123 | class TodoManager: |
| 124 | def __init__(self): |
| 125 | self.items = [] |
| 126 | |
| 127 | def update(self, items: list) -> str: |
| 128 | validated, ip = [], 0 |
| 129 | for i, item in enumerate(items): |
| 130 | content = str(item.get("content", "")).strip() |
| 131 | status = str(item.get("status", "pending")).lower() |
| 132 | af = str(item.get("activeForm", "")).strip() |
| 133 | if not content: raise ValueError(f"Item {i}: content required") |
| 134 | if status not in ("pending", "in_progress", "completed"): |
| 135 | raise ValueError(f"Item {i}: invalid status '{status}'") |
| 136 | if not af: raise ValueError(f"Item {i}: activeForm required") |
| 137 | if status == "in_progress": ip += 1 |
| 138 | validated.append({"content": content, "status": status, "activeForm": af}) |
| 139 | if len(validated) > 20: raise ValueError("Max 20 todos") |
| 140 | if ip > 1: raise ValueError("Only one in_progress allowed") |
| 141 | self.items = validated |
| 142 | return self.render() |
| 143 | |
| 144 | def render(self) -> str: |
| 145 | if not self.items: return "No todos." |
| 146 | lines = [] |
| 147 | for item in self.items: |
| 148 | m = {"completed": "[x]", "in_progress": "[>]", "pending": "[ ]"}.get(item["status"], "[?]") |
| 149 | suffix = f" <- {item['activeForm']}" if item["status"] == "in_progress" else "" |
| 150 | lines.append(f"{m} {item['content']}{suffix}") |
| 151 | done = sum(1 for t in self.items if t["status"] == "completed") |
| 152 | lines.append(f"\n({done}/{len(self.items)} completed)") |
| 153 | return "\n".join(lines) |
| 154 | |
| 155 | def has_open_items(self) -> bool: |
| 156 | return any(item.get("status") != "completed" for item in self.items) |
| 157 | |
| 158 | |
| 159 | # === SECTION: subagent (s04) === |