@agnishc/edb-todo 0.8.1 → 0.10.3
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/CHANGELOG.md +50 -0
- package/README.md +151 -18
- package/package.json +1 -1
- package/src/auto-clear.ts +87 -0
- package/src/component.ts +314 -69
- package/src/config.ts +25 -0
- package/src/file-store.ts +408 -0
- package/src/index.ts +584 -82
- package/src/process-tracker.ts +146 -0
- package/src/prompt.ts +19 -17
- package/src/schemas.ts +55 -24
- package/src/state.ts +251 -72
- package/src/types.ts +18 -2
package/src/index.ts
CHANGED
|
@@ -1,106 +1,249 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* edb-todo
|
|
3
3
|
*
|
|
4
|
-
* Task management extension
|
|
5
|
-
* agents to lose track of the original plan as context grows.
|
|
4
|
+
* Task management extension with pi-tasks tool names, descriptions, and behavior.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* branch, so /tree navigation and forking work correctly
|
|
6
|
+
* Tools:
|
|
7
|
+
* TaskCreate — Create a structured task
|
|
8
|
+
* TaskList — List all tasks with status and blocked-by info
|
|
9
|
+
* TaskGet — Get full task details, description, dependencies
|
|
10
|
+
* TaskUpdate — Update status, fields, dependencies; status:"deleted" removes
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
* todo_read — read the current task list
|
|
16
|
-
* Command: /todos — open interactive full-screen task viewer
|
|
12
|
+
* Command: /todos — interactive task manager with settings panel
|
|
17
13
|
*/
|
|
18
14
|
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
19
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
17
|
import { Text } from "@earendil-works/pi-tui";
|
|
21
18
|
import { Type } from "typebox";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import
|
|
19
|
+
import { AutoClearManager } from "./auto-clear.js";
|
|
20
|
+
import { openTodosMenu, TodoViewComponent } from "./component.js";
|
|
21
|
+
import { loadTodoConfig } from "./config.js";
|
|
22
|
+
import { FileTaskStore } from "./file-store.js";
|
|
23
|
+
import { ProcessTracker } from "./process-tracker.js";
|
|
24
|
+
import { buildSystemPromptBlock, formatListForLLM } from "./prompt.js";
|
|
25
|
+
import { TodoCreateParams, TodoGetParams, TodoUpdateParams } from "./schemas.js";
|
|
26
|
+
import { priorityColor, priorityLabel, renderTaskListResult, TodoWidget } from "./state.js";
|
|
27
|
+
import type { TaskDetails, TaskPriority } from "./types.js";
|
|
28
|
+
|
|
29
|
+
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const TASK_TOOL_NAMES = new Set(["TaskCreate", "TaskList", "TaskGet", "TaskUpdate", "TaskOutput", "TaskStop"]);
|
|
32
|
+
const REMINDER_INTERVAL = 4;
|
|
33
|
+
const AUTO_CLEAR_DELAY = 4;
|
|
34
|
+
|
|
35
|
+
const SYSTEM_REMINDER = `<system-reminder>
|
|
36
|
+
The task tools haven't been used recently. If you're working on tasks that would benefit from tracking progress, consider using TaskCreate to add new tasks and TaskUpdate to update task status (set to in_progress when starting, completed when done). Also consider cleaning up the task list if it has become stale. Only use these if relevant to the current work. This is just a gentle reminder - ignore if not applicable. Make sure that you NEVER mention this reminder to the user
|
|
37
|
+
</system-reminder>`;
|
|
27
38
|
|
|
28
39
|
// ── Extension ──────────────────────────────────────────────────────────────────
|
|
29
40
|
|
|
30
41
|
export default function todoExtension(pi: ExtensionAPI): void {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
let cwd = process.cwd();
|
|
43
|
+
let cfg = loadTodoConfig(cwd);
|
|
44
|
+
const taskScope = cfg.taskScope ?? "session";
|
|
45
|
+
|
|
46
|
+
function resolveStorePath(sessionId?: string): string | undefined {
|
|
47
|
+
const envVal = process.env.PI_TODO;
|
|
48
|
+
if (envVal === "off") return undefined;
|
|
49
|
+
if (envVal?.startsWith("/")) return envVal;
|
|
50
|
+
if (envVal?.startsWith(".")) return resolve(envVal);
|
|
51
|
+
if (envVal) return join(process.env.HOME ?? "~", ".pi", "tasks", `${envVal}.json`);
|
|
52
|
+
|
|
53
|
+
if (taskScope === "memory") return undefined;
|
|
54
|
+
if (taskScope === "session" && sessionId) {
|
|
55
|
+
return join(cwd, ".pi", "tasks", `tasks-${sessionId}.json`);
|
|
56
|
+
}
|
|
57
|
+
if (taskScope === "session") return undefined;
|
|
58
|
+
return join(cwd, ".pi", "tasks", "tasks.json");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let store = new FileTaskStore(resolveStorePath());
|
|
62
|
+
const tracker = new ProcessTracker();
|
|
63
|
+
const widget = new TodoWidget(store);
|
|
64
|
+
const autoClear = new AutoClearManager(
|
|
65
|
+
() => store,
|
|
66
|
+
() => cfg.autoClearCompleted ?? "on_list_complete",
|
|
67
|
+
AUTO_CLEAR_DELAY,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let storeUpgraded = false;
|
|
71
|
+
let persistedTasksShown = false;
|
|
72
|
+
|
|
73
|
+
function upgradeStoreIfNeeded(sessionId?: string) {
|
|
74
|
+
if (storeUpgraded) return;
|
|
75
|
+
if (taskScope === "session" && !process.env.PI_TODO) {
|
|
76
|
+
const path = resolveStorePath(sessionId);
|
|
77
|
+
if (path) {
|
|
78
|
+
store = new FileTaskStore(path);
|
|
79
|
+
widget.setStore(store);
|
|
80
|
+
autoClear.getStore = () => store;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
storeUpgraded = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function showPersistedTasks(isResume = false) {
|
|
87
|
+
if (persistedTasksShown) return;
|
|
88
|
+
persistedTasksShown = true;
|
|
89
|
+
const tasks = store.list();
|
|
90
|
+
if (tasks.length > 0) {
|
|
91
|
+
if (!isResume && tasks.every((t) => t.status === "completed")) {
|
|
92
|
+
store.clearCompleted();
|
|
93
|
+
if (taskScope === "session") store.deleteFileIfEmpty();
|
|
94
|
+
} else {
|
|
95
|
+
widget.update();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Turn tracking ──────────────────────────────────────────────────────────
|
|
101
|
+
let currentTurn = 0;
|
|
102
|
+
let lastTaskToolUseTurn = 0;
|
|
103
|
+
let reminderInjectedThisCycle = false;
|
|
104
|
+
|
|
105
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
106
|
+
currentTurn++;
|
|
107
|
+
cwd = ctx.cwd;
|
|
108
|
+
cfg = loadTodoConfig(cwd);
|
|
109
|
+
widget.setUICtx(ctx.ui);
|
|
110
|
+
upgradeStoreIfNeeded(ctx.sessionManager.getSessionId());
|
|
111
|
+
if (autoClear.onTurnStart(currentTurn)) widget.update();
|
|
35
112
|
});
|
|
36
113
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
114
|
+
// ── System-reminder injection ──────────────────────────────────────────────
|
|
115
|
+
pi.on("tool_result", async (event) => {
|
|
116
|
+
if (TASK_TOOL_NAMES.has(event.toolName)) {
|
|
117
|
+
lastTaskToolUseTurn = currentTurn;
|
|
118
|
+
reminderInjectedThisCycle = false;
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
if (currentTurn - lastTaskToolUseTurn < REMINDER_INTERVAL) return {};
|
|
122
|
+
if (reminderInjectedThisCycle) return {};
|
|
123
|
+
if (store.list().length === 0) return {};
|
|
124
|
+
reminderInjectedThisCycle = true;
|
|
125
|
+
lastTaskToolUseTurn = currentTurn;
|
|
126
|
+
return {
|
|
127
|
+
content: [...event.content, { type: "text" as const, text: SYSTEM_REMINDER }],
|
|
128
|
+
};
|
|
40
129
|
});
|
|
41
130
|
|
|
42
|
-
// ── System-prompt injection
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
131
|
+
// ── System-prompt injection ────────────────────────────────────────────────
|
|
132
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
133
|
+
cwd = ctx.cwd;
|
|
134
|
+
cfg = loadTodoConfig(cwd);
|
|
135
|
+
widget.setUICtx(ctx.ui);
|
|
136
|
+
upgradeStoreIfNeeded(ctx.sessionManager.getSessionId());
|
|
137
|
+
showPersistedTasks();
|
|
138
|
+
const block = buildSystemPromptBlock(store);
|
|
47
139
|
if (!block) return;
|
|
48
140
|
return { systemPrompt: `${event.systemPrompt}\n\n${block}` };
|
|
49
141
|
});
|
|
50
142
|
|
|
51
|
-
// ── Post-turn widget refresh ───────────────────────────────────────────
|
|
52
143
|
pi.on("agent_end", async (_event, ctx) => {
|
|
53
|
-
|
|
144
|
+
widget.setUICtx(ctx.ui);
|
|
145
|
+
widget.update();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── Session lifecycle ──────────────────────────────────────────────────────
|
|
149
|
+
pi.on("session_start", async (event, ctx) => {
|
|
150
|
+
const isResume = event.reason === "resume";
|
|
151
|
+
cwd = ctx.cwd;
|
|
152
|
+
cfg = loadTodoConfig(cwd);
|
|
153
|
+
storeUpgraded = false;
|
|
154
|
+
persistedTasksShown = false;
|
|
155
|
+
currentTurn = 0;
|
|
156
|
+
lastTaskToolUseTurn = 0;
|
|
157
|
+
reminderInjectedThisCycle = false;
|
|
158
|
+
autoClear.reset();
|
|
159
|
+
if (!isResume && taskScope === "memory") store.clearAll();
|
|
160
|
+
upgradeStoreIfNeeded(ctx.sessionManager.getSessionId());
|
|
161
|
+
widget.setUICtx(ctx.ui);
|
|
162
|
+
showPersistedTasks(isResume);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
166
|
+
widget.setUICtx(ctx.ui);
|
|
167
|
+
widget.update();
|
|
54
168
|
});
|
|
55
169
|
|
|
56
|
-
// ── Tool:
|
|
170
|
+
// ── Tool: TaskCreate ───────────────────────────────────────────────────────
|
|
57
171
|
pi.registerTool({
|
|
58
|
-
name: "
|
|
59
|
-
label: "
|
|
60
|
-
description:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
172
|
+
name: "TaskCreate",
|
|
173
|
+
label: "TaskCreate",
|
|
174
|
+
description: `Use this tool to create a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
|
|
175
|
+
It also helps the user understand the progress of the task and overall progress of their requests.
|
|
176
|
+
|
|
177
|
+
## When to Use This Tool
|
|
178
|
+
|
|
179
|
+
Use this tool proactively in these scenarios:
|
|
180
|
+
|
|
181
|
+
- Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
|
|
182
|
+
- Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
|
|
183
|
+
- User explicitly requests todo list - When the user directly asks you to use the todo list
|
|
184
|
+
- User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
|
|
185
|
+
- After receiving new instructions - Immediately capture user requirements as tasks
|
|
186
|
+
- When you start working on a task - Mark it as in_progress BEFORE beginning work
|
|
187
|
+
- After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation
|
|
188
|
+
|
|
189
|
+
## When NOT to Use This Tool
|
|
190
|
+
|
|
191
|
+
Skip using this tool when:
|
|
192
|
+
- There is only a single, straightforward task
|
|
193
|
+
- The task is trivial and tracking it provides no organizational benefit
|
|
194
|
+
- The task can be completed in less than 3 trivial steps
|
|
195
|
+
- The task is purely conversational or informational
|
|
196
|
+
|
|
197
|
+
NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
|
|
198
|
+
|
|
199
|
+
## Task Fields
|
|
200
|
+
|
|
201
|
+
- **content**: A brief, actionable title in imperative form (e.g., "Fix authentication bug in login flow")
|
|
202
|
+
- **description**: Detailed description of what needs to be done, including context and acceptance criteria
|
|
203
|
+
- **activeForm** (optional): Present continuous form shown in the spinner when in_progress (e.g., "Fixing authentication bug"). If omitted, the spinner shows the content instead.
|
|
204
|
+
- **priority**: high / medium / low
|
|
205
|
+
|
|
206
|
+
All tasks are created with status \`pending\`.
|
|
207
|
+
|
|
208
|
+
## Tips
|
|
209
|
+
|
|
210
|
+
- Create tasks with clear, specific content that describes the outcome
|
|
211
|
+
- Include enough detail in the description for another agent to understand and complete the task
|
|
212
|
+
- After creating tasks, use TaskUpdate to set up dependencies (blocks/blockedBy) if needed
|
|
213
|
+
- Check TaskList first to avoid creating duplicate tasks`,
|
|
65
214
|
promptGuidelines: [
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
"Only one task should be 'in_progress' at a time unless tasks are genuinely parallel.",
|
|
70
|
-
"Immediately after completing a task, call todo_write to mark it 'completed'. " +
|
|
71
|
-
"Do not batch completions — mark each task done as soon as it is finished.",
|
|
72
|
-
"todo_write REPLACES the entire list. Always include ALL tasks (both changed and unchanged) in every call.",
|
|
215
|
+
"When working on complex multi-step tasks, use TaskCreate to track progress and TaskUpdate to update status.",
|
|
216
|
+
"Mark tasks as in_progress before starting work and completed when done.",
|
|
217
|
+
"Use TaskList to check for available work after completing a task.",
|
|
73
218
|
],
|
|
74
|
-
parameters:
|
|
219
|
+
parameters: TodoCreateParams,
|
|
75
220
|
|
|
76
221
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
updateWidget(ctx);
|
|
87
|
-
|
|
222
|
+
autoClear.resetBatchCountdown();
|
|
223
|
+
const task = store.create(params.content, {
|
|
224
|
+
description: params.description,
|
|
225
|
+
priority: params.priority as TaskPriority | undefined,
|
|
226
|
+
activeForm: params.activeForm,
|
|
227
|
+
metadata: params.metadata,
|
|
228
|
+
});
|
|
229
|
+
widget.setUICtx(ctx.ui);
|
|
230
|
+
widget.update();
|
|
88
231
|
return {
|
|
89
|
-
content: [{ type: "text", text:
|
|
90
|
-
details: { tasks: [...
|
|
232
|
+
content: [{ type: "text", text: `Task #${task.id} created successfully: ${task.content}` }],
|
|
233
|
+
details: { tasks: [...store.list()] } satisfies TaskDetails,
|
|
91
234
|
};
|
|
92
235
|
},
|
|
93
236
|
|
|
94
237
|
renderCall(args, theme) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
238
|
+
const content = (args.content as string) ?? "";
|
|
239
|
+
const priority = (args.priority as string) ?? "medium";
|
|
240
|
+
const pColor = priorityColor(priority as TaskPriority);
|
|
241
|
+
const pLabel = theme.fg(pColor, priorityLabel(priority as TaskPriority));
|
|
242
|
+
return new Text(
|
|
243
|
+
`${theme.fg("toolTitle", theme.bold("TaskCreate ")) + pLabel} ${theme.fg("muted", content)}`,
|
|
244
|
+
0,
|
|
245
|
+
0,
|
|
246
|
+
);
|
|
104
247
|
},
|
|
105
248
|
|
|
106
249
|
renderResult(result, { expanded }, theme) {
|
|
@@ -108,23 +251,65 @@ export default function todoExtension(pi: ExtensionAPI): void {
|
|
|
108
251
|
},
|
|
109
252
|
});
|
|
110
253
|
|
|
111
|
-
// ── Tool:
|
|
254
|
+
// ── Tool: TaskList ─────────────────────────────────────────────────────────
|
|
112
255
|
pi.registerTool({
|
|
113
|
-
name: "
|
|
114
|
-
label: "
|
|
115
|
-
description:
|
|
116
|
-
|
|
256
|
+
name: "TaskList",
|
|
257
|
+
label: "TaskList",
|
|
258
|
+
description: `Use this tool to list all tasks in the task list.
|
|
259
|
+
|
|
260
|
+
## When to Use This Tool
|
|
261
|
+
|
|
262
|
+
- To see what tasks are available to work on (status: pending, not blocked)
|
|
263
|
+
- To check overall progress on the project
|
|
264
|
+
- To find tasks that are blocked and need dependencies resolved
|
|
265
|
+
- After completing a task, to check for newly unblocked work
|
|
266
|
+
- **Prefer working on tasks in ID order** (lowest ID first) when multiple tasks are available
|
|
267
|
+
|
|
268
|
+
## Output
|
|
269
|
+
|
|
270
|
+
Returns a summary of each task:
|
|
271
|
+
- **id**: Task identifier (use with TaskGet, TaskUpdate)
|
|
272
|
+
- **content**: Brief description of the task
|
|
273
|
+
- **status**: pending, in_progress, or completed
|
|
274
|
+
- **priority**: high, medium, or low
|
|
275
|
+
- **blockedBy**: Open task IDs that must be resolved first
|
|
276
|
+
|
|
277
|
+
Use TaskGet with a specific task ID to view full details including description.`,
|
|
117
278
|
parameters: Type.Object({}),
|
|
118
279
|
|
|
119
280
|
async execute() {
|
|
281
|
+
const tasks = store.list();
|
|
282
|
+
if (tasks.length === 0)
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: "No tasks found" }],
|
|
285
|
+
details: { tasks: [] } satisfies TaskDetails,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const statusOrder: Record<string, number> = { pending: 0, in_progress: 1, completed: 2 };
|
|
289
|
+
const sorted = [...tasks].sort((a, b) => {
|
|
290
|
+
const so = (statusOrder[a.status] ?? 0) - (statusOrder[b.status] ?? 0);
|
|
291
|
+
if (so !== 0) return so;
|
|
292
|
+
return a.id.localeCompare(b.id);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const lines = sorted.map((task) => {
|
|
296
|
+
let line = `[${task.status}] [${task.priority}] #${task.id} ${task.content}`;
|
|
297
|
+
const openBlockers = task.blockedBy.filter((bid) => {
|
|
298
|
+
const b = store.get(bid);
|
|
299
|
+
return b && b.status !== "completed";
|
|
300
|
+
});
|
|
301
|
+
if (openBlockers.length > 0) line += ` [blocked by ${openBlockers.map((id) => `#${id}`).join(", ")}]`;
|
|
302
|
+
return line;
|
|
303
|
+
});
|
|
304
|
+
|
|
120
305
|
return {
|
|
121
|
-
content: [{ type: "text", text:
|
|
122
|
-
details: { tasks:
|
|
306
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
307
|
+
details: { tasks: sorted } satisfies TaskDetails,
|
|
123
308
|
};
|
|
124
309
|
},
|
|
125
310
|
|
|
126
311
|
renderCall(_args, theme) {
|
|
127
|
-
return new Text(theme.fg("toolTitle", theme.bold("
|
|
312
|
+
return new Text(theme.fg("toolTitle", theme.bold("TaskList")), 0, 0);
|
|
128
313
|
},
|
|
129
314
|
|
|
130
315
|
renderResult(result, { expanded }, theme) {
|
|
@@ -132,16 +317,333 @@ export default function todoExtension(pi: ExtensionAPI): void {
|
|
|
132
317
|
},
|
|
133
318
|
});
|
|
134
319
|
|
|
135
|
-
// ──
|
|
320
|
+
// ── Tool: TaskGet ──────────────────────────────────────────────────────────
|
|
321
|
+
pi.registerTool({
|
|
322
|
+
name: "TaskGet",
|
|
323
|
+
label: "TaskGet",
|
|
324
|
+
description: `Use this tool to retrieve a task by its ID from the task list.
|
|
325
|
+
|
|
326
|
+
## When to Use This Tool
|
|
327
|
+
|
|
328
|
+
- When you need the full description and context before starting work on a task
|
|
329
|
+
- To understand task dependencies (what it blocks, what blocks it)
|
|
330
|
+
- After being assigned a task, to get complete requirements
|
|
331
|
+
|
|
332
|
+
## Output
|
|
333
|
+
|
|
334
|
+
Returns full task details:
|
|
335
|
+
- **content**: Task title
|
|
336
|
+
- **description**: Detailed requirements and context
|
|
337
|
+
- **status**: pending, in_progress, or completed
|
|
338
|
+
- **priority**: high, medium, or low
|
|
339
|
+
- **blocks**: Tasks waiting on this one to complete
|
|
340
|
+
- **blockedBy**: Tasks that must complete before this one can start
|
|
341
|
+
|
|
342
|
+
## Tips
|
|
343
|
+
|
|
344
|
+
- After fetching a task, verify its blockedBy list is empty before beginning work.
|
|
345
|
+
- Use TaskList to see all tasks in summary form.`,
|
|
346
|
+
parameters: TodoGetParams,
|
|
347
|
+
|
|
348
|
+
async execute(_id, params) {
|
|
349
|
+
const task = store.get(params.id);
|
|
350
|
+
if (!task) {
|
|
351
|
+
return {
|
|
352
|
+
content: [{ type: "text", text: `Task not found` }],
|
|
353
|
+
details: undefined,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const desc = task.description?.replace(/\\n/g, "\n") ?? "(no description)";
|
|
358
|
+
const lines: string[] = [
|
|
359
|
+
`Task #${task.id}: ${task.content}`,
|
|
360
|
+
`Status: ${task.status}`,
|
|
361
|
+
`Priority: ${task.priority}`,
|
|
362
|
+
];
|
|
363
|
+
if (task.owner) lines.push(`Owner: ${task.owner}`);
|
|
364
|
+
lines.push(`Description: ${desc}`);
|
|
365
|
+
|
|
366
|
+
const openBlockers = task.blockedBy.filter((bid) => {
|
|
367
|
+
const b = store.get(bid);
|
|
368
|
+
return b && b.status !== "completed";
|
|
369
|
+
});
|
|
370
|
+
if (openBlockers.length > 0) lines.push(`Blocked by: ${openBlockers.map((id) => `#${id}`).join(", ")}`);
|
|
371
|
+
if (task.blocks.length > 0) lines.push(`Blocks: ${task.blocks.map((id) => `#${id}`).join(", ")}`);
|
|
372
|
+
const metaKeys = Object.keys(task.metadata);
|
|
373
|
+
if (metaKeys.length > 0) lines.push(`Metadata: ${JSON.stringify(task.metadata)}`);
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
377
|
+
details: { tasks: [task] } satisfies TaskDetails,
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
renderCall(args, theme) {
|
|
382
|
+
return new Text(theme.fg("toolTitle", theme.bold("TaskGet ")) + theme.fg("muted", `#${args.id}`), 0, 0);
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
renderResult(result, { expanded }, theme) {
|
|
386
|
+
return renderTaskListResult((result.details as TaskDetails | undefined)?.tasks ?? [], expanded, theme);
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ── Tool: TaskUpdate ───────────────────────────────────────────────────────
|
|
391
|
+
pi.registerTool({
|
|
392
|
+
name: "TaskUpdate",
|
|
393
|
+
label: "TaskUpdate",
|
|
394
|
+
description: `Use this tool to update a task in the task list.
|
|
395
|
+
|
|
396
|
+
## When to Use This Tool
|
|
397
|
+
|
|
398
|
+
**Before starting work on a task:**
|
|
399
|
+
- Mark it in_progress BEFORE beginning — do not start work without updating status first
|
|
400
|
+
|
|
401
|
+
**Mark tasks as completed:**
|
|
402
|
+
- When you have completed the work described in a task
|
|
403
|
+
- IMPORTANT: Always mark your tasks as completed when you finish them
|
|
404
|
+
- After completing, call TaskList to find your next task
|
|
405
|
+
|
|
406
|
+
- ONLY mark a task as completed when you have FULLY accomplished it
|
|
407
|
+
- If you encounter errors, blockers, or cannot finish, keep the task as in_progress
|
|
408
|
+
- When blocked, create a new task describing what needs to be resolved
|
|
409
|
+
- Never mark a task as completed if:
|
|
410
|
+
- Tests are failing
|
|
411
|
+
- Implementation is partial
|
|
412
|
+
- You encountered unresolved errors
|
|
413
|
+
- You couldn't find necessary files or dependencies
|
|
414
|
+
|
|
415
|
+
**Delete tasks:**
|
|
416
|
+
- When a task is no longer relevant or was created in error
|
|
417
|
+
- Setting status to \`deleted\` permanently removes the task
|
|
418
|
+
|
|
419
|
+
**Update task details:**
|
|
420
|
+
- When requirements change or become clearer
|
|
421
|
+
- When establishing dependencies between tasks
|
|
422
|
+
|
|
423
|
+
## Fields You Can Update
|
|
424
|
+
|
|
425
|
+
- **status**: pending → in_progress → completed (or deleted to remove)
|
|
426
|
+
- **content**: Change the task title
|
|
427
|
+
- **description**: Change the task description
|
|
428
|
+
- **activeForm**: Spinner text when in_progress (e.g., "Running tests")
|
|
429
|
+
- **priority**: high, medium, or low
|
|
430
|
+
- **owner**: Agent name or owner
|
|
431
|
+
- **metadata**: Merge metadata keys (set a key to null to delete it)
|
|
432
|
+
- **addBlocks**: Task IDs that cannot start until this one completes
|
|
433
|
+
- **addBlockedBy**: Task IDs that must complete before this one can start
|
|
434
|
+
|
|
435
|
+
## Status Workflow
|
|
436
|
+
|
|
437
|
+
\`pending\` → \`in_progress\` → \`completed\`
|
|
438
|
+
|
|
439
|
+
Use \`deleted\` to permanently remove a task.
|
|
440
|
+
|
|
441
|
+
## Examples
|
|
442
|
+
|
|
443
|
+
Mark as in progress:
|
|
444
|
+
\`{ "id": "t1", "status": "in_progress" }\`
|
|
445
|
+
|
|
446
|
+
Mark as completed:
|
|
447
|
+
\`{ "id": "t1", "status": "completed" }\`
|
|
448
|
+
|
|
449
|
+
Delete a task:
|
|
450
|
+
\`{ "id": "t1", "status": "deleted" }\`
|
|
451
|
+
|
|
452
|
+
Set dependencies:
|
|
453
|
+
\`{ "id": "t2", "addBlockedBy": ["t1"] }\``,
|
|
454
|
+
promptGuidelines: [
|
|
455
|
+
"Mark tasks in_progress BEFORE starting work, completed immediately after finishing. Never batch completions.",
|
|
456
|
+
"ONLY mark completed when fully done — not when tests are failing or implementation is partial.",
|
|
457
|
+
],
|
|
458
|
+
parameters: TodoUpdateParams,
|
|
459
|
+
|
|
460
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
461
|
+
const { id, ...fields } = params;
|
|
462
|
+
const { task, changedFields, warnings } = store.update(id, fields as any);
|
|
463
|
+
|
|
464
|
+
if (changedFields.length === 0 && !task) {
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: `Task #${id} not found` }],
|
|
467
|
+
details: { tasks: [...store.list()] } satisfies TaskDetails,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (fields.status === "in_progress") {
|
|
472
|
+
widget.setActiveTask(id);
|
|
473
|
+
autoClear.resetBatchCountdown();
|
|
474
|
+
} else if (fields.status === "pending") {
|
|
475
|
+
autoClear.resetBatchCountdown();
|
|
476
|
+
} else if (fields.status === "completed") {
|
|
477
|
+
widget.setActiveTask(id, false);
|
|
478
|
+
autoClear.trackCompletion(id, currentTurn);
|
|
479
|
+
} else if (fields.status === "deleted") {
|
|
480
|
+
widget.setActiveTask(id, false);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
widget.setUICtx(ctx.ui);
|
|
484
|
+
widget.update();
|
|
485
|
+
|
|
486
|
+
let msg: string;
|
|
487
|
+
if (changedFields.includes("deleted")) {
|
|
488
|
+
msg = `→ Updated task #${id} deleted`;
|
|
489
|
+
} else {
|
|
490
|
+
msg = `→ Updated task #${id} ${changedFields.join(", ")}`;
|
|
491
|
+
}
|
|
492
|
+
if (warnings.length > 0) msg += ` (warning: ${warnings.join("; ")})`;
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
content: [{ type: "text", text: msg }],
|
|
496
|
+
details: { tasks: [...store.list()] } satisfies TaskDetails,
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
renderCall(args, theme) {
|
|
501
|
+
const id = args.id as string;
|
|
502
|
+
const status = args.status as string | undefined;
|
|
503
|
+
let extra = "";
|
|
504
|
+
if (status) extra = ` ${theme.fg("muted", `→ ${status}`)}`;
|
|
505
|
+
return new Text(theme.fg("toolTitle", theme.bold("TaskUpdate ")) + theme.fg("accent", `#${id}`) + extra, 0, 0);
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
renderResult(result, { expanded }, theme) {
|
|
509
|
+
return TodoViewComponent.renderTaskResult(result.details as TaskDetails | undefined, expanded, theme);
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// ── Tool: TaskOutput ────────────────────────────────────────────
|
|
514
|
+
pi.registerTool({
|
|
515
|
+
name: "TaskOutput",
|
|
516
|
+
label: "TaskOutput",
|
|
517
|
+
description:
|
|
518
|
+
"Retrieves output from a running or completed background task process.\n" +
|
|
519
|
+
"- Takes a task_id parameter identifying the task\n" +
|
|
520
|
+
"- Returns the task output along with status information\n" +
|
|
521
|
+
"- Use block=true (default) to wait for task completion\n" +
|
|
522
|
+
"- Use block=false for a non-blocking check of current status\n" +
|
|
523
|
+
"- Task IDs can be found using TaskList",
|
|
524
|
+
parameters: Type.Object({
|
|
525
|
+
task_id: Type.String({ description: "The task ID to get output from" }),
|
|
526
|
+
block: Type.Optional(Type.Boolean({ description: "Whether to wait for completion (default: true)" })),
|
|
527
|
+
timeout: Type.Optional(
|
|
528
|
+
Type.Number({
|
|
529
|
+
description: "Max wait time in ms (default: 30000, max: 600000)",
|
|
530
|
+
minimum: 0,
|
|
531
|
+
maximum: 600000,
|
|
532
|
+
}),
|
|
533
|
+
),
|
|
534
|
+
}),
|
|
535
|
+
|
|
536
|
+
async execute(_id, params, signal) {
|
|
537
|
+
const { task_id, block = true, timeout = 30000 } = params;
|
|
538
|
+
const processOutput = tracker.getOutput(task_id);
|
|
539
|
+
|
|
540
|
+
if (!processOutput) {
|
|
541
|
+
const task = store.get(task_id);
|
|
542
|
+
if (!task) {
|
|
543
|
+
return {
|
|
544
|
+
content: [{ type: "text", text: `No task or process found with ID ${task_id}` }],
|
|
545
|
+
details: undefined,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
content: [{ type: "text", text: `Task #${task_id} [${task.status}] — no background process attached` }],
|
|
550
|
+
details: undefined,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (block && processOutput.status === "running") {
|
|
555
|
+
const result = await tracker.waitForCompletion(task_id, timeout, signal ?? undefined);
|
|
556
|
+
if (result) {
|
|
557
|
+
return {
|
|
558
|
+
content: [
|
|
559
|
+
{
|
|
560
|
+
type: "text",
|
|
561
|
+
text: `Task #${task_id} (${result.status})${result.exitCode !== undefined ? ` exit ${result.exitCode}` : ""}\n\n${result.output}`,
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
details: undefined,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
content: [
|
|
571
|
+
{
|
|
572
|
+
type: "text",
|
|
573
|
+
text: `Task #${task_id} (${processOutput.status})${processOutput.exitCode !== undefined ? ` exit ${processOutput.exitCode}` : ""}\n\n${processOutput.output}`,
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
details: undefined,
|
|
577
|
+
};
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
renderCall(args, theme) {
|
|
581
|
+
return new Text(
|
|
582
|
+
theme.fg("toolTitle", theme.bold("TaskOutput ")) + theme.fg("muted", `#${args.task_id}`),
|
|
583
|
+
0,
|
|
584
|
+
0,
|
|
585
|
+
);
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// ── Tool: TaskStop ──────────────────────────────────────────────
|
|
590
|
+
pi.registerTool({
|
|
591
|
+
name: "TaskStop",
|
|
592
|
+
label: "TaskStop",
|
|
593
|
+
description:
|
|
594
|
+
"Stops a running background task process.\n" +
|
|
595
|
+
"- Sends SIGTERM, waits 5 seconds, then SIGKILL if still running\n" +
|
|
596
|
+
"- Marks the task as completed after stopping\n" +
|
|
597
|
+
"- Use this tool when you need to terminate a long-running task",
|
|
598
|
+
parameters: Type.Object({
|
|
599
|
+
task_id: Type.String({ description: "The task ID of the background process to stop" }),
|
|
600
|
+
}),
|
|
601
|
+
|
|
602
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
603
|
+
const { task_id } = params;
|
|
604
|
+
const stopped = await tracker.stop(task_id);
|
|
605
|
+
|
|
606
|
+
if (!stopped) {
|
|
607
|
+
const task = store.get(task_id);
|
|
608
|
+
if (!task)
|
|
609
|
+
return {
|
|
610
|
+
content: [{ type: "text", text: `No running background process for task ${task_id}` }],
|
|
611
|
+
details: undefined,
|
|
612
|
+
};
|
|
613
|
+
return {
|
|
614
|
+
content: [
|
|
615
|
+
{ type: "text", text: `Task #${task_id} has no running background process (status: ${task.status})` },
|
|
616
|
+
],
|
|
617
|
+
details: undefined,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
store.update(task_id, { status: "completed" });
|
|
622
|
+
autoClear.trackCompletion(task_id, currentTurn);
|
|
623
|
+
widget.setActiveTask(task_id, false);
|
|
624
|
+
widget.setUICtx(ctx.ui);
|
|
625
|
+
widget.update();
|
|
626
|
+
|
|
627
|
+
return { content: [{ type: "text", text: `Task #${task_id} stopped successfully` }], details: undefined };
|
|
628
|
+
},
|
|
629
|
+
|
|
630
|
+
renderCall(args, theme) {
|
|
631
|
+
return new Text(theme.fg("toolTitle", theme.bold("TaskStop ")) + theme.fg("accent", `#${args.task_id}`), 0, 0);
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ── Command: /todos ────────────────────────────────────────────────────────
|
|
136
636
|
pi.registerCommand("todos", {
|
|
137
|
-
description: "Open the interactive task viewer",
|
|
637
|
+
description: "Open the interactive task viewer and manager",
|
|
138
638
|
handler: async (_args, ctx) => {
|
|
139
639
|
if (!ctx.hasUI) {
|
|
140
|
-
ctx.ui.notify(
|
|
640
|
+
ctx.ui.notify(store.list().length === 0 ? "No tasks yet." : formatListForLLM(store), "info");
|
|
141
641
|
return;
|
|
142
642
|
}
|
|
143
|
-
await ctx.ui
|
|
144
|
-
|
|
643
|
+
await openTodosMenu(ctx.ui, store, cfg, cwd, (taskId, status) => {
|
|
644
|
+
if (status === "in_progress") widget.setActiveTask(taskId);
|
|
645
|
+
else if (status) widget.setActiveTask(taskId, false);
|
|
646
|
+
widget.update();
|
|
145
647
|
});
|
|
146
648
|
},
|
|
147
649
|
});
|