@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 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 `Ctrl+Shift+T`** — collapsed shows just the header.
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 ./packages/pi-todo-list
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: *"track these tasks as todos: …"*, or use the tool directly:
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
- `Ctrl+Shift+T` toggles the panel. If your terminal intercepts that chord
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo-alvarenga/pi-todo-list",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Session-aware todo list overlay for Pi: todo tool, /todos command, and a live TUI panel",
5
5
  "license": "MIT",
6
6
  "repository": "github.com/leo-alvarenga/pi-mono",
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
- id: Type.Optional(Type.Number({ description: "Task id (required for update/remove)" })),
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
  }