@zhushanwen/pi-todo 0.1.5 → 0.2.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,13 +28,10 @@ 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
+ updates?: Array<{ id: number; status?: string; text?: string }>;
42
32
  }
43
33
 
44
- // ── TodoParams schema(依赖 Pi 运行时包) ────────────
34
+ // ── TodoParams schema ────────────────────────────────
45
35
 
46
36
  export const TodoParams = Type.Object({
47
37
  action: StringEnum(["list", "add", "update", "delete", "clear"] as const),
@@ -52,27 +42,18 @@ export const TodoParams = Type.Object({
52
42
  status: Type.Optional(
53
43
  StringEnum(VALID_STATUSES, { description: "Target status (for update action)" }),
54
44
  ),
55
- verifyTexts: Type.Optional(
56
- Type.Array(Type.String(), {
57
- description: "Verification text list (one per texts entry, for add action)",
58
- }),
59
- ),
60
45
  updates: Type.Optional(
61
46
  Type.Array(
62
47
  Type.Object({
63
- id: Type.Number({ description: "Todo ID to update (in batch updates[])" }),
48
+ id: Type.Number({ description: "Todo ID to update" }),
64
49
  status: Type.Optional(
65
- Type.String({ description: "Target status (in batch updates[]); one of pending/in_progress/verifying/completed/failed" }),
50
+ Type.String({ description: "Target status; one of pending/in_progress/completed" }),
66
51
  ),
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)" })),
52
+ text: Type.Optional(Type.String({ description: "New todo text" })),
70
53
  }),
71
54
  { description: "Batch updates array (takes priority over single id/status/text)" },
72
55
  ),
73
56
  ),
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
57
  });
77
58
 
78
59
  // ── 错误结果构造 helper ──────────────────────────────
@@ -98,16 +79,16 @@ function errorResult(
98
79
  };
99
80
  }
100
81
 
101
- // ── 5 个 action handler(每个 < 80 行) ──────────────
82
+ // ── 5 个 action handler ──────────────────────────────
102
83
 
103
- /** list action: 格式化所有 todo 列表(AI 可读) */
84
+ /** list action */
104
85
  function handleList(state: TodoSessionState): string {
105
86
  return state.todos.length
106
87
  ? state.todos.map((t) => formatTodoLine(t)).join("\n")
107
88
  : "No todos";
108
89
  }
109
90
 
110
- /** add action: 批量添加 todo */
91
+ /** add action */
111
92
  function handleAdd(
112
93
  state: TodoSessionState,
113
94
  params: TodoActionParams,
@@ -116,7 +97,7 @@ function handleAdd(
116
97
  return { resultText: "", error: "texts required" };
117
98
  }
118
99
 
119
- const addResult = addTodos(state.todos, state.nextId, params.texts, params.verifyTexts);
100
+ const addResult = addTodos(state.todos, state.nextId, params.texts);
120
101
  if (addResult.error) {
121
102
  return { resultText: addResult.resultText || "", error: addResult.error };
122
103
  }
@@ -126,7 +107,7 @@ function handleAdd(
126
107
  return { resultText: addResult.resultText || "" };
127
108
  }
128
109
 
129
- /** update action: 批量 updates[] 路径 */
110
+ /** update action: batch */
130
111
  function handleBatchUpdate(
131
112
  state: TodoSessionState,
132
113
  params: TodoActionParams,
@@ -149,30 +130,10 @@ function handleBatchUpdate(
149
130
  };
150
131
  }
151
132
  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 };
133
+ return { resultText: result.resultText || "" };
173
134
  }
174
135
 
