@zhushanwen/pi-todo 0.1.3 → 0.1.4
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 +1 -1
- package/src/index.ts +54 -54
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -41,12 +41,12 @@ const VALID_STATUSES = ["pending", "in_progress", "completed"] as const;
|
|
|
41
41
|
|
|
42
42
|
const TodoParams = Type.Object({
|
|
43
43
|
action: StringEnum(["list", "add", "update", "delete", "clear"] as const),
|
|
44
|
-
text: Type.Optional(Type.String({ description: "Todo
|
|
45
|
-
id: Type.Optional(Type.Number({ description: "Todo ID
|
|
46
|
-
texts: Type.Optional(Type.Array(Type.String(), { description: "Todo
|
|
47
|
-
ids: Type.Optional(Type.Array(Type.Number(), { description: "Todo ID
|
|
44
|
+
text: Type.Optional(Type.String({ description: "Todo text (for update action)" })),
|
|
45
|
+
id: Type.Optional(Type.Number({ description: "Todo ID (for update action)" })),
|
|
46
|
+
texts: Type.Optional(Type.Array(Type.String(), { description: "Todo text list (for add action)" })),
|
|
47
|
+
ids: Type.Optional(Type.Array(Type.Number(), { description: "Todo ID list (for delete action)" })),
|
|
48
48
|
status: Type.Optional(
|
|
49
|
-
StringEnum(VALID_STATUSES, { description: "
|
|
49
|
+
StringEnum(VALID_STATUSES, { description: "Target status (for update action)" }),
|
|
50
50
|
),
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -93,11 +93,11 @@ class TodoListComponent {
|
|
|
93
93
|
lines.push("");
|
|
94
94
|
|
|
95
95
|
if (this.todos.length === 0) {
|
|
96
|
-
lines.push(truncateToWidth(` ${th.fg("dim", "
|
|
96
|
+
lines.push(truncateToWidth(` ${th.fg("dim", "No todos yet. Ask the agent to add some!")}`, width));
|
|
97
97
|
} else {
|
|
98
98
|
const completed = this.todos.filter((t) => t.status === "completed").length;
|
|
99
99
|
const total = this.todos.length;
|
|
100
|
-
lines.push(truncateToWidth(` ${th.fg("muted", `${completed}/${total}
|
|
100
|
+
lines.push(truncateToWidth(` ${th.fg("muted", `${completed}/${total} completed`)}`, width));
|
|
101
101
|
lines.push("");
|
|
102
102
|
|
|
103
103
|
for (const todo of this.todos) {
|
|
@@ -114,7 +114,7 @@ class TodoListComponent {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
lines.push("");
|
|
117
|
-
lines.push(truncateToWidth(` ${th.fg("dim", "
|
|
117
|
+
lines.push(truncateToWidth(` ${th.fg("dim", "Press Escape to close")}`, width));
|
|
118
118
|
lines.push("");
|
|
119
119
|
|
|
120
120
|
this.cachedWidth = width;
|
|
@@ -192,7 +192,7 @@ function buildRender(todoList: Todo[]): TodoDetails["_render"] {
|
|
|
192
192
|
const total = todoList.length;
|
|
193
193
|
return {
|
|
194
194
|
type: "task-list" as const,
|
|
195
|
-
summary: `${completed}/${total}
|
|
195
|
+
summary: `${completed}/${total} completed`,
|
|
196
196
|
data: {
|
|
197
197
|
items: todoList.map((t) => ({ id: t.id, text: t.text, status: t.status })),
|
|
198
198
|
meta: {},
|
|
@@ -211,9 +211,9 @@ const TODO_REMINDER_INTERVAL = 10;
|
|
|
211
211
|
|
|
212
212
|
function buildTodoListText(todoList: Todo[], options: { expanded: boolean }, theme: Theme): string {
|
|
213
213
|
if (todoList.length === 0) {
|
|
214
|
-
return theme.fg("dim", "
|
|
214
|
+
return theme.fg("dim", "No todos");
|
|
215
215
|
}
|
|
216
|
-
let listText = theme.fg("muted", `${todoList.length}
|
|
216
|
+
let listText = theme.fg("muted", `${todoList.length} todos:`);
|
|
217
217
|
const display = options.expanded ? todoList : todoList.slice(0, MAX_COLLAPSED_ITEMS);
|
|
218
218
|
for (const t of display) {
|
|
219
219
|
const status = getDisplayStatus(t);
|
|
@@ -228,7 +228,7 @@ function buildTodoListText(todoList: Todo[], options: { expanded: boolean }, the
|
|
|
228
228
|
listText += `\n${mark} ${theme.fg("accent", `#${t.id}`)} ${itemText}`;
|
|
229
229
|
}
|
|
230
230
|
if (!options.expanded && todoList.length > MAX_COLLAPSED_ITEMS) {
|
|
231
|
-
listText += `\n${theme.fg("dim", `...
|
|
231
|
+
listText += `\n${theme.fg("dim", `... ${todoList.length - MAX_COLLAPSED_ITEMS} more`)}`;
|
|
232
232
|
}
|
|
233
233
|
return listText;
|
|
234
234
|
}
|
|
@@ -244,7 +244,7 @@ function renderTodoResult(result: unknown, options: { expanded: boolean }, theme
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
if (details.error) {
|
|
247
|
-
return new Text(theme.fg("error",
|
|
247
|
+
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
const todoList = details.todos;
|
|
@@ -281,7 +281,7 @@ function renderTodoResult(result: unknown, options: { expanded: boolean }, theme
|
|
|
281
281
|
default: {
|
|
282
282
|
const text = r.content[0];
|
|
283
283
|
const msg = text?.type === "text" ? (text.text ?? "") : "";
|
|
284
|
-
return new Text(theme.fg("dim", msg || "
|
|
284
|
+
return new Text(theme.fg("dim", msg || "Done"), 0, 0);
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
}
|
|
@@ -334,14 +334,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
334
334
|
return `[${mark}] #${t.id}: ${t.text}`;
|
|
335
335
|
})
|
|
336
336
|
.join("\n")
|
|
337
|
-
: "
|
|
337
|
+
: "No todos";
|
|
338
338
|
break;
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
case "add": {
|
|
342
342
|
if (!params.texts || params.texts.length === 0) {
|
|
343
343
|
return {
|
|
344
|
-
content: [{ type: "text" as const, text: "
|
|
344
|
+
content: [{ type: "text" as const, text: "Error: add requires texts parameter (non-empty array)" }],
|
|
345
345
|
details: {
|
|
346
346
|
action: "add" as const,
|
|
347
347
|
todos: [...todos],
|
|
@@ -354,7 +354,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
354
354
|
const trimmed = params.texts.map((t) => t.trim()).filter((t) => t.length > 0);
|
|
355
355
|
if (trimmed.length === 0) {
|
|
356
356
|
return {
|
|
357
|
-
content: [{ type: "text" as const, text: "
|
|
357
|
+
content: [{ type: "text" as const, text: "Error: texts must contain at least one non-empty string" }],
|
|
358
358
|
details: {
|
|
359
359
|
action: "add" as const,
|
|
360
360
|
todos: [...todos],
|
|
@@ -369,7 +369,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
369
369
|
todos.push({ id: nextId++, text: t, status: "pending" });
|
|
370
370
|
}
|
|
371
371
|
const endId = nextId - 1;
|
|
372
|
-
resultText =
|
|
372
|
+
resultText = `Added ${trimmed.length} todos (#${startId}-#${endId})`;
|
|
373
373
|
// v3: 新增 todo 表示未全部完成
|
|
374
374
|
allCompletedAtCount = null;
|
|
375
375
|
break;
|
|
@@ -378,7 +378,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
378
|
case "update": {
|
|
379
379
|
if (params.id === undefined) {
|
|
380
380
|
return {
|
|
381
|
-
content: [{ type: "text" as const, text: "
|
|
381
|
+
content: [{ type: "text" as const, text: "Error: update requires id parameter" }],
|
|
382
382
|
details: {
|
|
383
383
|
action: "update" as const,
|
|
384
384
|
todos: [...todos],
|
|
@@ -390,7 +390,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
390
390
|
}
|
|
391
391
|
if (params.status === undefined && params.text === undefined) {
|
|
392
392
|
return {
|
|
393
|
-
content: [{ type: "text" as const, text: "
|
|
393
|
+
content: [{ type: "text" as const, text: "Error: update requires at least status or text parameter" }],
|
|
394
394
|
details: {
|
|
395
395
|
action: "update" as const,
|
|
396
396
|
todos: [...todos],
|
|
@@ -402,7 +402,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
402
402
|
}
|
|
403
403
|
if (params.text !== undefined && params.text === "") {
|
|
404
404
|
return {
|
|
405
|
-
content: [{ type: "text" as const, text: "
|
|
405
|
+
content: [{ type: "text" as const, text: "Error: text cannot be empty string" }],
|
|
406
406
|
details: {
|
|
407
407
|
action: "update" as const,
|
|
408
408
|
todos: [...todos],
|
|
@@ -420,7 +420,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
420
420
|
content: [
|
|
421
421
|
{
|
|
422
422
|
type: "text" as const,
|
|
423
|
-
text:
|
|
423
|
+
text: `Error: status only accepts ${VALID_STATUSES.join(" / ")}`,
|
|
424
424
|
},
|
|
425
425
|
],
|
|
426
426
|
details: {
|
|
@@ -436,7 +436,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
436
436
|
const todo = todos.find((t) => t.id === params.id);
|
|
437
437
|
if (!todo) {
|
|
438
438
|
return {
|
|
439
|
-
content: [{ type: "text" as const, text: `Todo #${params.id}
|
|
439
|
+
content: [{ type: "text" as const, text: `Todo #${params.id} not found` }],
|
|
440
440
|
details: {
|
|
441
441
|
action: "update" as const,
|
|
442
442
|
todos: [...todos],
|
|
@@ -463,13 +463,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
463
463
|
todo.text = params.text;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
const parts: string[] = [
|
|
467
|
-
if (params.status !== undefined) parts.push(
|
|
468
|
-
if (params.text !== undefined) parts.push(
|
|
469
|
-
resultText = parts.join("
|
|
466
|
+
const parts: string[] = [`Updated todo #${todo.id}`];
|
|
467
|
+
if (params.status !== undefined) parts.push(`status → ${params.status}`);
|
|
468
|
+
if (params.text !== undefined) parts.push(`text → "${todo.text}"`);
|
|
469
|
+
resultText = parts.join(", ");
|
|
470
470
|
|
|
471
471
|
if (isLastCompletion) {
|
|
472
|
-
resultText += "\n\
|
|
472
|
+
resultText += "\n\nAll todos completed. Please summarize your work.";
|
|
473
473
|
}
|
|
474
474
|
|
|
475
475
|
// v3: 检查是否所有 todo 已完成
|
|
@@ -485,7 +485,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
485
485
|
case "delete": {
|
|
486
486
|
if (!params.ids || params.ids.length === 0) {
|
|
487
487
|
return {
|
|
488
|
-
content: [{ type: "text" as const, text: "
|
|
488
|
+
content: [{ type: "text" as const, text: "Error: delete requires ids parameter (non-empty array)" }],
|
|
489
489
|
details: {
|
|
490
490
|
action: "delete" as const,
|
|
491
491
|
todos: [...todos],
|
|
@@ -500,7 +500,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
500
500
|
if (missing.length > 0) {
|
|
501
501
|
const missingStr = missing.map((id) => `#${id}`).join(", ");
|
|
502
502
|
return {
|
|
503
|
-
content: [{ type: "text" as const, text:
|
|
503
|
+
content: [{ type: "text" as const, text: `Error: Todo ${missingStr} not found` }],
|
|
504
504
|
details: {
|
|
505
505
|
action: "delete" as const,
|
|
506
506
|
todos: [...todos],
|
|
@@ -518,7 +518,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
518
518
|
removedIds.push(id);
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
|
-
resultText =
|
|
521
|
+
resultText = `Deleted ${removedIds.length} items (#${removedIds.join(", #")}), ${todos.length} remaining`;
|
|
522
522
|
break;
|
|
523
523
|
}
|
|
524
524
|
|
|
@@ -526,7 +526,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
526
526
|
const count = todos.length;
|
|
527
527
|
todos = [];
|
|
528
528
|
nextId = 1;
|
|
529
|
-
resultText = count > 0 ?
|
|
529
|
+
resultText = count > 0 ? `Cleared ${count} todos` : "No todos to clear";
|
|
530
530
|
// v3: 手动清空后重置
|
|
531
531
|
allCompletedAtCount = null;
|
|
532
532
|
break;
|
|
@@ -534,7 +534,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
534
534
|
|
|
535
535
|
default:
|
|
536
536
|
return {
|
|
537
|
-
content: [{ type: "text" as const, text:
|
|
537
|
+
content: [{ type: "text" as const, text: `Unknown action: ${params.action}` }],
|
|
538
538
|
details: {
|
|
539
539
|
action: "list" as const,
|
|
540
540
|
todos: [...todos],
|
|
@@ -630,7 +630,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
630
630
|
return {
|
|
631
631
|
message: {
|
|
632
632
|
customType: "todo-auto-clear",
|
|
633
|
-
content:
|
|
633
|
+
content: `All ${count} todos completed, list auto-cleared.`,
|
|
634
634
|
display: true,
|
|
635
635
|
},
|
|
636
636
|
};
|
|
@@ -646,7 +646,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
646
646
|
return {
|
|
647
647
|
message: {
|
|
648
648
|
customType: "todo-verification-nudge",
|
|
649
|
-
content: "
|
|
649
|
+
content: "You completed 3+ tasks without a verification step. Consider adding a verification task before summarizing.",
|
|
650
650
|
display: true,
|
|
651
651
|
},
|
|
652
652
|
};
|
|
@@ -663,7 +663,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
663
663
|
return {
|
|
664
664
|
message: {
|
|
665
665
|
customType: "todo-reminder",
|
|
666
|
-
content: "
|
|
666
|
+
content: "The todo tool hasn't been used recently. If working on tasks, consider using it to track progress.",
|
|
667
667
|
display: true,
|
|
668
668
|
},
|
|
669
669
|
};
|
|
@@ -681,23 +681,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
681
681
|
name: "todo",
|
|
682
682
|
label: "Todo",
|
|
683
683
|
description:
|
|
684
|
-
"
|
|
685
|
-
"\n\
|
|
686
|
-
"\n- list
|
|
687
|
-
"\n- add
|
|
688
|
-
"\n- update
|
|
689
|
-
"\n- delete
|
|
690
|
-
"\n- clear
|
|
691
|
-
promptSnippet: "
|
|
684
|
+
"Manage a todo list." +
|
|
685
|
+
"\n\nAvailable actions:" +
|
|
686
|
+
"\n- list: View all todos" +
|
|
687
|
+
"\n- add: Batch add todos (requires texts array)" +
|
|
688
|
+
"\n- update: Update a todo (requires id, optional status/text)" +
|
|
689
|
+
"\n- delete: Batch delete todos (requires ids array)" +
|
|
690
|
+
"\n- clear: Clear all todos and reset IDs",
|
|
691
|
+
promptSnippet: "Lightweight task list for tracking progress on multi-step work, without requiring /goal mode",
|
|
692
692
|
promptGuidelines: [
|
|
693
|
-
"[
|
|
694
|
-
"[
|
|
695
|
-
"[
|
|
696
|
-
"[
|
|
697
|
-
"[
|
|
698
|
-
"[
|
|
699
|
-
"[
|
|
700
|
-
"[
|
|
693
|
+
"[Usage] Use for multi-step tasks (3+ steps), progress tracking, or when explicitly requested",
|
|
694
|
+
"[Not for] Single-step operations, trivial tasks, or when goal_manager is already active",
|
|
695
|
+
"[Timing] Create before starting work, mark completed immediately when done",
|
|
696
|
+
"[Status] At most one in_progress at a time; mark completed immediately",
|
|
697
|
+
"[Granularity] One todo per verifiable work unit, 3-8 items ideal",
|
|
698
|
+
"[Completion] All todos auto-clear when completed (retained for 2 turns)",
|
|
699
|
+
"[Verification] When completing 3+ tasks, consider adding a verification step",
|
|
700
|
+
"[Scope] Do not use todo as a substitute for goal_manager — they serve different purposes",
|
|
701
701
|
],
|
|
702
702
|
parameters: TodoParams,
|
|
703
703
|
|
|
@@ -732,10 +732,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
732
732
|
|
|
733
733
|
// ── Command: /todos ─────────────────────────────────
|
|
734
734
|
pi.registerCommand("todos", {
|
|
735
|
-
description: "
|
|
735
|
+
description: "View all todos for the current branch",
|
|
736
736
|
handler: async (_args: string | undefined, ctx: ExtensionCommandContext) => {
|
|
737
737
|
if (!ctx.hasUI) {
|
|
738
|
-
ctx.ui.notify("/todos
|
|
738
|
+
ctx.ui.notify("/todos requires interactive mode", "error");
|
|
739
739
|
return;
|
|
740
740
|
}
|
|
741
741
|
|