@zhushanwen/pi-todo 0.1.1 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +45 -0
  2. package/package.json +20 -3
  3. package/src/index.ts +293 -291
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # todo
2
+
3
+ 轻量级 AI 任务清单 — 三态(pending / in_progress / completed),支持 session 持久化、状态栏、批量操作。
4
+
5
+ ## 功能
6
+
7
+ - **三态任务**:`pending` → `in_progress` → `completed`
8
+ - **批量操作**:add / update / delete / clear
9
+ - **Session 持久化**:任务状态保存在 session entries 中,重启后恢复
10
+ - **状态栏**:底部显示任务进度(如 `2/5 done`)
11
+ - **自动清理**:所有任务完成后自动清空
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ # symlink 方式(开发推荐)
17
+ ln -s /path/to/xyz-pi-extensions-workspace/main/packages/todo \
18
+ ~/.pi/agent/extensions/todo
19
+
20
+ # npm 方式(正式)
21
+ pi install npm:@zhushanwen/pi-todo
22
+ ```
23
+
24
+ ## 使用
25
+
26
+ AI 可调用 `todo` 工具:
27
+
28
+ | Action | 说明 |
29
+ |--------|------|
30
+ | `list` | 查看所有 todo |
31
+ | `add` | 批量添加 todo |
32
+ | `update` | 更新 todo(状态/文本) |
33
+ | `delete` | 批量删除 |
34
+ | `clear` | 清空所有 |
35
+
36
+ 用户命令:`/todos` 交互式面板。
37
+
38
+ ## 文件结构
39
+
40
+ ```
41
+ todo/
42
+ ├── index.ts
43
+ └── src/
44
+ └── index.ts # 入口 — 工具、命令、事件、状态栏
45
+ ```
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-todo",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "AI-driven todo list for Pi — stateful task management with session persistence and /todos command.",
5
+ "type": "module",
5
6
  "main": "src/index.ts",
7
+ "pi": {
8
+ "extensions": [
9
+ "./src/index.ts"
10
+ ]
11
+ },
6
12
  "keywords": [
7
- "pi",
13
+ "pi-package",
8
14
  "extension",
9
15
  "todo",
10
16
  "task",
@@ -16,7 +22,18 @@
16
22
  "index.ts"
17
23
  ],
18
24
  "peerDependencies": {
19
- "@mariozechner/pi-coding-agent": "*"
25
+ "@mariozechner/pi-coding-agent": "*",
26
+ "@earendil-works/pi-tui": "*",
27
+ "@earendil-works/pi-ai": "*",
28
+ "@sinclair/typebox": "*"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "@earendil-works/pi-tui": {
32
+ "optional": true
33
+ },
34
+ "@earendil-works/pi-ai": {
35
+ "optional": true
36
+ }
20
37
  },
21
38
  "scripts": {
22
39
  "typecheck": "npx tsc --noEmit"
package/src/index.ts CHANGED
@@ -10,9 +10,9 @@
10
10
  */
11
11
 
12
12
  import { StringEnum } from "@mariozechner/pi-ai";
13
- import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
13
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
14
14
  import { matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui";
15
- import { Type } from "typebox";
15
+ import { Type, type Static } from "typebox";
16
16
 
17
17
  // ── 数据模型 ─────────────────────────────────────────
18
18
 
@@ -186,38 +186,6 @@ function renderWidgetLines(todoList: Todo[], th: Theme): string[] {
186
186
  return lines;
187
187
  }
188
188
 
189
- /** 更新状态栏和 widget */
190
- function refreshDisplay(ctx: ExtensionContext): void {
191
- const statusText = renderStatusText(todos, ctx.ui.theme);
192
- ctx.ui.setStatus("todo", statusText || undefined);
193
-
194
- if (todos.length === 0) {
195
- ctx.ui.setWidget("todo", undefined);
196
- } else {
197
- ctx.ui.setWidget("todo", renderWidgetLines(todos, ctx.ui.theme));
198
- }
199
- }
200
-
201
- // ── 模块级状态 ───────────────────────────────────────
202
-
203
- let todos: Todo[] = [];
204
- let nextId = 1;
205
-
206
- // v3: 用户消息轮数与提醒追踪
207
- let userMessageCount: number = 0;
208
- // null 表示未全部完成;设置后保留 AUTO_CLEAR_DELAY_ROUNDS 轮再清空
209
- let allCompletedAtCount: number | null = null;
210
- // 均用 number(初始 0),与 userMessageCount 直接做差值比较
211
- let lastTodoCallCount: number = 0;
212
- let lastReminderCount: number = 0;
213
-
214
- /** v3: 自动清空延迟轮数(全部完成后保留 N 轮用户消息) */
215
- const AUTO_CLEAR_DELAY_ROUNDS = 2;
216
- /** v3: Verification Nudge 触发阈值(完成 N 个任务以上时检查) */
217
- const VERIFICATION_NUDGE_THRESHOLD = 3;
218
- /** v3: Todo Reminder 触发间隔(N 轮未调用 todo 工具时提醒) */
219
- const TODO_REMINDER_INTERVAL = 10;
220
-
221
189
  /** 构建 _render 描述符 */
222
190
  function buildRender(todoList: Todo[]): TodoDetails["_render"] {
223
191
  const completed = todoList.filter((t) => t.status === "completed").length;
@@ -232,251 +200,12 @@ function buildRender(todoList: Todo[]): TodoDetails["_render"] {
232
200
  };
233
201
  }
234
202
 
235
- // ── Tool execute handler ─────────────────────────────
236
-
237
- function executeTodoAction(params: { action: string; text?: string; id?: number; texts?: string[]; ids?: number[]; status?: string }, ctx: ExtensionContext) {
238
- let resultText = "";
239
-
240
- // v3: 追踪 todo 工具调用轮数
241
- lastTodoCallCount = userMessageCount;
242
-
243
- switch (params.action) {
244
- case "list": {
245
- resultText = todos.length
246
- ? todos
247
- .map((t) => {
248
- const mark =
249
- t.status === "completed"
250
- ? "x"
251
- : t.status === "in_progress"
252
- ? "~"
253
- : " ";
254
- return `[${mark}] #${t.id}: ${t.text}`;
255
- })
256
- .join("\n")
257
- : "\u6682\u65e0 todo";
258
- break;
259
- }
260
-
261
- case "add": {
262
- if (!params.texts || params.texts.length === 0) {
263
- return {
264
- content: [{ type: "text" as const, text: "\u9519\u8bef\uff1aadd \u9700\u8981 texts \u53c2\u6570\uff08\u975e\u7a7a\u6570\u7ec4\uff09" }],
265
- details: {
266
- action: "add" as const,
267
- todos: [...todos],
268
- nextId,
269
- error: "texts required",
270
- _render: buildRender(todos),
271
- } as TodoDetails,
272
- };
273
- }
274
- const trimmed = params.texts.map((t) => t.trim()).filter((t) => t.length > 0);
275
- if (trimmed.length === 0) {
276
- return {
277
- content: [{ type: "text" as const, text: "\u9519\u8bef\uff1atexts \u4e2d\u81f3\u5c11\u9700\u8981\u4e00\u4e2a\u975e\u7a7a\u5b57\u7b26\u4e32" }],
278
- details: {
279
- action: "add" as const,
280
- todos: [...todos],
281
- nextId,
282
- error: "all texts empty",
283
- _render: buildRender(todos),
284
- } as TodoDetails,
285
- };
286
- }
287
- const startId = nextId;
288
- for (const t of trimmed) {
289
- todos.push({ id: nextId++, text: t, status: "pending" });
290
- }
291
- const endId = nextId - 1;
292
- resultText = `\u5df2\u6dfb\u52a0 ${trimmed.length} \u9879 todo (#${startId}-#${endId})`;
293
- // v3: 新增 todo 表示未全部完成
294
- allCompletedAtCount = null;
295
- break;
296
- }
297
-
298
- case "update": {
299
- if (params.id === undefined) {
300
- return {
301
- content: [{ type: "text" as const, text: "\u9519\u8bef\uff1aupdate \u9700\u8981 id \u53c2\u6570" }],
302
- details: {
303
- action: "update" as const,
304
- todos: [...todos],
305
- nextId,
306
- error: "id required",
307
- _render: buildRender(todos),
308
- } as TodoDetails,
309
- };
310
- }
311
- if (params.status === undefined && params.text === undefined) {
312
- return {
313
- content: [{ type: "text" as const, text: "\u9519\u8bef\uff1aupdate \u81f3\u5c11\u9700\u8981 status \u6216 text \u53c2\u6570" }],
314
- details: {
315
- action: "update" as const,
316
- todos: [...todos],
317
- nextId,
318
- error: "need status or text",
319
- _render: buildRender(todos),
320
- } as TodoDetails,
321
- };
322
- }
323
- if (params.text !== undefined && params.text === "") {
324
- return {
325
- content: [{ type: "text" as const, text: "\u9519\u8bef\uff1atext \u4e0d\u80fd\u4e3a\u7a7a\u5b57\u7b26\u4e32" }],
326
- details: {
327
- action: "update" as const,
328
- todos: [...todos],
329
- nextId,
330
- error: "text empty",
331
- _render: buildRender(todos),
332
- } as TodoDetails,
333
- };
334
- }
335
- if (
336
- params.status !== undefined &&
337
- !VALID_STATUSES.includes(params.status as (typeof VALID_STATUSES)[number])
338
- ) {
339
- return {
340
- content: [
341
- {
342
- type: "text" as const,
343
- text: `\u9519\u8bef\uff1astatus \u53ea\u63a5\u53d7 ${VALID_STATUSES.join(" / ")}`,
344
- },
345
- ],
346
- details: {
347
- action: "update" as const,
348
- todos: [...todos],
349
- nextId,
350
- error: `invalid status: ${params.status}`,
351
- _render: buildRender(todos),
352
- } as TodoDetails,
353
- };
354
- }
355
-
356
- const todo = todos.find((t) => t.id === params.id);
357
- if (!todo) {
358
- return {
359
- content: [{ type: "text" as const, text: `Todo #${params.id} \u4e0d\u5b58\u5728` }],
360
- details: {
361
- action: "update" as const,
362
- todos: [...todos],
363
- nextId,
364
- error: `#${params.id} not found`,
365
- _render: buildRender(todos),
366
- } as TodoDetails,
367
- };
368
- }
369
-
370
- // T5 完成引导:判断是否是最后一个 pending 即将完成
371
- const incompleteBefore = todos.filter(
372
- (t) => t.status !== "completed",
373
- );
374
- const isLastCompletion =
375
- params.status === "completed" &&
376
- incompleteBefore.length === 1 &&
377
- incompleteBefore[0].id === todo.id;
378
-
379
- if (params.status !== undefined) {
380
- todo.status = params.status as Todo["status"];
381
- }
382
- if (params.text !== undefined) {
383
- todo.text = params.text;
384
- }
385
-
386
- const parts: string[] = [`\u5df2\u66f4\u65b0 todo #${todo.id}`];
387
- if (params.status !== undefined) parts.push(`\u72b6\u6001 \u2192 ${params.status}`);
388
- if (params.text !== undefined) parts.push(`\u6587\u672c \u2192 "${todo.text}"`);
389
- resultText = parts.join("\uff0c");
390
-
391
- if (isLastCompletion) {
392
- resultText += "\n\n\u6240\u6709 todo \u5df2\u5b8c\u6210\u3002\u8bf7\u603b\u7ed3\u5de5\u4f5c\u6210\u679c\u3002";
393
- }
394
-
395
- // v3: 检查是否所有 todo 已完成
396
- const allCompleted = todos.every((t) => t.status === "completed");
397
- if (allCompleted && todos.length > 0) {
398
- allCompletedAtCount = userMessageCount;
399
- } else {
400
- allCompletedAtCount = null;
401
- }
402
- break;
403
- }
404
-
405
- case "delete": {
406
- if (!params.ids || params.ids.length === 0) {
407
- return {
408
- content: [{ type: "text" as const, text: "\u9519\u8bef\uff1adelete \u9700\u8981 ids \u53c2\u6570\uff08\u975e\u7a7a\u6570\u7ec4\uff09" }],
409
- details: {
410
- action: "delete" as const,
411
- todos: [...todos],
412
- nextId,
413
- error: "ids required",
414
- _render: buildRender(todos),
415
- } as TodoDetails,
416
- };
417
- }
418
- const uniqueIds = [...new Set(params.ids)];
419
- const missing = uniqueIds.filter((id) => !todos.some((t) => t.id === id));
420
- if (missing.length > 0) {
421
- const missingStr = missing.map((id) => `#${id}`).join(", ");
422
- return {
423
- content: [{ type: "text" as const, text: `\u9519\u8bef\uff1aTodo ${missingStr} \u4e0d\u5b58\u5728` }],
424
- details: {
425
- action: "delete" as const,
426
- todos: [...todos],
427
- nextId,
428
- error: `#${missing.map((id) => id).join(", #")} not found`,
429
- _render: buildRender(todos),
430
- } as TodoDetails,
431
- };
432
- }
433
- const removedIds: number[] = [];
434
- for (const id of uniqueIds) {
435
- const idx = todos.findIndex((t) => t.id === id);
436
- if (idx !== -1) {
437
- todos.splice(idx, 1);
438
- removedIds.push(id);
439
- }
440
- }
441
- resultText = `\u5df2\u5220\u9664 ${removedIds.length} \u9879 (#${removedIds.join(", #")})\uff0c\u5269\u4f59 ${todos.length} \u9879`;
442
- break;
443
- }
444
-
445
- case "clear": {
446
- const count = todos.length;
447
- todos = [];
448
- nextId = 1;
449
- resultText = count > 0 ? `\u5df2\u6e05\u7a7a ${count} \u9879 todo` : "\u6682\u65e0 todo\uff0c\u65e0\u9700\u6e05\u7a7a";
450
- // v3: 手动清空后重置
451
- allCompletedAtCount = null;
452
- break;
453
- }
454
-
455
- default:
456
- return {
457
- content: [{ type: "text" as const, text: `\u672a\u77e5 action: ${params.action}` }],
458
- details: {
459
- action: "list" as const,
460
- todos: [...todos],
461
- nextId,
462
- error: `unknown action: ${params.action}`,
463
- _render: buildRender(todos),
464
- } as TodoDetails,
465
- };
466
- }
467
-
468
- refreshDisplay(ctx);
469
-
470
- return {
471
- content: [{ type: "text" as const, text: resultText }],
472
- details: {
473
- action: params.action as TodoDetails["action"],
474
- todos: [...todos],
475
- nextId,
476
- _render: buildRender(todos),
477
- } as TodoDetails,
478
- };
479
- }
203
+ /** v3: 自动清空延迟轮数(全部完成后保留 N 轮用户消息) */
204
+ const AUTO_CLEAR_DELAY_ROUNDS = 2;
205
+ /** v3: Verification Nudge 触发阈值(完成 N 个任务以上时检查) */
206
+ const VERIFICATION_NUDGE_THRESHOLD = 3;
207
+ /** v3: Todo Reminder 触发间隔(N 轮未调用 todo 工具时提醒) */
208
+ const TODO_REMINDER_INTERVAL = 10;
480
209
 
