@pi-archimedes/todo 2.3.0 → 2.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 +6 -6
- package/package.json +6 -2
- package/src/index.test.ts +160 -0
- package/src/index.ts +12 -2
- package/src/prepare-args.test.ts +177 -109
- package/src/prepare-args.ts +131 -71
- package/src/state-manager.test.ts +113 -42
- package/src/state-manager.ts +19 -18
- package/src/tool.test.ts +60 -3
- package/src/tool.ts +25 -18
- package/src/types.ts +11 -13
- package/src/ui/todo-widget.ts +9 -7
package/src/tool.test.ts
CHANGED
|
@@ -19,6 +19,7 @@ const VALIDATION_ERROR_TEXT = [
|
|
|
19
19
|
// tell apart the status colour, the glyph, and the label.
|
|
20
20
|
const theme = {
|
|
21
21
|
fg: (token: unknown, text: unknown) => `[${token}]${text}`,
|
|
22
|
+
strikethrough: (text: unknown) => `[s]${text}`,
|
|
22
23
|
} as unknown as Theme;
|
|
23
24
|
|
|
24
25
|
const tool = createManageTodoListTool(new TodoStateManager(), () => {});
|
|
@@ -72,6 +73,40 @@ describe("renderResult — harness error shapes", () => {
|
|
|
72
73
|
});
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
describe("tool schema", () => {
|
|
77
|
+
const params = tool.parameters as unknown as {
|
|
78
|
+
properties: Record<string, { type?: string; enum?: string[]; items?: unknown }>;
|
|
79
|
+
required?: string[];
|
|
80
|
+
};
|
|
81
|
+
const itemSchema = params.properties.todoList?.items as unknown as {
|
|
82
|
+
properties: Record<string, { type?: string; enum?: string[] }>;
|
|
83
|
+
required?: string[];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
it("declares content + canonical status enum, with no id/title and optional description", () => {
|
|
87
|
+
const props = itemSchema.properties;
|
|
88
|
+
expect(props.id).toBeUndefined();
|
|
89
|
+
expect(props.title).toBeUndefined();
|
|
90
|
+
expect(props.content?.type).toBe("string");
|
|
91
|
+
expect(props.status?.enum).toEqual(["pending", "in_progress", "completed"]);
|
|
92
|
+
expect(itemSchema.required).toContain("content");
|
|
93
|
+
expect(itemSchema.required).toContain("status");
|
|
94
|
+
expect(itemSchema.required).not.toContain("description");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("keeps the manage_todo_list tool name, op and prepareArguments wiring", () => {
|
|
98
|
+
expect(tool.name).toBe("manage_todo_list");
|
|
99
|
+
expect(typeof tool.prepareArguments).toBe("function");
|
|
100
|
+
expect(params.properties.operation?.enum).toEqual(["write", "read"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("model-facing description uses canonical status tokens only", () => {
|
|
104
|
+
expect(tool.description).not.toMatch(/not-started|in-progress/);
|
|
105
|
+
expect(tool.description).toContain("in_progress");
|
|
106
|
+
expect(tool.description).toContain("\"content\": \"Fix the auth middleware\"");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
75
110
|
describe("renderResult — successful results unchanged", () => {
|
|
76
111
|
it("renders the completed counter from details.todos", () => {
|
|
77
112
|
const result = {
|
|
@@ -79,13 +114,35 @@ describe("renderResult — successful results unchanged", () => {
|
|
|
79
114
|
details: {
|
|
80
115
|
operation: "write" as const,
|
|
81
116
|
todos: [
|
|
82
|
-
{
|
|
83
|
-
{
|
|
84
|
-
{
|
|
117
|
+
{ content: "a", status: "completed" as const },
|
|
118
|
+
{ content: "b", status: "completed" as const },
|
|
119
|
+
{ content: "c", status: "completed" as const },
|
|
85
120
|
],
|
|
86
121
|
},
|
|
87
122
|
} as unknown as AgentToolResult<TodoDetails>;
|
|
88
123
|
const [line] = renderLines(result, false).split("\n");
|
|
89
124
|
expect(line).toContain("3/3 completed");
|
|
90
125
|
});
|
|
126
|
+
|
|
127
|
+
it("numbers items by array position and renders content", () => {
|
|
128
|
+
const result = {
|
|
129
|
+
content: [{ type: "text" as const, text: "1/3 completed." }],
|
|
130
|
+
details: {
|
|
131
|
+
operation: "write" as const,
|
|
132
|
+
todos: [
|
|
133
|
+
{ content: "First", status: "completed" as const },
|
|
134
|
+
{ content: "Second", status: "in_progress" as const },
|
|
135
|
+
{ content: "Third", status: "pending" as const },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
} as unknown as AgentToolResult<TodoDetails>;
|
|
139
|
+
const lines = renderLines(result, true);
|
|
140
|
+
expect(lines).toContain("[accent]1.");
|
|
141
|
+
expect(lines).toContain("[accent]2.");
|
|
142
|
+
expect(lines).toContain("[accent]3.");
|
|
143
|
+
expect(lines).toContain("[s]First");
|
|
144
|
+
expect(lines).toContain("[warning]Second");
|
|
145
|
+
expect(lines).toContain("[muted]Third");
|
|
146
|
+
expect(lines).not.toContain("undefined");
|
|
147
|
+
});
|
|
91
148
|
});
|
package/src/tool.ts
CHANGED
|
@@ -9,14 +9,15 @@ import { STATUS_ICONS } from "./types.js";
|
|
|
9
9
|
import type { TodoDetails } from "./types.js";
|
|
10
10
|
|
|
11
11
|
const TodoItemSchema = Type.Object({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
description: Type.String({
|
|
15
|
-
description: "Detailed context, requirements, or implementation notes. Include file paths, specific methods, or acceptance criteria.",
|
|
12
|
+
content: Type.String({
|
|
13
|
+
description: "Short imperative label for the task (3-10 words). Displayed in UI. Example: \"Fix the auth middleware\".",
|
|
16
14
|
}),
|
|
17
|
-
status: StringEnum(["
|
|
18
|
-
description: "
|
|
15
|
+
status: StringEnum(["pending", "in_progress", "completed"] as const, {
|
|
16
|
+
description: "pending: Not begun | in_progress: Currently working (multiple allowed for parallel work/subagents) | completed: Fully finished with no blockers",
|
|
19
17
|
}),
|
|
18
|
+
description: Type.Optional(Type.String({
|
|
19
|
+
description: "Optional detailed context: file paths, specific methods, or acceptance criteria.",
|
|
20
|
+
})),
|
|
20
21
|
});
|
|
21
22
|
|
|
22
23
|
export const ManageTodoListParams = Type.Object({
|
|
@@ -38,7 +39,7 @@ When to use this tool:
|
|
|
38
39
|
- Complex multi-step work requiring planning and tracking
|
|
39
40
|
- When user provides multiple tasks or requests (numbered, comma-separated)
|
|
40
41
|
- After receiving new instructions that require multiple steps
|
|
41
|
-
- BEFORE starting work on any todo (mark as
|
|
42
|
+
- BEFORE starting work on any todo (mark as in_progress)
|
|
42
43
|
- IMMEDIATELY after completing each todo (mark completed individually)
|
|
43
44
|
- When breaking down larger tasks into smaller actionable steps
|
|
44
45
|
- To give users visibility into your progress and planning
|
|
@@ -50,14 +51,19 @@ When NOT to use:
|
|
|
50
51
|
|
|
51
52
|
CRITICAL workflow:
|
|
52
53
|
1. Plan tasks by writing todo list with specific, actionable items
|
|
53
|
-
2. Mark todo(s) as
|
|
54
|
+
2. Mark todo(s) as in_progress before starting work
|
|
54
55
|
3. Complete the work for that specific todo
|
|
55
56
|
4. Mark that todo as completed IMMEDIATELY
|
|
56
57
|
5. Move to next todo and repeat
|
|
57
58
|
|
|
59
|
+
Todo item shape:
|
|
60
|
+
{"content": "Fix the auth middleware", "status": "pending", "description": "optional: file paths, acceptance criteria"}
|
|
61
|
+
- content: the short displayed label — do NOT assign ids (numbering is order)
|
|
62
|
+
- status: exactly one of pending | in_progress | completed
|
|
63
|
+
|
|
58
64
|
Todo states:
|
|
59
|
-
-
|
|
60
|
-
-
|
|
65
|
+
- pending: Todo not yet begun
|
|
66
|
+
- in_progress: Currently working (multiple allowed for parallel work/subagents)
|
|
61
67
|
- completed: Finished successfully
|
|
62
68
|
|
|
63
69
|
IMPORTANT: Mark todos completed as soon as they are done. Do not batch completions.
|
|
@@ -188,21 +194,22 @@ export function createManageTodoListTool(state: TodoStateManager, onUpdate: () =
|
|
|
188
194
|
let text = renderStatusLabel("success", label, theme);
|
|
189
195
|
|
|
190
196
|
if (expanded) {
|
|
191
|
-
for (
|
|
192
|
-
const
|
|
197
|
+
for (let i = 0; i < todos.length; i++) {
|
|
198
|
+
const todo = todos[i]!;
|
|
199
|
+
const iconChar = (STATUS_ICONS[todo.status] ?? "?").trim();
|
|
193
200
|
const icon =
|
|
194
201
|
todo.status === "completed"
|
|
195
202
|
? theme.fg("success", iconChar)
|
|
196
|
-
: todo.status === "
|
|
203
|
+
: todo.status === "in_progress"
|
|
197
204
|
? theme.fg("warning", iconChar)
|
|
198
205
|
: theme.fg("dim", iconChar);
|
|
199
206
|
const title =
|
|
200
207
|
todo.status === "completed"
|
|
201
|
-
? theme.fg("dim", theme.strikethrough(todo.
|
|
202
|
-
: todo.status === "
|
|
203
|
-
? theme.fg("warning", todo.
|
|
204
|
-
: theme.fg("muted", todo.
|
|
205
|
-
text += `\n ${icon} ${theme.fg("accent", `${
|
|
208
|
+
? theme.fg("dim", theme.strikethrough(todo.content ?? ""))
|
|
209
|
+
: todo.status === "in_progress"
|
|
210
|
+
? theme.fg("warning", todo.content ?? "")
|
|
211
|
+
: theme.fg("muted", todo.content ?? "");
|
|
212
|
+
text += `\n ${icon} ${theme.fg("accent", `${i + 1}.`)} ${title}`;
|
|
206
213
|
}
|
|
207
214
|
}
|
|
208
215
|
|
package/src/types.ts
CHANGED
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
* Core types for the todo list extension.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
/** Status of a single todo item */
|
|
6
|
-
export type TodoStatus = "
|
|
5
|
+
/** Status of a single todo item (Claude Code aligned). */
|
|
6
|
+
export type TodoStatus = "pending" | "in_progress" | "completed";
|
|
7
7
|
|
|
8
|
-
/** A single todo item */
|
|
8
|
+
/** A single todo item. No `id` — display numbering is array position. */
|
|
9
9
|
export interface TodoItem {
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
/** Detailed context, requirements, or implementation notes. */
|
|
15
|
-
description: string;
|
|
10
|
+
/** Short imperative label of the task (3-10 words). Displayed in UI. */
|
|
11
|
+
content: string;
|
|
12
|
+
/** Optional detailed context: file paths, methods, acceptance criteria. */
|
|
13
|
+
description?: string;
|
|
16
14
|
/** Current status. */
|
|
17
15
|
status: TodoStatus;
|
|
18
16
|
}
|
|
@@ -29,7 +27,7 @@ export interface TodoStats {
|
|
|
29
27
|
total: number;
|
|
30
28
|
completed: number;
|
|
31
29
|
inProgress: number;
|
|
32
|
-
|
|
30
|
+
pending: number;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
/** Validation result */
|
|
@@ -40,7 +38,7 @@ export interface ValidationResult {
|
|
|
40
38
|
|
|
41
39
|
/** Status icons for each todo state */
|
|
42
40
|
export const STATUS_ICONS: Record<TodoStatus, string> = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
completed: "✓",
|
|
42
|
+
in_progress: "◉ ",
|
|
43
|
+
pending: "○",
|
|
46
44
|
};
|
package/src/ui/todo-widget.ts
CHANGED
|
@@ -52,7 +52,9 @@ export function updateWidget(
|
|
|
52
52
|
for (const source of sortedSources) {
|
|
53
53
|
const todos = currentSub.get(source)!;
|
|
54
54
|
if (todos.length > 0) {
|
|
55
|
-
|
|
55
|
+
// Per-child sources are subagent:<agent>:<uuid>; render only the agent
|
|
56
|
+
// name so the header stays `subagent (myagent)` regardless of suffix.
|
|
57
|
+
const agentName = source.replace("subagent:", "").split(":")[0];
|
|
56
58
|
columns.push({ header: `subagent (${agentName})`, todos });
|
|
57
59
|
}
|
|
58
60
|
}
|
|
@@ -103,7 +105,7 @@ export function updateWidget(
|
|
|
103
105
|
const todo = column.todos[todoIndex];
|
|
104
106
|
if (todo) {
|
|
105
107
|
const icon = getStatusIcon(todo.status, theme);
|
|
106
|
-
const idStr = theme.fg("accent", `${
|
|
108
|
+
const idStr = theme.fg("accent", `${todoIndex + 1}.`);
|
|
107
109
|
const title = formatTodoTitle(todo, theme);
|
|
108
110
|
cellText = ` ${icon} ${idStr} ${title}`;
|
|
109
111
|
} else {
|
|
@@ -142,18 +144,18 @@ export function updateWidget(
|
|
|
142
144
|
function getStatusIcon(status: TodoItem["status"], theme: Theme): string {
|
|
143
145
|
const icon = STATUS_ICONS[status] ?? "?";
|
|
144
146
|
if (status === "completed") return theme.fg("success", icon);
|
|
145
|
-
if (status === "
|
|
147
|
+
if (status === "in_progress") return theme.fg("warning", icon.trim());
|
|
146
148
|
return theme.fg("dim", icon);
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
function formatTodoTitle(todo: TodoItem, theme: Theme): string {
|
|
150
152
|
if (todo.status === "completed") {
|
|
151
|
-
return theme.fg("dim", theme.strikethrough(todo.
|
|
153
|
+
return theme.fg("dim", theme.strikethrough(todo.content ?? ""));
|
|
152
154
|
}
|
|
153
|
-
if (todo.status === "
|
|
154
|
-
return theme.fg("warning", todo.
|
|
155
|
+
if (todo.status === "in_progress") {
|
|
156
|
+
return theme.fg("warning", todo.content ?? "");
|
|
155
157
|
}
|
|
156
|
-
return theme.fg("muted", todo.
|
|
158
|
+
return theme.fg("muted", todo.content ?? "");
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
/** Clear the widget */
|