@zhushanwen/pi-todo 0.1.6 → 0.3.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/src/tool.ts CHANGED
@@ -1,18 +1,11 @@
1
1
  /**
2
2
  * Todo tool 注册 + execute dispatcher + 5 个 action handler。
3
- *
4
- * 拆分理由:原 src/index.ts 的 executeTodoAction 函数 318 行(远超 80 行
5
- * 限制),且工厂函数体也 612 行。本文件将 dispatcher 与子 handler 分离,
6
- * 满足 §11 "单文件 ≤ 500 行" 与 "函数 ≤ 80 行" 规范。
7
- *
8
- * 行为契约:所有 handler 行为与原 index.ts 内的 switch case 完全一致;
9
- * 任何与原代码的偏差都属于 bug。
10
3
  */
11
4
 
12
5
  import { StringEnum } from "@mariozechner/pi-ai";
13
6
  import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
14
7
  import { Text } from "@mariozechner/pi-tui";
15
- import { type Static,Type } from "typebox";
8
+ import { type Static, Type } from "typebox";
16
9
 
17
10
  import {
18
11
  addTodos,
@@ -26,7 +19,7 @@ import {
26
19
  import { renderTodoResult } from "./render";
27
20
  import type { TodoSessionState } from "./state";
28
21
 
29
- // ── Action 参数类型(dispatcher → handler) ──────────
22
+ // ── Action 参数类型 ──────────────────────────────────
30
23
 
31
24
  export interface TodoActionParams {
32
25
  action: string;
@@ -35,45 +28,39 @@ export interface TodoActionParams {
35
28
  texts?: string[];
36
29
  ids?: number[];
37
30
  status?: string;
38
- verifyTexts?: string[];
39
- updates?: Array<{ id: number; status?: string; text?: string; verified?: boolean; evidence?: string }>;
40
- verified?: boolean;
41
- evidence?: string;
31
+ isVerification?: boolean;
32
+ updates?: Array<{ id: number; status?: string; text?: string }>;
42
33
  }
43
34
 
44
- // ── TodoParams schema(依赖 Pi 运行时包) ────────────
35
+ // ── TodoParams schema ────────────────────────────────
45
36
 
46
- export const TodoParams = Type.Object({
37
+ const TodoParams = Type.Object({
47
38
  action: StringEnum(["list", "add", "update", "delete", "clear"] as const),
48
39
  text: Type.Optional(Type.String({ description: "Todo text (for update action)" })),
49
40
  id: Type.Optional(Type.Number({ description: "Todo ID (for update action)" })),
50
41
  texts: Type.Optional(Type.Array(Type.String(), { description: "Todo text list (for add action)" })),
51
42
  ids: Type.Optional(Type.Array(Type.Number(), { description: "Todo ID list (for delete action)" })),
52
43
  status: Type.Optional(
53
- StringEnum(VALID_STATUSES, { description: "Target status (for update action)" }),
54
- ),
55
- verifyTexts: Type.Optional(
56
- Type.Array(Type.String(), {
57
- description: "Verification text list (one per texts entry, for add action)",
44
+ StringEnum(VALID_STATUSES, { description: "Target status (for update action)" }),
45
+ ),
46
+ isVerification: Type.Optional(
47
+ Type.Boolean({
48
+ description: "Mark added todos as verification tasks (for add action). Verification todos must be completed (not cancelled) before goal completion.",
58
49
  }),
59
50
  ),
60
51
  updates: Type.Optional(
61
- Type.Array(
62
- Type.Object({
63
- id: Type.Number({ description: "Todo ID to update (in batch updates[])" }),
64
- status: Type.Optional(
65
- Type.String({ description: "Target status (in batch updates[]); one of pending/in_progress/verifying/completed/failed" }),
66
- ),
67
- text: Type.Optional(Type.String({ description: "New todo text (in batch updates[])" })),
68
- verified: Type.Optional(Type.Boolean({ description: "Required true when skipping verifying to mark completed on tasks with verifyText" })),
69
- evidence: Type.Optional(Type.String({ description: "Verification evidence (≥10 chars, required for verifying→completed or in_progress→verifying)" })),
70
- }),
71
- { description: "Batch updates array (takes priority over single id/status/text)" },
52
+ Type.Array(
53
+ Type.Object({
54
+ id: Type.Number({ description: "Todo ID to update" }),
55
+ status: Type.Optional(
56
+ Type.String({ description: "Target status; one of pending/in_progress/completed/cancelled" }),
57
+ ),
58
+ text: Type.Optional(Type.String({ description: "New todo text" })),
59
+ }),
60
+ { description: "Batch updates array (takes priority over single id/status/text)" },
61
+ ),
72
62
  ),
73
- ),
74
- verified: Type.Optional(Type.Boolean({ description: "Required true when skipping verifying to mark completed on a task with verifyText" })),
75
- evidence: Type.Optional(Type.String({ description: "Verification evidence (≥10 chars, required for verifying/completed on tasks with verifyText)" })),
76
- });
63
+ });
77
64
 
78
65
  // ── 错误结果构造 helper ──────────────────────────────
79
66
 
@@ -98,16 +85,16 @@ function errorResult(
98
85
  };
99
86
  }
100
87
 
101
- // ── 5 个 action handler(每个 < 80 行) ──────────────
88
+ // ── 5 个 action handler ──────────────────────────────
102
89
 
103
- /** list action: 格式化所有 todo 列表(AI 可读) */
90
+ /** list action */
104
91
  function handleList(state: TodoSessionState): string {
105
92
  return state.todos.length
106
93
  ? state.todos.map((t) => formatTodoLine(t)).join("\n")
107
94
  : "No todos";
108
95
  }
109
96
 
110
- /** add action: 批量添加 todo */
97
+ /** add action */
111
98
  function handleAdd(
112
99
  state: TodoSessionState,
113
100
  params: TodoActionParams,
@@ -116,7 +103,7 @@ function handleAdd(
116
103
  return { resultText: "", error: "texts required" };
117
104
  }
118
105
 
119
- const addResult = addTodos(state.todos, state.nextId, params.texts, params.verifyTexts);
106
+ const addResult = addTodos(state.todos, state.nextId, params.texts, params.isVerification);
120
107
  if (addResult.error) {
121
108
  return { resultText: addResult.resultText || "", error: addResult.error };
122
109
  }
@@ -126,7 +113,7 @@ function handleAdd(
126
113
  return { resultText: addResult.resultText || "" };
127
114
  }
128
115
 
129
- /** update action: 批量 updates[] 路径 */
116
+ /** update action: batch */
130
117
  function handleBatchUpdate(
131
118
  state: TodoSessionState,
132
119
  params: TodoActionParams,
@@ -149,31 +136,11 @@ function handleBatchUpdate(
149
136
  };
150
137
  }
151
138
  state.todos = result.updatedTodos;
152
- const resultText = result.resultText || "";
153
-
154
- // 检查是否有状态转换拦截
155
- if (result.blocked && result.blocked.length > 0) {
156
- return {
157
- resultText,
158
- error: "blocked",
159
- earlyReturn: {
160
- content: [{ type: "text" as const, text: resultText }],
161
- details: {
162
- action: "update" as const,
163
- todos: [...state.todos],
164
- nextId: state.nextId,
165
- error: "blocked",
166
- _render: buildRender(state.todos),
167
- } as TodoDetails,
168
- },
169
- };
170
- }
171
-
172
- return { resultText };
139
+ return { resultText: result.resultText || "" };
173
140
  }
174
141
 
175
- /** update action: 单条 update 路径(含参数验证 + 状态转换拦截 + 应用) */
176
- function handleSingleUpdate(
142
+ /** update action: single */
143
+ export function handleSingleUpdate(
177
144
  state: TodoSessionState,
178
145
  params: TodoActionParams,
179
146
  ): { resultText: string; error?: string } {
@@ -190,49 +157,16 @@ function handleSingleUpdate(
190
157
  const todo = state.todos.find((t) => t.id === params.id);
191
158
  if (!todo) return { resultText: "", error: `#${params.id} not found` };
192
159
 
193
- // 状态转换拦截
194
- const MIN_EVIDENCE_LEN = 10;
195
- if (params.status === "verifying") {
196
- if (!todo.verifyText) return { resultText: "", error: "no verifyText" };
197
- if (!params.evidence || params.evidence.trim().length < MIN_EVIDENCE_LEN) {
198
- return { resultText: "", error: "evidence required" };
199
- }
200
- } else if (params.status === "completed") {
201
- if (todo.verifyText && todo.status !== "verifying") {
202
- if (params.verified !== true) return { resultText: "", error: "verify required" };
203
- if (!params.evidence || params.evidence.trim().length < MIN_EVIDENCE_LEN) {
204
- return { resultText: "", error: "evidence required" };
205
- }
206
- } else if (todo.status === "verifying") {
207
- if (!params.evidence || params.evidence.trim().length < MIN_EVIDENCE_LEN) {
208
- return { resultText: "", error: "evidence required" };
209
- }
210
- }
160
+ // FR-6 不变量守卫:(a) cancelled 不可恢复;(b) 验证任务不可 cancelled
161
+ if (todo.status === "cancelled" && params.status !== undefined) {
162
+ return { resultText: "", error: `#${params.id} is cancelled (cannot restore)` };
163
+ }
164
+ if (todo.isVerification && params.status === "cancelled") {
165
+ return { resultText: "", error: `#${params.id} is verification todo (cannot cancel)` };
211
166
  }
212
-
213
- // T5 完成引导:判断是否是最后一个 pending 即将完成
214
- const incompleteBefore = state.todos.filter((t) => t.status !== "completed");
215
- const isLastCompletion =
216
- params.status === "completed" &&
217
- incompleteBefore.length === 1 &&
218
- incompleteBefore[0].id === todo.id;
219
167
 
220
168
  if (params.status !== undefined) {
221
- const oldStatus = todo.status;
222
169
  todo.status = params.status as Todo["status"];
223
- if (params.evidence && (params.status === "verifying" || params.status === "completed")) {
224
- todo.evidence = params.evidence.trim();
225
- }
226
- // 检测验证失败: completed/verifying → in_progress
227
- if (
228
- params.status === "in_progress" &&
229
- todo.verifyText &&
230
- todo.verifyAttempts < 2 // 来自 MAX_VERIFY_ATTEMPTS(与 handlers.ts / model.ts 保持一致)
231
- ) {
232
- if (oldStatus === "completed" || oldStatus === "verifying") {
233
- todo.verifyAttempts++;
234
- }
235
- }
236
170
  }
237
171
  if (params.text !== undefined) {
238
172
  todo.text = params.text;
@@ -241,29 +175,29 @@ function handleSingleUpdate(
241
175
  const parts: string[] = [`Updated todo #${todo.id}`];
242
176
  if (params.status !== undefined) parts.push(`status → ${params.status}`);
243
177
  if (params.text !== undefined) parts.push(`text → "${todo.text}"`);
244
- const resultText = parts.join(", ");
245
178
 
246
- if (isLastCompletion) {
247
- return { resultText: resultText + "\n\nAll todos completed. Please summarize your work." };
179
+ // 最后一个完成提示
180
+ const incompleteAfter = state.todos.filter((t) => t.status !== "completed");
181
+ if (params.status === "completed" && incompleteAfter.length === 0) {
182
+ return { resultText: parts.join(", ") + "\n\nAll todos completed. Please summarize your work." };
248
183
  }
249
- return { resultText };
184
+ return { resultText: parts.join(", ") };
250
185
  }
251
186
 
252
- /** update action: 入口(dispatcher:batch vs single) */
187
+ /** update action: dispatcher */
253
188
  function handleUpdate(
254
189
  state: TodoSessionState,
255
190
  params: TodoActionParams,
256
191
  ):
257
192
  | { resultText: string; error?: string; earlyReturn?: { content: Array<{ type: "text"; text: string }>; details: TodoDetails } }
258
193
  | undefined {
259
- // Batch updates[] takes priority over single id/status/text
260
194
  if (params.updates && params.updates.length > 0) {
261
195
  return handleBatchUpdate(state, params);
262
196
  }
263
197
  return handleSingleUpdate(state, params);
264
198
  }
265
199
 
266
- /** delete action: 批量删除 todo */
200
+ /** delete action */
267
201
  function handleDelete(
268
202
  state: TodoSessionState,
269
203
  params: TodoActionParams,
@@ -287,20 +221,19 @@ function handleDelete(
287
221
  return { resultText: `Deleted ${removedIds.length} items (#${removedIds.join(", #")}), ${state.todos.length} remaining` };
288
222
  }
289
223
 
290
- /** clear action: 清空 todo 列表 */
224
+ /** clear action */
291
225
  function handleClear(state: TodoSessionState): string {
292
226
  const count = state.todos.length;
293
227
  state.todos = [];
294
228
  state.nextId = 1;
295
229
  state.allCompletedAtCount = null;
296
- // v3: 手动清空后重置
230
+ state.completionSteered = false;
297
231
  return count > 0 ? `Cleared ${count} todos` : "No todos to clear";
298
232
  }
299
233
 
300
- // ── Dispatcher(≤ 80 行,纯 switch 分发) ────────────
234
+ // ── Dispatcher ───────────────────────────────────────
301
235
 
302
- /** Tool execute dispatcher — 接受 TodoActionParams + state + ctx,调用对应 handler */
303
- export function executeTodoAction(
236
+ function executeTodoAction(
304
237
  params: TodoActionParams,
305
238
  state: TodoSessionState,
306
239
  ctx: ExtensionContext,
@@ -309,7 +242,6 @@ export function executeTodoAction(
309
242
  content: Array<{ type: "text"; text: string }>;
310
243
  details: TodoDetails;
311
244
  } {
312
- // v3: 记录本次 todo 工具调用轮次(userMessageCount 在 agent_start 中递增)
313
245
  state.lastTodoCallCount = state.userMessageCount;
314
246
  state.stallNotified = false;
315
247
 
@@ -330,7 +262,6 @@ export function executeTodoAction(
330
262
  return errorResult("add", state, r.resultText, r.error);
331
263
  }
332
264
  resultText = r.resultText;
333
- // v3: 新增 todo 表示未全部完成
334
265
  break;
335
266
  }
336
267
 
@@ -342,7 +273,6 @@ export function executeTodoAction(
342
273
  }
343
274
  if (r.earlyReturn) return r.earlyReturn;
344
275
  if (r.error) {
345
- // 把原始人类可读错误文本与 error code 关联
346
276
  const errorText = mapUpdateErrorText(state, params, r.error);
347
277
  return errorResult("update", state, errorText, r.error);
348
278
  }
@@ -384,8 +314,7 @@ export function executeTodoAction(
384
314
  };
385
315
  }
386
316
 
387
- /** handleUpdate error code 映射回原 index.ts 中的人类可读错误文本 */
388
- function mapUpdateErrorText(state: TodoSessionState, params: TodoActionParams, code: string): string {
317
+ function mapUpdateErrorText(state: TodoSessionState, _params: TodoActionParams, code: string): string {
389
318
  switch (code) {
390
319
  case "id required":
391
320
  return "Error: update requires id parameter";
@@ -397,19 +326,8 @@ function mapUpdateErrorText(state: TodoSessionState, params: TodoActionParams, c
397
326
  if (code.startsWith("invalid status:")) {
398
327
  return `Error: status only accepts ${VALID_STATUSES.join(" / ")}`;
399
328
  }
400
- if (code === "#not found" || /^\#\d+ not found$/.test(code)) {
401
- return `Error: Todo ${code.replace(/^#/, "#")} not found`;
402
- }
403
- if (code === "no verifyText") {
404
- const todo = state.todos.find((t) => t.id === params.id);
405
- return `Error: #${todo?.id ?? "?"} 无 verifyText,不能进入 verifying 状态`;
406
- }
407
- if (code === "verify required") {
408
- const todo = state.todos.find((t) => t.id === params.id);
409
- return `⚠️ Task #${todo?.id ?? "?"} "${todo?.text ?? ""}" 有验证要求。\n请先: todo update(id=${todo?.id}, status=verifying, evidence="验证进度")\n或跳过: todo update(id=${todo?.id}, status=completed, verified=true, evidence="验证结论")\n验证标准: ${todo?.verifyText}`;
410
- }
411
- if (code === "evidence required") {
412
- return "⚠️ evidence required (≥10 chars)";
329
+ if (code.startsWith("#") && code.includes("not found")) {
330
+ return `Error: Todo ${code} not found`;
413
331
  }
414
332
  return `Error: ${code}`;
415
333
  }
@@ -417,7 +335,6 @@ function mapUpdateErrorText(state: TodoSessionState, params: TodoActionParams, c
417
335
 
418
336
  // ── Tool 注册入口 ─────────────────────────────────────
419
337
 
420
- /** 注册 todo tool 到 pi */
421
338
  export function registerTodoTool(
422
339
  pi: ExtensionAPI,
423
340
  state: TodoSessionState,
@@ -430,26 +347,22 @@ export function registerTodoTool(
430
347
  "Manage a todo list." +
431
348
  "\n\nAvailable actions:" +
432
349
  "\n- list: View all todos" +
433
- "\n- add: Batch add todos (requires texts array, optional verifyTexts)" +
350
+ "\n- add: Batch add todos (requires texts array; optional isVerification marks verification tasks)" +
434
351
  "\n- update: Update a todo (requires id, optional status/text)" +
435
352
  "\n- delete: Batch delete todos (requires ids array)" +
436
- "\n- clear: Clear all todos and reset IDs"
437
- + "\nWhen /goal is active, do NOT use this tool use goal_manager's add_subtasks instead.",
438
- promptSnippet: "Use todo when breaking multi-step work into trackable items during normal (non-goal) conversation. Not for single-step operations.",
353
+ "\n- clear: Clear all todos and reset IDs",
354
+ promptSnippet: "Use todo when breaking multi-step work into trackable items. Add verification todos (isVerification=true) for checks like running tests.",
439
355
  promptGuidelines: [
440
356
  "[Usage] 多步骤工作(3+步)时使用。AI 自发创建,无需用户触发",
441
- "[Goal 冲突] /goal 激活后禁止使用 todo 改用 add_subtasks",
357
+ "[验证任务] 执行任务 + 验证任务(isVerification=true,如 run tests / typecheck)一起建",
442
358
  "[批量优先] 完成多项任务时使用 updates[] 批量更新,减少工具调用次数",
443
- "[验证流程] 有 verifyText 的任务: in_progress → verifying(evidence=\"验证进度\") → completed(evidence=\"验证结论\")。evidence ≥ 10 字符",
444
- "[跳过验证] 有 verifyText 但想直接 completed: 必须传 verified=true + evidence",
445
- "[验证失败] verifying/completed 被改回 in_progress 时 verifyAttempts++,2 次后进入 failed",
446
359
  "[自动闭合] 全部完成后工具自动清理,无需手动 clear",
447
- "[Not for] 单步操作、简单对话、/goal 已激活时",
360
+ "[Not for] 单步操作、简单对话",
448
361
  ],
362
+ executionMode: "sequential",
449
363
  parameters: TodoParams,
450
364
 
451
365
  async execute(_toolCallId: string, params: Static<typeof TodoParams>, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
452
- // P1-5: 尊重 signal —— 异步被取消时提前返回
453
366
  if (signal?.aborted) {
454
367
  return {
455
368
  content: [{ type: "text" as const, text: "Todo call aborted by signal." }],
@@ -463,7 +376,6 @@ export function registerTodoTool(
463
376
  };
464
377
  }
465
378
  const result = executeTodoAction(params as TodoActionParams, state, ctx, refreshDisplay);
466
- // Append input params to error results for debugging
467
379
  const details = result.details as { error?: string } | undefined;
468
380
  if (details?.error) {
469
381
  const textPart = result.content[0];