@pi-archimedes/todo 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @pi-archimedes/todo
2
+
3
+ Todo list management with auto-clear and live subagent visibility for the [Pi coding agent](https://github.com/earendil-works/pi).
4
+
5
+ ## Features
6
+
7
+ - **`manage_todo_list` tool** — structured todo tracking with `read` and `write` operations
8
+ - **Auto-clear** — when all todos are completed, the list clears itself after a brief 2-second delay
9
+ - **Multi-column widget** — main agent todos on the left, each subagent's todos in their own column to the right
10
+ - **Live subagent visibility** — subagent todos stream through the core bus so you can see what they're working on
11
+ - **`/todos` command** — toggle the widget or clear todos (`/todos clear`)
12
+ - **Session persistence** — todos survive `/reload` via session branch reconstruction
13
+
14
+ ## Screenshots
15
+
16
+ ### Multiple todos with progress tracking
17
+
18
+ Widget showing three todos with completion status — completed items dimmed with strikethrough, in-progress highlighted:
19
+
20
+ ![todos multiple todos](../../docs/images/todos-multiple-todos.png)
21
+
22
+ ### Main agent + subagent side by side
23
+
24
+ Main agent todos (left) alongside a subagent's todos (right), separated by a divider. Subagent column auto-removes when the subagent finishes:
25
+
26
+ ![todos and subagent](../../docs/images/todos-and-subagent.png)
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pi install @pi-archimedes/todo
32
+ ```
33
+
34
+ Or install the full [pi-archimedes](../..) meta package for the integrated experience:
35
+
36
+ ```bash
37
+ pi install pi-archimedes
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### As a tool
43
+
44
+ The `manage_todo_list` tool accepts two operations:
45
+
46
+ **Read current todos:**
47
+
48
+ ```jsonc
49
+ {
50
+ "operation": "read"
51
+ }
52
+ ```
53
+
54
+ **Write (replace) the todo list:**
55
+
56
+ ```jsonc
57
+ {
58
+ "operation": "write",
59
+ "todoList": [
60
+ { "id": 1, "title": "Parse config files", "description": "Read and validate all config files", "status": "in-progress" },
61
+ { "id": 2, "title": "Build state manager", "description": "Implement TodoStateManager class", "status": "not-started" },
62
+ { "id": 3, "title": "Wire up widget", "description": "Connect widget to bus events", "status": "not-started" }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ ### As a command
68
+
69
+ - `/todos` — toggle the todo widget visibility
70
+ - `/todos clear` — clear all todos immediately
71
+
72
+ ## Todo statuses
73
+
74
+ | Status | Icon | Description |
75
+ |--------|------|-------------|
76
+ | `not-started` | ○ | Not yet begun |
77
+ | `in-progress` | ◉ | Currently being worked on |
78
+ | `completed` | ✓ | Fully finished |
79
+
80
+ ## Auto-clear
81
+
82
+ When all todos in the list are marked `completed`, the widget shows the all-done state for 2 seconds, then auto-clears. No need to manually run `/todos clear`.
83
+
84
+ ## Subagent integration
85
+
86
+ When installed via `pi-archimedes` (the meta package), subagent todo events flow through `@pi-archimedes/core/bus` and appear as separate columns in the widget. Each subagent gets its own column labeled with its agent name. The column auto-removes when the subagent finishes.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@pi-archimedes/todo",
3
+ "version": "1.2.0",
4
+ "type": "module",
5
+ "keywords": [
6
+ "pi-package"
7
+ ],
8
+ "description": "Todo list tool with auto-clear and subagent visibility",
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "main": "./src/index.ts",
13
+ "dependencies": {
14
+ "@pi-archimedes/core": "workspace:*"
15
+ },
16
+ "peerDependencies": {
17
+ "@earendil-works/pi-ai": ">=0.1.0",
18
+ "@earendil-works/pi-coding-agent": ">=0.1.0",
19
+ "@earendil-works/pi-tui": ">=0.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^6.0.0"
23
+ },
24
+ "pi": {
25
+ "image": "https://raw.githubusercontent.com/danielcherubini/pi-archimedes/main/docs/images/todos-and-subagent.png"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import { getBus, Events } from "@pi-archimedes/core/bus";
3
+ import { TodoStateManager } from "./state-manager.js";
4
+ import { createManageTodoListTool } from "./tool.js";
5
+ import { updateWidget, clearWidget } from "./ui/todo-widget.js";
6
+ import type { TodoItem } from "./types.js";
7
+
8
+ export default function (pi: ExtensionAPI): void {
9
+ registerTodo(pi);
10
+ }
11
+
12
+ export function registerTodo(pi: ExtensionAPI): void {
13
+ const state = new TodoStateManager();
14
+ const subagentTodos = new Map<string, TodoItem[]>();
15
+ const unsubscribes: Array<() => void> = [];
16
+ let currentCtx: ExtensionContext | undefined;
17
+
18
+ const refreshWidget = () => {
19
+ if (currentCtx) {
20
+ updateWidget(state, currentCtx, subagentTodos);
21
+ }
22
+ };
23
+
24
+ // Subscribe to bus events for subagent todos
25
+ const unsubTodosUpdate = getBus().on(Events.TODOS_UPDATE, (payload: unknown) => {
26
+ const data = payload as { source: string; todos: TodoItem[] };
27
+ if (data.source === "main") return; // main handled locally
28
+ subagentTodos.set(data.source, data.todos);
29
+ refreshWidget();
30
+ });
31
+ unsubscribes.push(unsubTodosUpdate);
32
+
33
+ const unsubTodosClear = getBus().on(Events.TODOS_CLEAR, (payload: unknown) => {
34
+ const data = payload as { source: string };
35
+ subagentTodos.delete(data.source);
36
+ refreshWidget();
37
+ });
38
+ unsubscribes.push(unsubTodosClear);
39
+
40
+ // Reconstruct state from session on load/resume/fork/tree
41
+ const reconstructState = (ctx: ExtensionContext) => {
42
+ currentCtx = ctx;
43
+ state.loadFromSession(ctx);
44
+ updateWidget(state, ctx, subagentTodos);
45
+ };
46
+
47
+ pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
48
+ pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
49
+
50
+ // Keep ctx reference fresh on every turn
51
+ pi.on("turn_start", async (_event, ctx) => {
52
+ currentCtx = ctx;
53
+ });
54
+
55
+ // Update widget after each turn (in case tool was called)
56
+ pi.on("turn_end", async (_event, ctx) => {
57
+ currentCtx = ctx;
58
+ updateWidget(state, ctx, subagentTodos);
59
+ });
60
+
61
+ // session_shutdown handler (top-level to prevent accumulation on /reload)
62
+ pi.on("session_shutdown", (_event, _ctx) => {
63
+ unsubscribes.forEach((unsub) => unsub());
64
+ unsubscribes.length = 0;
65
+ subagentTodos.clear();
66
+ state.cancelAutoClear();
67
+ if (currentCtx) {
68
+ clearWidget(currentCtx);
69
+ }
70
+ });
71
+
72
+ // Register the manage_todo_list tool
73
+ const tool = createManageTodoListTool(state, refreshWidget);
74
+ pi.registerTool(tool);
75
+
76
+ // Register /todos command
77
+ pi.registerCommand("todos", {
78
+ description: "Toggle todo list widget or clear todos (/todos clear)",
79
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
80
+ currentCtx = ctx;
81
+
82
+ if (args?.trim().toLowerCase() === "clear") {
83
+ state.clear();
84
+ clearWidget(ctx);
85
+ ctx.ui.notify("Todo list cleared.", "info");
86
+ return;
87
+ }
88
+
89
+ const todos = state.read();
90
+ if (todos.length === 0) {
91
+ ctx.ui.notify("No todos. The LLM will create them when working on complex tasks.", "info");
92
+ } else {
93
+ updateWidget(state, ctx, subagentTodos);
94
+ const stats = state.getStats();
95
+ ctx.ui.notify(`${stats.completed}/${stats.total} todos completed.`, "info");
96
+ }
97
+ },
98
+ });
99
+ }
@@ -0,0 +1,93 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { TodoItem, TodoStats, ValidationResult } from "./types.js";
3
+
4
+ /** Manages the in-memory todo list state. */
5
+ export class TodoStateManager {
6
+ private todos: TodoItem[] = [];
7
+ private autoClearTimer: ReturnType<typeof setTimeout> | undefined;
8
+
9
+ read(): TodoItem[] {
10
+ return [...this.todos];
11
+ }
12
+
13
+ write(todos: TodoItem[]): void {
14
+ this.cancelAutoClear();
15
+ this.todos = todos.map((t) => ({ ...t }));
16
+ // If all completed and non-empty, schedule auto-clear
17
+ if (todos.length > 0 && todos.every((t) => t.status === "completed")) {
18
+ this.scheduleAutoClear(() => this.clear());
19
+ }
20
+ }
21
+
22
+ clear(): void {
23
+ this.cancelAutoClear();
24
+ this.todos = [];
25
+ }
26
+
27
+ getStats(): TodoStats {
28
+ const total = this.todos.length;
29
+ const completed = this.todos.filter((t) => t.status === "completed").length;
30
+ const inProgress = this.todos.filter((t) => t.status === "in-progress").length;
31
+ const notStarted = this.todos.filter((t) => t.status === "not-started").length;
32
+ return { total, completed, inProgress, notStarted };
33
+ }
34
+
35
+ validate(todos: TodoItem[]): ValidationResult {
36
+ const errors: string[] = [];
37
+ if (!Array.isArray(todos)) {
38
+ return { valid: false, errors: ["todoList must be an array"] };
39
+ }
40
+ const validStatuses = new Set(["not-started", "in-progress", "completed"]);
41
+ for (let i = 0; i < todos.length; i++) {
42
+ const item = todos[i];
43
+ const prefix = `Item ${i + 1}`;
44
+ if (!item) {
45
+ errors.push(`${prefix}: undefined item`);
46
+ continue;
47
+ }
48
+ if (item.id == null) {
49
+ errors.push(`${prefix}: missing 'id'`);
50
+ } else if (typeof item.id !== "number") {
51
+ errors.push(`${prefix}: 'id' must be a number`);
52
+ }
53
+ if (!item.title || typeof item.title !== "string") {
54
+ errors.push(`${prefix}: missing or invalid 'title'`);
55
+ }
56
+ if (!item.description || typeof item.description !== "string") {
57
+ errors.push(`${prefix}: missing or invalid 'description'`);
58
+ }
59
+ if (!item.status || !validStatuses.has(item.status)) {
60
+ errors.push(`${prefix}: 'status' must be one of: not-started, in-progress, completed`);
61
+ }
62
+ }
63
+ return { valid: errors.length === 0, errors };
64
+ }
65
+
66
+ loadFromSession(ctx: ExtensionContext): void {
67
+ this.todos = [];
68
+ for (const entry of ctx.sessionManager.getBranch()) {
69
+ if (entry.type !== "message") continue;
70
+ const msg = entry.message;
71
+ if (msg.role !== "toolResult" || msg.toolName !== "manage_todo_list") continue;
72
+ const details = msg.details as { todos?: TodoItem[] } | undefined;
73
+ if (details?.todos) {
74
+ this.todos = details.todos.map((t) => ({ ...t }));
75
+ }
76
+ }
77
+ }
78
+
79
+ scheduleAutoClear(callback: () => void): void {
80
+ this.cancelAutoClear();
81
+ this.autoClearTimer = setTimeout(() => {
82
+ this.autoClearTimer = undefined;
83
+ callback();
84
+ }, 2000);
85
+ }
86
+
87
+ cancelAutoClear(): void {
88
+ if (this.autoClearTimer) {
89
+ clearTimeout(this.autoClearTimer);
90
+ this.autoClearTimer = undefined;
91
+ }
92
+ }
93
+ }
package/src/tool.ts ADDED
@@ -0,0 +1,196 @@
1
+ import { type Static, StringEnum, Type } from "@earendil-works/pi-ai";
2
+ import type { AgentToolResult, ExtensionContext, Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { getBus, Events } from "@pi-archimedes/core/bus";
5
+ import type { TodoStateManager } from "./state-manager.js";
6
+ import { STATUS_ICONS } from "./types.js";
7
+ import type { TodoDetails } from "./types.js";
8
+
9
+ const TodoItemSchema = Type.Object({
10
+ id: Type.Number({ description: "Unique identifier for the todo. Use sequential numbers starting from 1." }),
11
+ title: Type.String({ description: "Concise action-oriented todo label (3-7 words). Displayed in UI." }),
12
+ description: Type.String({
13
+ description: "Detailed context, requirements, or implementation notes. Include file paths, specific methods, or acceptance criteria.",
14
+ }),
15
+ status: StringEnum(["not-started", "in-progress", "completed"] as const, {
16
+ description: "not-started: Not begun | in-progress: Currently working (multiple allowed for parallel work) | completed: Fully finished with no blockers",
17
+ }),
18
+ });
19
+
20
+ export const ManageTodoListParams = Type.Object({
21
+ operation: StringEnum(["write", "read"] as const, {
22
+ description: "write: Replace entire todo list with new content. read: Retrieve current todo list. ALWAYS provide complete list when writing - partial updates not supported.",
23
+ }),
24
+ todoList: Type.Optional(
25
+ Type.Array(TodoItemSchema, {
26
+ description: "Complete array of all todo items (required for write operation, ignored for read). Must include ALL items - both existing and new.",
27
+ })
28
+ ),
29
+ });
30
+
31
+ export type ManageTodoListInput = Static<typeof ManageTodoListParams>;
32
+
33
+ export const TOOL_DESCRIPTION = `Manage a structured todo list to track progress and plan tasks throughout your coding session. Use this tool VERY frequently to ensure task visibility and proper planning.
34
+
35
+ When to use this tool:
36
+ - Complex multi-step work requiring planning and tracking
37
+ - When user provides multiple tasks or requests (numbered, comma-separated)
38
+ - After receiving new instructions that require multiple steps
39
+ - BEFORE starting work on any todo (mark as in-progress)
40
+ - IMMEDIATELY after completing each todo (mark completed individually)
41
+ - When breaking down larger tasks into smaller actionable steps
42
+ - To give users visibility into your progress and planning
43
+
44
+ When NOT to use:
45
+ - Single, trivial tasks that can be completed in one step
46
+ - Purely conversational/informational requests
47
+ - When just reading files or performing simple searches
48
+
49
+ CRITICAL workflow:
50
+ 1. Plan tasks by writing todo list with specific, actionable items
51
+ 2. Mark todo(s) as in-progress before starting work
52
+ 3. Complete the work for that specific todo
53
+ 4. Mark that todo as completed IMMEDIATELY
54
+ 5. Move to next todo and repeat
55
+
56
+ Todo states:
57
+ - not-started: Todo not yet begun
58
+ - in-progress: Currently working (multiple allowed for parallel work/subagents)
59
+ - completed: Finished successfully
60
+
61
+ IMPORTANT: Mark todos completed as soon as they are done. Do not batch completions.
62
+ When all todos are completed, the list auto-clears after a brief delay.`;
63
+
64
+ export function createManageTodoListTool(state: TodoStateManager, onUpdate: () => void) {
65
+ return {
66
+ name: "manage_todo_list",
67
+ label: "Todo List",
68
+ description: TOOL_DESCRIPTION,
69
+ parameters: ManageTodoListParams,
70
+
71
+ async execute(
72
+ _toolCallId: string,
73
+ params: ManageTodoListInput,
74
+ _signal: AbortSignal | undefined,
75
+ _onStreamUpdate: undefined,
76
+ _ctx: ExtensionContext
77
+ ) {
78
+ if (params.operation === "read") {
79
+ const todos = state.read();
80
+ return {
81
+ content: [
82
+ {
83
+ type: "text" as const,
84
+ text: todos.length
85
+ ? JSON.stringify(todos, null, 2)
86
+ : "No todos. Use write operation to create a todo list.",
87
+ },
88
+ ],
89
+ details: { operation: "read", todos } as TodoDetails,
90
+ };
91
+ }
92
+
93
+ // write
94
+ const todoList = params.todoList;
95
+ if (!todoList || !Array.isArray(todoList)) {
96
+ return {
97
+ content: [{ type: "text" as const, text: "Error: todoList is required for write operation." }],
98
+ details: { operation: "write", todos: state.read(), error: "todoList required" } as TodoDetails,
99
+ isError: true,
100
+ };
101
+ }
102
+
103
+ const validation = state.validate(todoList);
104
+ if (!validation.valid) {
105
+ return {
106
+ content: [{ type: "text" as const, text: `Validation failed:\n${validation.errors.map((e) => ` - ${e}`).join("\n")}` }],
107
+ details: { operation: "write", todos: state.read(), error: validation.errors.join("; ") } as TodoDetails,
108
+ isError: true,
109
+ };
110
+ }
111
+
112
+ state.write(todoList);
113
+ onUpdate();
114
+
115
+ // Emit bus event for widget update
116
+ try {
117
+ getBus().emit(Events.TODOS_UPDATE, { source: "main", todos: state.read() });
118
+ } catch {
119
+ // Bus may not be initialized in subagent child process — ignore
120
+ }
121
+
122
+ const stats = state.getStats();
123
+ const todos = state.read();
124
+
125
+ let message = `Todos have been modified successfully. ${stats.completed}/${stats.total} completed. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable.`;
126
+
127
+ if (todoList.length < 3) {
128
+ message += `\n\nWarning: Small todo list (<3 items). This task might not need a todo list.`;
129
+ }
130
+
131
+ return {
132
+ content: [{ type: "text" as const, text: message }],
133
+ details: { operation: "write", todos } as TodoDetails,
134
+ };
135
+ },
136
+
137
+ renderCall(args: ManageTodoListInput, theme: Theme) {
138
+ let text = theme.fg("toolTitle", theme.bold("manage_todo_list "));
139
+ text += theme.fg("muted", args.operation);
140
+
141
+ if (args.operation === "write" && args.todoList) {
142
+ const count = args.todoList.length;
143
+ text += theme.fg("dim", ` (${count} item${count !== 1 ? "s" : ""})`);
144
+ }
145
+
146
+ return new Text(text, 0, 0);
147
+ },
148
+
149
+ renderResult(
150
+ result: AgentToolResult<TodoDetails | undefined>,
151
+ { expanded }: ToolRenderResultOptions,
152
+ theme: Theme
153
+ ) {
154
+ const details = result.details;
155
+ if (!details) {
156
+ const first = result.content[0];
157
+ return new Text(first && "text" in first ? first.text : "", 0, 0);
158
+ }
159
+
160
+ if (details.error) {
161
+ return new Text(theme.fg("error", `✗ ${details.error}`), 0, 0);
162
+ }
163
+
164
+ const todos = details.todos;
165
+ const completed = todos.filter((t) => t.status === "completed").length;
166
+ const total = todos.length;
167
+
168
+ if (total === 0) {
169
+ return new Text(theme.fg("dim", "No todos"), 0, 0);
170
+ }
171
+
172
+ let text = theme.fg("success", "✓ ") + theme.fg("muted", `${completed}/${total} completed`);
173
+
174
+ if (expanded) {
175
+ for (const todo of todos) {
176
+ const iconChar = STATUS_ICONS[todo.status] ?? "?";
177
+ const icon =
178
+ todo.status === "completed"
179
+ ? theme.fg("success", iconChar)
180
+ : todo.status === "in-progress"
181
+ ? theme.fg("warning", iconChar)
182
+ : theme.fg("dim", iconChar);
183
+ const title =
184
+ todo.status === "completed"
185
+ ? theme.fg("dim", theme.strikethrough(todo.title))
186
+ : todo.status === "in-progress"
187
+ ? theme.fg("warning", todo.title)
188
+ : theme.fg("muted", todo.title);
189
+ text += `\n ${icon} ${theme.fg("accent", `${todo.id}.`)} ${title}`;
190
+ }
191
+ }
192
+
193
+ return new Text(text, 0, 0);
194
+ },
195
+ };
196
+ }
package/src/types.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Core types for the todo list extension.
3
+ */
4
+
5
+ /** Status of a single todo item */
6
+ export type TodoStatus = "not-started" | "in-progress" | "completed";
7
+
8
+ /** A single todo item */
9
+ export interface TodoItem {
10
+ /** Sequential identifier starting from 1 */
11
+ id: number;
12
+ /** Concise action-oriented label (3-7 words). Displayed in UI. */
13
+ title: string;
14
+ /** Detailed context, requirements, or implementation notes. */
15
+ description: string;
16
+ /** Current status. */
17
+ status: TodoStatus;
18
+ }
19
+
20
+ /** Stored in tool result details for session persistence */
21
+ export interface TodoDetails {
22
+ operation: "read" | "write";
23
+ todos: TodoItem[];
24
+ error?: string;
25
+ }
26
+
27
+ /** Stats about the current todo list */
28
+ export interface TodoStats {
29
+ total: number;
30
+ completed: number;
31
+ inProgress: number;
32
+ notStarted: number;
33
+ }
34
+
35
+ /** Validation result */
36
+ export interface ValidationResult {
37
+ valid: boolean;
38
+ errors: string[];
39
+ }
40
+
41
+ /** Status icons for each todo state */
42
+ export const STATUS_ICONS: Record<TodoStatus, string> = {
43
+ "completed": "✓",
44
+ "in-progress": "◉ ",
45
+ "not-started": "○",
46
+ };
@@ -0,0 +1,154 @@
1
+ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { STATUS_ICONS } from "../types.js";
4
+ import type { TodoItem } from "../types.js";
5
+ import type { TodoStateManager } from "../state-manager.js";
6
+
7
+ const WIDGET_ID = "todo-list";
8
+
9
+ interface Column {
10
+ header: string;
11
+ todos: TodoItem[];
12
+ }
13
+
14
+ /**
15
+ * Update (or clear) the todo widget.
16
+ */
17
+ export function updateWidget(
18
+ state: TodoStateManager,
19
+ ctx: ExtensionContext,
20
+ subagentTodos: Map<string, TodoItem[]>,
21
+ ): void {
22
+ const mainTodos = state.read();
23
+
24
+ // Hide widget if everything is empty
25
+ if (mainTodos.length === 0 && subagentTodos.size === 0) {
26
+ ctx.ui.setWidget(WIDGET_ID, undefined);
27
+ return;
28
+ }
29
+
30
+ ctx.ui.setWidget(WIDGET_ID, (_tui, theme) => {
31
+ // Re-read in case state changed since widget was set
32
+ const currentMain = state.read();
33
+ const currentSub = new Map(subagentTodos);
34
+
35
+ if (currentMain.length === 0 && currentSub.size === 0) {
36
+ ctx.ui.setWidget(WIDGET_ID, undefined);
37
+ return { render: () => [], invalidate: () => {} };
38
+ }
39
+
40
+ const currentStats = state.getStats();
41
+
42
+ // Build columns
43
+ const columns: Column[] = [];
44
+
45
+ // Main column (always first, no header label)
46
+ if (currentMain.length > 0) {
47
+ columns.push({ header: "", todos: currentMain });
48
+ }
49
+
50
+ // Subagent columns (sorted by source name)
51
+ const sortedSources = Array.from(currentSub.keys()).sort();
52
+ for (const source of sortedSources) {
53
+ const todos = currentSub.get(source)!;
54
+ if (todos.length > 0) {
55
+ const agentName = source.replace("subagent:", "");
56
+ columns.push({ header: `subagent (${agentName})`, todos });
57
+ }
58
+ }
59
+
60
+ if (columns.length === 0) {
61
+ return { render: () => [], invalidate: () => {} };
62
+ }
63
+
64
+ const maxRows = Math.max(...columns.map((c) => c.todos.length));
65
+
66
+ return {
67
+ render(width: number) {
68
+ const lines: string[] = [];
69
+
70
+ // Header line
71
+ const header =
72
+ theme.fg("accent", " Todo List ") +
73
+ theme.fg("muted", ` — ${currentStats.completed}/${currentStats.total} completed`);
74
+ lines.push(truncateToWidth(header, width));
75
+
76
+ // Calculate column widths
77
+ const numCols = columns.length;
78
+ const divider = theme.fg("dim", " │ ");
79
+ const dividerWidth = numCols > 1 ? (numCols - 1) * visibleWidth(divider) : 0;
80
+ const minColWidth = 20;
81
+ let colWidth = numCols > 0 ? Math.floor((width - dividerWidth) / numCols) : width;
82
+ colWidth = Math.max(colWidth, minColWidth);
83
+
84
+ // Render subagent headers on row 0
85
+ const hasSubHeader = columns.some((c) => c.header.length > 0);
86
+
87
+ for (let row = 0; row < maxRows; row++) {
88
+ const cellParts: string[] = [];
89
+
90
+ for (const column of columns) {
91
+ let cellText: string;
92
+
93
+ if (row === 0 && hasSubHeader && column.header) {
94
+ cellText = ` ${column.header}`;
95
+ } else if (row < column.todos.length) {
96
+ const todo = column.todos[row];
97
+ if (todo) {
98
+ const icon = getStatusIcon(todo.status, theme);
99
+ const idStr = theme.fg("accent", `${todo.id}.`);
100
+ const title = formatTodoTitle(todo, theme);
101
+ cellText = ` ${icon} ${idStr} ${title}`;
102
+ } else {
103
+ cellText = "";
104
+ }
105
+ } else {
106
+ cellText = "";
107
+ }
108
+
109
+ // Truncate and pad to column width
110
+ const visible = visibleWidth(cellText);
111
+ if (visible > colWidth) {
112
+ // Truncate
113
+ const truncated = truncateToWidth(cellText, colWidth);
114
+ cellParts.push(truncated);
115
+ } else {
116
+ // Pad
117
+ const padding = " ".repeat(colWidth - visible);
118
+ cellParts.push(cellText + padding);
119
+ }
120
+ }
121
+
122
+ // Join columns with dividers
123
+ let line = cellParts.join(divider);
124
+ lines.push(truncateToWidth(line, width));
125
+ }
126
+
127
+ return lines;
128
+ },
129
+ invalidate: () => {},
130
+ };
131
+ });
132
+ }
133
+
134
+ function getStatusIcon(status: TodoItem["status"], theme: Theme): string {
135
+ const icon = STATUS_ICONS[status] ?? "?";
136
+ if (status === "completed") return theme.fg("success", icon);
137
+ if (status === "in-progress") return theme.fg("warning", icon.trim());
138
+ return theme.fg("dim", icon);
139
+ }
140
+
141
+ function formatTodoTitle(todo: TodoItem, theme: Theme): string {
142
+ if (todo.status === "completed") {
143
+ return theme.fg("dim", theme.strikethrough(todo.title));
144
+ }
145
+ if (todo.status === "in-progress") {
146
+ return theme.fg("warning", todo.title);
147
+ }
148
+ return theme.fg("muted", todo.title);
149
+ }
150
+
151
+ /** Clear the widget */
152
+ export function clearWidget(ctx: ExtensionContext): void {
153
+ ctx.ui.setWidget(WIDGET_ID, undefined);
154
+ }