@ian-pascoe/pi-todo 0.0.0 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-todo",
3
- "version": "0.0.0",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
5
  "description": "Minimal session-native Todo List for Pi agents.",
6
6
  "keywords": [
@@ -32,10 +32,6 @@
32
32
  "access": "public",
33
33
  "provenance": true
34
34
  },
35
- "scripts": {
36
- "test": "vitest run --config ../../vitest.config.ts --root .",
37
- "typecheck": "tsc --noEmit -p tsconfig.json"
38
- },
39
35
  "peerDependencies": {
40
36
  "@earendil-works/pi-ai": "*",
41
37
  "@earendil-works/pi-coding-agent": "*",
@@ -52,5 +48,9 @@
52
48
  "skills": [
53
49
  "./skills"
54
50
  ]
51
+ },
52
+ "scripts": {
53
+ "test": "vitest run --config ../../vitest.config.ts --root .",
54
+ "typecheck": "tsc --noEmit -p tsconfig.json"
55
55
  }
56
- }
56
+ }
@@ -120,7 +120,7 @@ export default function piTodoExtension(pi: ExtensionAPI): void {
120
120
  const runTodoAction = (input: TodoActionInput, context: ExtensionContext) => {
121
121
  const result = applyTodoAction(state, input);
122
122
  if (!result.ok) throw result.error;
123
- if (result.changed) commitTodoState(result.state, context);
123
+ if (result.state !== state) commitTodoState(result.state, context);
124
124
  return result;
125
125
  };
126
126
  const restoreState = (context: ExtensionContext): void => {
@@ -163,11 +163,7 @@ export default function piTodoExtension(pi: ExtensionAPI): void {
163
163
  renderResult: (result, { expanded }, theme) => {
164
164
  const text = result.content.find((item) => item.type === "text");
165
165
  if (!result.details) {
166
- return new Text(
167
- theme.fg("error", text?.type === "text" ? text.text : "Todo operation failed"),
168
- 0,
169
- 0,
170
- );
166
+ return new Text(theme.fg("error", text?.text ?? "Todo operation failed"), 0, 0);
171
167
  }
172
168
  if (result.details.action === "list") {
173
169
  const tasks = result.details.tasks;
@@ -188,11 +184,7 @@ export default function piTodoExtension(pi: ExtensionAPI): void {
188
184
  if (remaining > 0) lines.push(theme.fg("dim", `… ${remaining} more`));
189
185
  return new Text(lines.join("\n"), 0, 0);
190
186
  }
191
- return new Text(
192
- theme.fg("success", "✓ ") + theme.fg("muted", text?.type === "text" ? text.text : "Done"),
193
- 0,
194
- 0,
195
- );
187
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", text?.text ?? "Done"), 0, 0);
196
188
  },
197
189
  });
198
190
 
package/src/todo-list.ts CHANGED
@@ -79,7 +79,6 @@ export class TodoOperationError extends Error {
79
79
  export type TodoActionResult =
80
80
  | {
81
81
  readonly ok: true;
82
- readonly changed: boolean;
83
82
  readonly state: TodoStateSnapshot;
84
83
  readonly message: string;
85
84
  readonly details: TodoToolDetails;
@@ -136,7 +135,6 @@ export function applyTodoAction(
136
135
  case "list":
137
136
  return {
138
137
  ok: true,
139
- changed: false,
140
138
  state,
141
139
  message: formatTodoList(state.tasks),
142
140
  details: { action: "list", tasks: state.tasks },
@@ -158,31 +156,43 @@ export function applyTodoAction(
158
156
  : { id, title, status: input.status ?? "pending" };
159
157
  return {
160
158
  ok: true,
161
- changed: true,
162
159
  state: { nextId: state.nextId + 1, tasks: [...state.tasks, task] },
163
160
  message: `Added Task #${task.id}`,
164
161
  details: { action: "add" },
165
162
  };
166
163
  }
167
164
 
168
- case "update": {
165
+ case "update":
166
+ case "remove": {
167
+ const action = input.action;
169
168
  if (input.id === undefined) {
170
- return todoOperationFailure("update", "Todo update failed: id is required");
169
+ return todoOperationFailure(action, `Todo ${action} failed: id is required`);
171
170
  }
172
171
  const id = parseTodoTaskId(input.id);
173
172
  if (id === undefined) {
174
173
  return todoOperationFailure(
175
- "update",
176
- "Todo update failed: id must be a positive safe integer",
174
+ action,
175
+ `Todo ${action} failed: id must be a positive safe integer`,
177
176
  );
178
177
  }
179
178
  const task = state.tasks.find((candidate) => candidate.id === id);
180
179
  if (!task) {
181
180
  return todoOperationFailure(
182
- "update",
183
- `Todo update failed: Task #${input.id} was not found`,
181
+ action,
182
+ `Todo ${action} failed: Task #${input.id} was not found`,
184
183
  );
185
184
  }
185
+ if (action === "remove") {
186
+ return {
187
+ ok: true,
188
+ state: {
189
+ nextId: state.nextId,
190
+ tasks: state.tasks.filter((candidate) => candidate.id !== task.id),
191
+ },
192
+ message: `Removed Task #${task.id}`,
193
+ details: { action: "remove" },
194
+ };
195
+ }
186
196
  if (
187
197
  input.title === undefined &&
188
198
  input.description === undefined &&
@@ -198,11 +208,7 @@ export function applyTodoAction(
198
208
  return todoOperationFailure("update", "Todo update failed: title must not be empty");
199
209
  }
200
210
  const description =
201
- input.description === undefined
202
- ? task.description
203
- : input.description === null
204
- ? undefined
205
- : input.description.trim();
211
+ input.description === undefined ? task.description : input.description?.trim();
206
212
  if (input.description !== undefined && input.description !== null && !description) {
207
213
  return todoOperationFailure("update", "Todo update failed: description must not be empty");
208
214
  }
@@ -211,7 +217,6 @@ export function applyTodoAction(
211
217
  : { id: task.id, title, status: input.status ?? task.status };
212
218
  return {
213
219
  ok: true,
214
- changed: true,
215
220
  state: {
216
221
  nextId: state.nextId,
217
222
  tasks: state.tasks.map((candidate) =>
@@ -223,42 +228,11 @@ export function applyTodoAction(
223
228
  };
224
229
  }
225
230
 
226
- case "remove": {
227
- if (input.id === undefined) {
228
- return todoOperationFailure("remove", "Todo remove failed: id is required");
229
- }
230
- const id = parseTodoTaskId(input.id);
231
- if (id === undefined) {
232
- return todoOperationFailure(
233
- "remove",
234
- "Todo remove failed: id must be a positive safe integer",
235
- );
236
- }
237
- const task = state.tasks.find((candidate) => candidate.id === id);
238
- if (!task) {
239
- return todoOperationFailure(
240
- "remove",
241
- `Todo remove failed: Task #${input.id} was not found`,
242
- );
243
- }
244
- return {
245
- ok: true,
246
- changed: true,
247
- state: {
248
- nextId: state.nextId,
249
- tasks: state.tasks.filter((candidate) => candidate.id !== task.id),
250
- },
251
- message: `Removed Task #${task.id}`,
252
- details: { action: "remove" },
253
- };
254
- }
255
-
256
231
  case "clear": {
257
232
  const count = state.tasks.length;
258
233
  return {
259
234
  ok: true,
260
- changed: count > 0 || state.nextId !== 1,
261
- state: createEmptyTodoState(),
235
+ state: count > 0 || state.nextId !== 1 ? createEmptyTodoState() : state,
262
236
  message: `Cleared ${count} ${count === 1 ? "Task" : "Tasks"}`,
263
237
  details: { action: "clear" },
264
238
  };
@@ -273,10 +247,7 @@ export function todoStatusMarker(status: TodoStatus): "[ ]" | "[>]" | "[x]" {
273
247
  }
274
248
 
275
249
  function formatTodoDescription(description: string): string {
276
- return description
277
- .split("\n")
278
- .map((line) => ` ${line}`)
279
- .join("\n");
250
+ return " " + description.replaceAll("\n", "\n ");
280
251
  }
281
252
 
282
253
  /** Formats the complete numeric-ID-ordered Todo List for tools and model context. */