@leo-alvarenga/pi-todo-list 0.2.0 → 0.4.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 +9 -17
- package/package.json +1 -1
- package/src/core.ts +42 -0
- package/src/index.ts +2 -1
- package/src/tool.ts +48 -3
package/README.md
CHANGED
|
@@ -12,9 +12,13 @@ The agent manages tasks with a `todo` tool while you watch them live in a panel
|
|
|
12
12
|
- **Live TUI panel** above the input editor — `Todos (done/total)` header,
|
|
13
13
|
status glyphs (`○` pending, `◐` in-progress, `✓` completed), blocked-task
|
|
14
14
|
hints, and a hardcoded row budget with a `+N more` summary line.
|
|
15
|
-
- **Toggle with `
|
|
15
|
+
- **Toggle with `Alt+T`** — collapsed shows just the header.
|
|
16
16
|
- **`/todos` command** — prints the full list grouped by status straight to
|
|
17
17
|
the terminal transcript.
|
|
18
|
+
- **Batch operations** — `todo add` accepts a `texts` array to add many at
|
|
19
|
+
once, and `todo remove` accepts an `ids` array to remove many at once.
|
|
20
|
+
- **`todo_complete_all` tool** — marks every todo completed and clears the
|
|
21
|
+
list in one call, telling you how many items were completed.
|
|
18
22
|
- **Session-isolated state** — each session has its own list; it survives
|
|
19
23
|
`/reload` and context compaction with no external files, because state is
|
|
20
24
|
replayed from the session branch (tool results + custom entries), never
|
|
@@ -25,7 +29,7 @@ The agent manages tasks with a `todo` tool while you watch them live in a panel
|
|
|
25
29
|
## Install
|
|
26
30
|
|
|
27
31
|
```bash
|
|
28
|
-
pi install
|
|
32
|
+
pi install npm:@leo-alvarenga/pi-todo-list
|
|
29
33
|
```
|
|
30
34
|
|
|
31
35
|
or add the local path / npm spec to your project `.pi/settings.json`, then
|
|
@@ -33,7 +37,7 @@ run `/reload`.
|
|
|
33
37
|
|
|
34
38
|
## Usage
|
|
35
39
|
|
|
36
|
-
Tell the agent:
|
|
40
|
+
Tell the agent: _"track these tasks as todos: …"_, or use the tool directly:
|
|
37
41
|
|
|
38
42
|
- `todo add` with `text`
|
|
39
43
|
- `todo update` with `id` and optional `status` / `text` / `blockedBy`
|
|
@@ -43,22 +47,10 @@ Run `/todos` yourself at any time to see the full grouped list.
|
|
|
43
47
|
|
|
44
48
|
## Keybinding
|
|
45
49
|
|
|
46
|
-
`
|
|
50
|
+
`Alt+T` toggles the panel. If your terminal intercepts that chord
|
|
47
51
|
(e.g. GNOME Terminal opens a new tab), pick a free one and change
|
|
48
52
|
`PANEL_TOGGLE_CHORD` in `src/constants.ts`.
|
|
49
53
|
|
|
50
|
-
## Development
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
pnpm --filter @leo-alvarenga/pi-todo-list typecheck
|
|
54
|
-
pnpm --filter @leo-alvarenga/pi-todo-list test
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
To release a new version, bump `version` in `package.json`, then push a tag
|
|
58
|
-
(`git tag pi-todo-list@0.1.0 && git push origin pi-todo-list@0.1.0`). The
|
|
59
|
-
`.github/workflows/publish-pi-todo-list.yml` workflow publishes to npm
|
|
60
|
-
(requires the `NPM_TOKEN` repository secret).
|
|
61
|
-
|
|
62
54
|
## License
|
|
63
55
|
|
|
64
|
-
MIT — see [LICENSE](LICENSE). Copyright (c) 2026 Leonardo A. Alvarenga.
|
|
56
|
+
MIT — see [LICENSE](LICENSE). Copyright (c) 2026 Leonardo A. Alvarenga.
|
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -168,6 +168,44 @@ function clear(): ActionResult {
|
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function addMany(state: TodoState, texts: string[]): ActionResult {
|
|
172
|
+
const cleaned = texts.map(cleanText);
|
|
173
|
+
if (cleaned.some((t) => !t)) return { ok: false, error: "text required for add" };
|
|
174
|
+
if (cleaned.some((t) => t && t.length > MAX_TEXT_LENGTH)) return textError();
|
|
175
|
+
|
|
176
|
+
let nextId = state.nextId;
|
|
177
|
+
const added: Todo[] = cleaned.map((t) => ({
|
|
178
|
+
text: t as string,
|
|
179
|
+
blockedBy: [],
|
|
180
|
+
id: nextId++,
|
|
181
|
+
status: "pending",
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
text: `Added ${added.length} todos: ${added.map((t) => `#${t.id}`).join(", ")}`,
|
|
187
|
+
state: { todos: [...state.todos, ...added], nextId },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function removeMany(state: TodoState, ids: number[]): ActionResult {
|
|
192
|
+
const missing = ids.filter((id) => !state.todos.some((t) => t.id === id));
|
|
193
|
+
if (missing.length > 0) {
|
|
194
|
+
return { ok: false, error: `todos not found: #${missing.join(", #")}` };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const gone = new Set(ids);
|
|
198
|
+
const todos = state.todos
|
|
199
|
+
.filter((t) => !gone.has(t.id))
|
|
200
|
+
.map((t) => ({ ...t, blockedBy: t.blockedBy.filter((b) => !gone.has(b)) }));
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
text: `Removed ${ids.length} todos`,
|
|
205
|
+
state: { todos, nextId: state.nextId },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
171
209
|
/**
|
|
172
210
|
* Apply a tool action against a copy of the state.
|
|
173
211
|
* Returns the new state only when validation passes; the caller commits it.
|
|
@@ -176,7 +214,9 @@ export function applyAction(
|
|
|
176
214
|
state: TodoState,
|
|
177
215
|
params: {
|
|
178
216
|
id?: number;
|
|
217
|
+
ids?: number[];
|
|
179
218
|
text?: string;
|
|
219
|
+
texts?: string[];
|
|
180
220
|
action: TodoAction;
|
|
181
221
|
status?: TodoStatus;
|
|
182
222
|
blockedBy?: number[];
|
|
@@ -184,6 +224,7 @@ export function applyAction(
|
|
|
184
224
|
): ActionResult {
|
|
185
225
|
switch (params.action) {
|
|
186
226
|
case "add":
|
|
227
|
+
if (params.texts !== undefined && params.texts.length > 0) return addMany(state, params.texts);
|
|
187
228
|
return add(state, params.text ?? "");
|
|
188
229
|
|
|
189
230
|
case "update":
|
|
@@ -194,6 +235,7 @@ export function applyAction(
|
|
|
194
235
|
return update(state, params.id, params);
|
|
195
236
|
|
|
196
237
|
case "remove":
|
|
238
|
+
if (params.ids !== undefined && params.ids.length > 0) return removeMany(state, params.ids);
|
|
197
239
|
if (params.id === undefined) {
|
|
198
240
|
return { ok: false, error: "id required for remove" };
|
|
199
241
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { registerTodosCommand } from "./command";
|
|
4
4
|
import { STATE_ENTRY, WIDGET_KEY } from "./constants";
|
|
5
5
|
import { TodoStore } from "./state";
|
|
6
|
-
import { registerTodoTool } from "./tool";
|
|
6
|
+
import { registerTodoTool, registerTodoCompleteAllTool } from "./tool";
|
|
7
7
|
import { registerTodoWidget, refreshWidget } from "./widget";
|
|
8
8
|
|
|
9
9
|
export default function (pi: ExtensionAPI): void {
|
|
@@ -13,6 +13,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
13
13
|
);
|
|
14
14
|
|
|
15
15
|
registerTodoTool(pi, store);
|
|
16
|
+
registerTodoCompleteAllTool(pi, store);
|
|
16
17
|
registerTodosCommand(pi, store);
|
|
17
18
|
registerTodoWidget(pi, store);
|
|
18
19
|
|
package/src/tool.ts
CHANGED
|
@@ -6,12 +6,14 @@ import { Type } from "typebox";
|
|
|
6
6
|
import { STATUSES } from "./constants";
|
|
7
7
|
import { applyAction } from "./core";
|
|
8
8
|
import type { TodoStore } from "./state";
|
|
9
|
-
import type { TodoDetails } from "./types";
|
|
9
|
+
import type { TodoDetails, TodoState } from "./types";
|
|
10
10
|
|
|
11
11
|
const TodoParams = Type.Object({
|
|
12
12
|
action: StringEnum(["add", "update", "remove", "list", "clear"] as const),
|
|
13
13
|
text: Type.Optional(Type.String({ description: "Task text (required for add; optional for update)" })),
|
|
14
|
-
|
|
14
|
+
texts: Type.Optional(Type.Array(Type.String(), { description: "Task texts; add all at once (optional; used with action=add)" })),
|
|
15
|
+
id: Type.Optional(Type.Number({ description: "Task id (required for remove; alternative to ids)" })),
|
|
16
|
+
ids: Type.Optional(Type.Array(Type.Number(), { description: "Task ids; remove all at once (optional; used with action=remove)" })),
|
|
15
17
|
status: Type.Optional(StringEnum([...STATUSES] as const)),
|
|
16
18
|
blockedBy: Type.Optional(Type.Array(Type.Number(), { description: "Task ids this task depends on" })),
|
|
17
19
|
});
|
|
@@ -22,7 +24,14 @@ export function registerTodoTool(pi: ExtensionAPI, store: TodoStore): void {
|
|
|
22
24
|
label: "Todo",
|
|
23
25
|
parameters: TodoParams,
|
|
24
26
|
description:
|
|
25
|
-
"Manage the session todo list. Actions: add (text), update (id, optional status/text/blockedBy), remove (id), list, clear",
|
|
27
|
+
"Manage the session todo list. Actions: add (text or texts), update (id, optional status/text/blockedBy), remove (id or ids), list, clear",
|
|
28
|
+
promptSnippet: "Manage the session todo list: add/update/remove/list/clear tasks",
|
|
29
|
+
promptGuidelines: [
|
|
30
|
+
"Always track the work using the todo tools.",
|
|
31
|
+
"Record each item the user wants tracked with todo add.",
|
|
32
|
+
"Keep todos current: todo update to completed as you finish items, todo remove for dropped ones.",
|
|
33
|
+
"When the user says 'complete everything' / 'done with all', use todo_complete_all instead of looping todo update.",
|
|
34
|
+
],
|
|
26
35
|
|
|
27
36
|
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
28
37
|
const current = store.getState(ctx);
|
|
@@ -63,4 +72,40 @@ export function registerTodoTool(pi: ExtensionAPI, store: TodoStore): void {
|
|
|
63
72
|
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", msg), 0, 0);
|
|
64
73
|
},
|
|
65
74
|
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerTodoCompleteAllTool(pi: ExtensionAPI, store: TodoStore): void {
|
|
78
|
+
pi.registerTool({
|
|
79
|
+
name: "todo_complete_all",
|
|
80
|
+
label: "Complete All Todos",
|
|
81
|
+
parameters: Type.Object({}),
|
|
82
|
+
description:
|
|
83
|
+
"Mark every todo as completed and clear the list in one shot. The result tells the user how many items were completed.",
|
|
84
|
+
promptSnippet:
|
|
85
|
+
"When the user says 'complete everything' / 'done with all', use todo_complete_all instead of looping todo update.",
|
|
86
|
+
|
|
87
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
88
|
+
const current = store.getState(ctx);
|
|
89
|
+
const n = current.todos.length;
|
|
90
|
+
const cleared: TodoState = { todos: [], nextId: 1 };
|
|
91
|
+
store.commit(ctx, cleared);
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: `All ${n} todos completed and cleared` }],
|
|
94
|
+
details: { action: "clear", todos: [], nextId: 1 } satisfies TodoDetails,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
renderCall(_args, theme) {
|
|
99
|
+
return new Text(theme.fg("toolTitle", theme.bold("todo_complete_all")), 0, 0);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
renderResult(result, _options, theme) {
|
|
103
|
+
const text = result.content[0];
|
|
104
|
+
return new Text(
|
|
105
|
+
theme.fg("success", "✓ ") + theme.fg("muted", text?.type === "text" ? text.text : ""),
|
|
106
|
+
0,
|
|
107
|
+
0,
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
66
111
|
}
|