175
- /** update action: 单条 update 路径(含参数验证 + 状态转换拦截 + 应用) */
136
+ /** update action: single */
176
137
  function handleSingleUpdate(
177
138
  state: TodoSessionState,
178
139
  params: TodoActionParams,
@@ -190,49 +151,8 @@ function handleSingleUpdate(
190
151
  const todo = state.todos.find((t) => t.id === params.id);
191
152
  if (!todo) return { resultText: "", error: `#${params.id} not found` };
192
153
 
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
- }
211
- }
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
-
220
154
  if (params.status !== undefined) {
221
- const oldStatus = todo.status;
222
155
  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
156
  }
237
157
  if (params.text !== undefined) {
238
158
  todo.text = params.text;
@@ -241,29 +161,29 @@ function handleSingleUpdate(
241
161
  const parts: string[] = [`Updated todo #${todo.id}`];
242
162
  if (params.status !== undefined) parts.push(`status → ${params.status}`);
243
163
  if (params.text !== undefined) parts.push(`text → "${todo.text}"`);
244
- const resultText = parts.join(", ");
245
164
 
246
- if (isLastCompletion) {
247
- return { resultText: resultText + "\n\nAll todos completed. Please summarize your work." };
165
+ // 最后一个完成提示
166
+ const incompleteAfter = state.todos.filter((t) => t.status !== "completed");
167
+ if (params.status === "completed" && incompleteAfter.length === 0) {
168
+ return { resultText: parts.join(", ") + "\n\nAll todos completed. Please summarize your work." };
248
169
  }
249
- return { resultText };
170
+ return { resultText: parts.join(", ") };
250
171
  }
251
172
 
252
- /** update action: 入口(dispatcher:batch vs single) */
173
+ /** update action: dispatcher */
253
174
  function handleUpdate(
254
175
  state: TodoSessionState,
255
176
  params: TodoActionParams,
256
177
  ):
257
178
  | { resultText: string; error?: string; earlyReturn?: { content: Array<{ type: "text"; text: string }>; details: TodoDetails } }
258
179
  | undefined {
259
- // Batch updates[] takes priority over single id/status/text
260
180
  if (params.updates && params.updates.length > 0) {
261
181
  return handleBatchUpdate(state, params);
262
182
  }
263
183
  return handleSingleUpdate(state, params);
264
184
  }
265
185
 
266
- /** delete action: 批量删除 todo */
186
+ /** delete action */
267
187
  function handleDelete(
268
188
  state: TodoSessionState,
269
189
  params: TodoActionParams,
@@ -287,19 +207,18 @@ function handleDelete(
287
207
  return { resultText: `Deleted ${removedIds.length} items (#${removedIds.join(", #")}), ${state.todos.length} remaining` };
288
208
  }
289
209
 
290
- /** clear action: 清空 todo 列表 */
210
+ /** clear action */
291
211
  function handleClear(state: TodoSessionState): string {
292
212
  const count = state.todos.length;
293
213
  state.todos = [];
294
214
  state.nextId = 1;
295
215
  state.allCompletedAtCount = null;
296
- // v3: 手动清空后重置
216
+ state.completionSteered = false;
297
217
  return count > 0 ? `Cleared ${count} todos` : "No todos to clear";
298
218
  }
299
219
 
300
- // ── Dispatcher(≤ 80 行,纯 switch 分发) ────────────
220
+ // ── Dispatcher ───────────────────────────────────────
301
221
 
302
- /** Tool execute dispatcher — 接受 TodoActionParams + state + ctx,调用对应 handler */
303
222
  export function executeTodoAction(
304
223
  params: TodoActionParams,
305
224
  state: TodoSessionState,
@@ -309,7 +228,6 @@ export function executeTodoAction(
309
228
  content: Array<{ type: "text"; text: string }>;
310
229
  details: TodoDetails;
311
230
  } {
312
- // v3: 记录本次 todo 工具调用轮次(userMessageCount 在 agent_start 中递增)
313
231
  state.lastTodoCallCount = state.userMessageCount;
314
232
  state.stallNotified = false;
315
233
 
@@ -330,7 +248,6 @@ export function executeTodoAction(
330
248
  return errorResult("add", state, r.resultText, r.error);
331
249
  }
332
250
  resultText = r.resultText;
333
- // v3: 新增 todo 表示未全部完成
334
251
  break;
335
252
  }
336
253
 
@@ -342,7 +259,6 @@ export function executeTodoAction(
342
259
  }
343
260
  if (r.earlyReturn) return r.earlyReturn;
344
261
  if (r.error) {
345
- // 把原始人类可读错误文本与 error code 关联
346
262
  const errorText = mapUpdateErrorText(state, params, r.error);
347
263
  return errorResult("update", state, errorText, r.error);
348
264
  }
@@ -384,8 +300,7 @@ export function executeTodoAction(
384
300
  };
385
301
  }
386
302
 
387
- /** handleUpdate error code 映射回原 index.ts 中的人类可读错误文本 */
388
- function mapUpdateErrorText(state: TodoSessionState, params: TodoActionParams, code: string): string {
303
+ function mapUpdateErrorText(state: TodoSessionState, _params: TodoActionParams, code: string): string {
389
304
  switch (code) {
390
305
  case "id required":
391
306
  return "Error: update requires id parameter";
@@ -397,19 +312,8 @@ function mapUpdateErrorText(state: TodoSessionState, params: TodoActionParams, c
397
312
  if (code.startsWith("invalid status:")) {
398
313
  return `Error: status only accepts ${VALID_STATUSES.join(" / ")}`;
399
314
  }
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)";
315
+ if (code.startsWith("#") && code.includes("not found")) {
316
+ return `Error: Todo ${code} not found`;
413
317
  }
414
318
  return `Error: ${code}`;
415
319
  }
@@ -417,7 +321,6 @@ function mapUpdateErrorText(state: TodoSessionState, params: TodoActionParams, c
417
321
 
418
322
  // ── Tool 注册入口 ─────────────────────────────────────
419
323
 
420
- /** 注册 todo tool 到 pi */
421
324
  export function registerTodoTool(
422
325
  pi: ExtensionAPI,
423
326
  state: TodoSessionState,
@@ -430,7 +333,7 @@ export function registerTodoTool(
430
333
  "Manage a todo list." +
431
334
  "\n\nAvailable actions:" +
432
335
  "\n- list: View all todos" +
433
- "\n- add: Batch add todos (requires texts array, optional verifyTexts)" +
336
+ "\n- add: Batch add todos (requires texts array)" +
434
337
  "\n- update: Update a todo (requires id, optional status/text)" +
435
338
  "\n- delete: Batch delete todos (requires ids array)" +
436
339
  "\n- clear: Clear all todos and reset IDs"
@@ -440,16 +343,12 @@ export function registerTodoTool(
440
343
  "[Usage] 多步骤工作(3+步)时使用。AI 自发创建,无需用户触发",
441
344
  "[Goal 冲突] /goal 激活后禁止使用 todo — 改用 add_subtasks",
442
345
  "[批量优先] 完成多项任务时使用 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
346
  "[自动闭合] 全部完成后工具自动清理,无需手动 clear",
447
347
  "[Not for] 单步操作、简单对话、/goal 已激活时",
448
348
  ],
449
349
  parameters: TodoParams,
450
350
 
451
351
  async execute(_toolCallId: string, params: Static<typeof TodoParams>, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
452
- // P1-5: 尊重 signal —— 异步被取消时提前返回
453
352
  if (signal?.aborted) {
454
353
  return {
455
354
  content: [{ type: "text" as const, text: "Todo call aborted by signal." }],
@@ -463,7 +362,6 @@ export function registerTodoTool(
463
362
  };
464
363
  }
465
364
  const result = executeTodoAction(params as TodoActionParams, state, ctx, refreshDisplay);
466
- // Append input params to error results for debugging
467
365
  const details = result.details as { error?: string } | undefined;
468
366
  if (details?.error) {
469
367
  const textPart = result.content[0];