@zhushanwen/pi-todo 0.1.2 → 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 (2) hide show
  1. package/package.json +13 -2
  2. package/src/index.ts +282 -280
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-todo",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "AI-driven todo list for Pi — stateful task management with session persistence and /todos command.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -22,7 +22,18 @@
22
22
  "index.ts"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@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
+ }
26
37
  },
27
38
  "scripts": {
28
39
  "typecheck": "npx tsc --noEmit"
package/src/index.ts CHANGED
@@ -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,8 +600,9 @@ export default function (pi: ExtensionAPI) {
601
600
  entries.splice(staleIndices[j], 1);
602
601
  }
603
602
  }
604
- };
603
+ }
605
604
 
605
+ // ── 事件处理器 ──────────────────────────────────────
606
606
  pi.on("session_start", async (_event: any, ctx: ExtensionContext) => {
607
607
  reconstructState(ctx);
608
608
  refreshDisplay(ctx);
@@ -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",
@@ -701,7 +702,7 @@ export default function (pi: ExtensionAPI) {
701
702
  parameters: TodoParams,
702
703
 
703
704
  async execute(_toolCallId: string, params: Static<typeof TodoParams>, _signal: AbortSignal | undefined, _onUpdate: any, ctx: ExtensionContext) {
704
- const result = await executeTodoAction(params as any, ctx);
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) {
@@ -729,6 +730,7 @@ export default function (pi: ExtensionAPI) {
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
736
  handler: async (_args: string | undefined, ctx: ExtensionCommandContext) => {