481
210
  // ── 列表渲染辅助函数 ─────────────────────────────────
482
211
 
@@ -560,7 +289,277 @@ function renderTodoResult(result: unknown, options: { expanded: boolean }, theme
560
289
  // ── 扩展入口 ─────────────────────────────────────────
561
290
 
562
291
  export default function (pi: ExtensionAPI) {
563
- const reconstructState = (ctx: ExtensionContext) => {
292
+ // ── 闭包内状态(session 隔离) ─────────────────────
293
+ let todos: Todo[] = [];
294
+ let nextId = 1;
295
+
296
+ // v3: 用户消息轮数与提醒追踪
297
+ let userMessageCount = 0;
298
+ let allCompletedAtCount: number | null = null;
299
+ let lastTodoCallCount = 0;
300
+ let lastReminderCount = 0;
301
+
302
+ // ── 刷新显示(依赖闭包 state) ─────────────────────
303
+ function refreshDisplay(ctx: ExtensionContext): void {
304
+ const statusText = renderStatusText(todos, ctx.ui.theme);
305
+ ctx.ui.setStatus("todo", statusText || undefined);
306
+ if (todos.length === 0) {
307
+ ctx.ui.setWidget("todo", undefined);
308
+ } else {
309
+ ctx.ui.setWidget("todo", renderWidgetLines(todos, ctx.ui.theme));
310
+ }
311
+ }
312
+
313
+ // ── Tool execute handler ─────────────────────────────
314
+ function executeTodoAction(
315
+ params: { action: string; text?: string; id?: number; texts?: string[]; ids?: number[]; status?: string },
316
+ ctx: ExtensionContext,
317
+ ) {
318
+ let resultText = "";
319
+
320
+ // v3: 追踪 todo 工具调用轮数
321
+ lastTodoCallCount = userMessageCount;
322
+
323
+ switch (params.action) {
324
+ case "list": {
325
+ resultText = todos.length
326
+ ? todos
327
+ .map((t) => {
328
+ const mark =
329
+ t.status === "completed"
330
+ ? "x"
331
+ : t.status === "in_progress"
332
+ ? "~"
333
+ : " ";
334
+ return `[${mark}] #${t.id}: ${t.text}`;
335
+ })
336
+ .join("\n")
337
+ : "\u6682\u65e0 todo";
338
+ break;
339
+ }
340
+
341
+ case "add": {
342
+ if (!params.texts || params.texts.length === 0) {
343
+ return {
344
+ content: [{ type: "text" as const, text: "\u9519\u8bef\uff1aadd \u9700\u8981 texts \u53c2\u6570\uff08\u975e\u7a7a\u6570\u7ec4\uff09" }],
345
+ details: {
346
+ action: "add" as const,
347
+ todos: [...todos],
348
+ nextId,
349
+ error: "texts required",
350
+ _render: buildRender(todos),
351
+ } as TodoDetails,
352
+ };
353
+ }
354
+ const trimmed = params.texts.map((t) => t.trim()).filter((t) => t.length > 0);
355
+ if (trimmed.length === 0) {
356
+ return {
357
+ content: [{ type: "text" as const, text: "\u9519\u8bef\uff1atexts \u4e2d\u81f3\u5c11\u9700\u8981\u4e00\u4e2a\u975e\u7a7a\u5b57\u7b26\u4e32" }],
358
+ details: {
359
+ action: "add" as const,
360
+ todos: [...todos],
361
+ nextId,
362
+ error: "all texts empty",
363
+ _render: buildRender(todos),
364
+ } as TodoDetails,
365
+ };
366
+ }
367
+ const startId = nextId;
368
+ for (const t of trimmed) {
369
+ todos.push({ id: nextId++, text: t, status: "pending" });
370
+ }
371
+ const endId = nextId - 1;
372
+ resultText = `\u5df2\u6dfb\u52a0 ${trimmed.length} \u9879 todo (#${startId}-#${endId})`;
373
+ // v3: 新增 todo 表示未全部完成
374
+ allCompletedAtCount = null;
375
+ break;
376
+ }
377
+
378
+ case "update": {
379
+ if (params.id === undefined) {
380
+ return {
381
+ content: [{ type: "text" as const, text: "\u9519\u8bef\uff1aupdate \u9700\u8981 id \u53c2\u6570" }],
382
+ details: {
383
+ action: "update" as const,
384
+ todos: [...todos],
385
+ nextId,
386
+ error: "id required",
387
+ _render: buildRender(todos),
388
+ } as TodoDetails,
389
+ };
390
+ }
391
+ if (params.status === undefined && params.text === undefined) {
392
+ return {
393
+ content: [{ type: "text" as const, text: "\u9519\u8bef\uff1aupdate \u81f3\u5c11\u9700\u8981 status \u6216 text \u53c2\u6570" }],
394
+ details: {
395
+ action: "update" as const,
396
+ todos: [...todos],
397
+ nextId,
398
+ error: "need status or text",
399
+ _render: buildRender(todos),
400
+ } as TodoDetails,
401
+ };
402
+ }
403
+ if (params.text !== undefined && params.text === "") {
404
+ return {
405
+ content: [{ type: "text" as const, text: "\u9519\u8bef\uff1atext \u4e0d\u80fd\u4e3a\u7a7a\u5b57\u7b26\u4e32" }],
406
+ details: {
407
+ action: "update" as const,
408
+ todos: [...todos],
409
+ nextId,
410
+ error: "text empty",
411
+ _render: buildRender(todos),
412
+ } as TodoDetails,
413
+ };
414
+ }
415
+ if (
416
+ params.status !== undefined &&
417
+ !VALID_STATUSES.includes(params.status as (typeof VALID_STATUSES)[number])
418
+ ) {
419
+ return {
420
+ content: [
421
+ {
422
+ type: "text" as const,
423
+ text: `\u9519\u8bef\uff1astatus \u53ea\u63a5\u53d7 ${VALID_STATUSES.join(" / ")}`,
424
+ },
425
+ ],
426
+ details: {
427
+ action: "update" as const,
428
+ todos: [...todos],
429
+ nextId,
430
+ error: `invalid status: ${params.status}`,
431
+ _render: buildRender(todos),
432
+ } as TodoDetails,
433
+ };
434
+ }
435
+
436
+ const todo = todos.find((t) => t.id === params.id);
437
+ if (!todo) {
438
+ return {
439
+ content: [{ type: "text" as const, text: `Todo #${params.id} \u4e0d\u5b58\u5728` }],
440
+ details: {
441
+ action: "update" as const,
442
+ todos: [...todos],
443
+ nextId,
444
+ error: `#${params.id} not found`,
445
+ _render: buildRender(todos),
446
+ } as TodoDetails,
447
+ };
448
+ }
449
+
450
+ // T5 完成引导:判断是否是最后一个 pending 即将完成
451
+ const incompleteBefore = todos.filter(
452
+ (t) => t.status !== "completed",
453
+ );
454
+ const isLastCompletion =
455
+ params.status === "completed" &&
456
+ incompleteBefore.length === 1 &&
457
+ incompleteBefore[0].id === todo.id;
458
+
459
+ if (params.status !== undefined) {
460
+ todo.status = params.status as Todo["status"];
461
+ }
462
+ if (params.text !== undefined) {
463
+ todo.text = params.text;
464
+ }
465
+
466
+ const parts: string[] = [`\u5df2\u66f4\u65b0 todo #${todo.id}`];
467
+ if (params.status !== undefined) parts.push(`\u72b6\u6001 \u2192 ${params.status}`);
468
+ if (params.text !== undefined) parts.push(`\u6587\u672c \u2192 "${todo.text}"`);
469
+ resultText = parts.join("\uff0c");
470
+
471
+ if (isLastCompletion) {
472
+ resultText += "\n\n\u6240\u6709 todo \u5df2\u5b8c\u6210\u3002\u8bf7\u603b\u7ed3\u5de5\u4f5c\u6210\u679c\u3002";
473
+ }
474
+
475
+ // v3: 检查是否所有 todo 已完成
476
+ const allCompleted = todos.every((t) => t.status === "completed");
477
+ if (allCompleted && todos.length > 0) {
478
+ allCompletedAtCount = userMessageCount;
479
+ } else {
480
+ allCompletedAtCount = null;
481
+ }
482
+ break;
483
+ }
484
+
485
+ case "delete": {
486
+ if (!params.ids || params.ids.length === 0) {
487
+ return {
488
+ content: [{ type: "text" as const, text: "\u9519\u8bef\uff1adelete \u9700\u8981 ids \u53c2\u6570\uff08\u975e\u7a7a\u6570\u7ec4\uff09" }],
489
+ details: {
490
+ action: "delete" as const,
491
+ todos: [...todos],
492
+ nextId,
493
+ error: "ids required",
494
+ _render: buildRender(todos),
495
+ } as TodoDetails,
496
+ };
497
+ }
498
+ const uniqueIds = [...new Set(params.ids)];
499
+ const missing = uniqueIds.filter((id) => !todos.some((t) => t.id === id));
500
+ if (missing.length > 0) {
501
+ const missingStr = missing.map((id) => `#${id}`).join(", ");
502
+ return {
503
+ content: [{ type: "text" as const, text: `\u9519\u8bef\uff1aTodo ${missingStr} \u4e0d\u5b58\u5728` }],
504
+ details: {
505
+ action: "delete" as const,
506
+ todos: [...todos],
507
+ nextId,
508
+ error: `#${missing.map((id) => id).join(", #")} not found`,
509
+ _render: buildRender(todos),
510
+ } as TodoDetails,
511
+ };
512
+ }
513
+ const removedIds: number[] = [];
514
+ for (const id of uniqueIds) {
515
+ const idx = todos.findIndex((t) => t.id === id);
516
+ if (idx !== -1) {
517
+ todos.splice(idx, 1);
518
+ removedIds.push(id);
519
+ }
520
+ }
521
+ resultText = `\u5df2\u5220\u9664 ${removedIds.length} \u9879 (#${removedIds.join(", #")})\uff0c\u5269\u4f59 ${todos.length} \u9879`;
522
+ break;
523
+ }
524
+
525
+ case "clear": {
526
+ const count = todos.length;
527
+ todos = [];
528
+ nextId = 1;
529
+ resultText = count > 0 ? `\u5df2\u6e05\u7a7a ${count} \u9879 todo` : "\u6682\u65e0 todo\uff0c\u65e0\u9700\u6e05\u7a7a";
530
+ // v3: 手动清空后重置
531
+ allCompletedAtCount = null;
532
+ break;
533
+ }
534
+
535
+ default:
536
+ return {
537
+ content: [{ type: "text" as const, text: `\u672a\u77e5 action: ${params.action}` }],
538
+ details: {
539
+ action: "list" as const,
540
+ todos: [...todos],
541
+ nextId,
542
+ error: `unknown action: ${params.action}`,
543
+ _render: buildRender(todos),
544
+ } as TodoDetails,
545
+ };
546
+ }
547
+
548
+ refreshDisplay(ctx);
549
+
550
+ return {
551
+ content: [{ type: "text" as const, text: resultText }],
552
+ details: {
553
+ action: params.action as TodoDetails["action"],
554
+ todos: [...todos],
555
+ nextId,
556
+ _render: buildRender(todos),
557
+ } as TodoDetails,
558
+ };
559
+ }
560
+
561
+ // ── 状态重建 ───────────────────────────────────────
562
+ function reconstructState(ctx: ExtensionContext) {
564
563
  todos = [];
565
564
  nextId = 1;
566
565
 
@@ -601,24 +600,25 @@ export default function (pi: ExtensionAPI) {
601
600
  entries.splice(staleIndices[j], 1);
602
601
  }
603
602
  }
604
- };
603
+ }
605
604
 
606
- pi.on("session_start", async (_event, ctx) => {
605
+ // ── 事件处理器 ──────────────────────────────────────
606
+ pi.on("session_start", async (_event: any, ctx: ExtensionContext) => {
607
607
  reconstructState(ctx);
608
608
  refreshDisplay(ctx);
609
609
  });
610
- pi.on("session_tree", async (_event, ctx) => {
610
+ pi.on("session_tree", async (_event: any, ctx: ExtensionContext) => {
611
611
  reconstructState(ctx);
612
612
  refreshDisplay(ctx);
613
613
  });
614
614
 
615
615
  // v3: 追踪用户消息轮数
616
- pi.on("agent_start", async (_event, _ctx) => {
616
+ pi.on("agent_start", async (_event: any, _ctx: ExtensionContext) => {
617
617
  userMessageCount++;
618
618
  });
619
619
 
620
620
  // v3: 自动清空与提醒检查
621
- pi.on("before_agent_start", async (_event, ctx) => {
621
+ pi.on("before_agent_start", async (_event: any, ctx: ExtensionContext) => {
622
622
  try {
623
623
  // 1. 自动清空:全部完成后经过 2 轮用户消息
624
624
  if (allCompletedAtCount !== null && userMessageCount - allCompletedAtCount >= AUTO_CLEAR_DELAY_ROUNDS) {
@@ -676,6 +676,7 @@ export default function (pi: ExtensionAPI) {
676
676
  }
677
677
  });
678
678
 
679
+ // ── Tool: todo ──────────────────────────────────────
679
680
  pi.registerTool({
680
681
  name: "todo",
681
682
  label: "Todo",
@@ -700,8 +701,8 @@ export default function (pi: ExtensionAPI) {
700
701
  ],
701
702
  parameters: TodoParams,
702
703
 
703
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
704
- const result = await executeTodoAction(params, ctx);
704
+ async execute(_toolCallId: string, params: Static<typeof TodoParams>, _signal: AbortSignal | undefined, _onUpdate: any, ctx: ExtensionContext) {
705
+ const result = executeTodoAction(params as any, ctx);
705
706
  // Append input params to error results for debugging
706
707
  const details = result.details as { error?: string } | undefined;
707
708
  if (details?.error) {
@@ -714,7 +715,7 @@ export default function (pi: ExtensionAPI) {
714
715
  return result;
715
716
  },
716
717
 
717
- renderCall(args, theme, _context) {
718
+ renderCall(args: any, theme: Theme, _context?: any) {
718
719
  let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", args.action);
719
720
  if (args.texts && args.texts.length > 0) text += ` ${theme.fg("dim", `(${args.texts.length} items)`)}`;
720
721
  if (args.ids && args.ids.length > 0) text += ` ${theme.fg("accent", `#${args.ids.join(", #")}`)}`;
@@ -724,20 +725,21 @@ export default function (pi: ExtensionAPI) {
724
725
  return new Text(text, 0, 0);
725
726
  },
726
727
 
727
- renderResult(result, options, theme, _context) {
728
+ renderResult(result: any, options: any, theme: Theme, _context?: any) {
728
729
  return renderTodoResult(result, options, theme);
729
730
  },
730
731
  });
731
732
 
733
+ // ── Command: /todos ─────────────────────────────────
732
734
  pi.registerCommand("todos", {
733
735
  description: "\u67e5\u770b\u5f53\u524d\u5206\u652f\u7684\u6240\u6709 todo",
734
- handler: async (_args, ctx) => {
736
+ handler: async (_args: string | undefined, ctx: ExtensionCommandContext) => {
735
737
  if (!ctx.hasUI) {
736
738
  ctx.ui.notify("/todos \u9700\u8981\u4ea4\u4e92\u6a21\u5f0f", "error");
737
739
  return;
738
740
  }
739
741
 
740
- await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
742
+ await ctx.ui.custom((_tui: any, theme: Theme, _kb: any, done: () => void) => {
741
743
  return new TodoListComponent(todos, theme, () => done());
742
744
  });
743
745
  },