@zhushanwen/pi-todo 0.8.6 → 0.8.7
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/README.md +17 -19
- package/package.json +3 -3
- package/src/__tests__/tool-detectors.test.ts +4 -4
- package/src/__tests__/tool-prompt.test.ts +2 -1
- package/src/commands.ts +1 -1
- package/src/index.ts +1 -1
- package/src/model.ts +0 -4
- package/src/render.ts +3 -4
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# todo
|
|
2
2
|
|
|
3
|
-
轻量三态任务清单 — `pending` / `in_progress` / `completed`。支持 session
|
|
3
|
+
轻量三态任务清单 — `pending` / `in_progress` / `completed`。支持 session 持久化、状态栏、单双列自适应 widget、`/todos` TUI 视图,以及延迟 steer 驱动任务推进。
|
|
4
4
|
|
|
5
5
|
## 设计定位
|
|
6
6
|
|
|
7
7
|
| 维度 | todo | goal |
|
|
8
8
|
|------|------|------|
|
|
9
|
-
| 状态机 | **刻意无约束**,任意状态自由流转(含反向) |
|
|
9
|
+
| 状态机 | **刻意无约束**,任意状态自由流转(含反向) | 6 态状态机(active/paused/blocked/complete/budget_limited/cancelled) |
|
|
10
10
|
| 持久化 | 复用 Pi 的 toolResult entry(不调用 appendEntry) | appendEntry 主动写入 |
|
|
11
|
-
| 定位 | 多步骤工作的临时进度追踪 |
|
|
11
|
+
| 定位 | 多步骤工作的临时进度追踪 | 持久化目标驱动循环(完成需逐条对照 successCriteria) |
|
|
12
12
|
|
|
13
13
|
`in_progress` 非强制,`pending → completed` 直接跳转合法。
|
|
14
14
|
|
|
@@ -28,10 +28,10 @@ pi install npm:@zhushanwen/pi-todo
|
|
|
28
28
|
| `add` | `texts: string[]` | 是 | 批量追加,自动分配连续 ID,初始 `status=pending` |
|
|
29
29
|
| `update` | `id` + `status` 或 `text`;**或** `updates: Array<{id, status?, text?}>` | `id` 必填 | `updates[]` **优先于** single 的 `id/status/text` |
|
|
30
30
|
| `delete` | `ids: number[]` | 是 | 批量删除;**部分 id 缺失则整体拒绝**(原子性) |
|
|
31
|
-
| `clear` | — | — | 清空全部,重置 `nextId=1` 和完成态标记 |
|
|
32
31
|
|
|
33
32
|
- `status` 枚举:`pending` / `in_progress` / `completed`
|
|
34
|
-
- `add` 不接受 `status`(恒为 pending),不存在 `verifyTexts
|
|
33
|
+
- `add` 不接受 `status`(恒为 pending),不存在 `verifyTexts`(goal 侧的对应概念是 `successCriteria`)
|
|
34
|
+
- 全部 completed 后由 **auto-clear 机制**延迟 2 轮自动清空并重置 `nextId=1`(无手动 clear action)
|
|
35
35
|
|
|
36
36
|
### 错误处理约定
|
|
37
37
|
|
|
@@ -42,11 +42,13 @@ handler 失败**直接 `throw new Error()`**,不返回错误成功模式(见
|
|
|
42
42
|
| `add` 缺 `texts` | `add requires texts parameter (non-empty array)` |
|
|
43
43
|
| `update` 缺 `id` | `update requires id parameter` |
|
|
44
44
|
| `update` 缺 `status` 和 `text` | `update requires at least status or text parameter` |
|
|
45
|
-
| `update` `text` 空串 | `text cannot be empty
|
|
45
|
+
| `update` `text` 空串 | `text cannot be empty or whitespace-only` |
|
|
46
46
|
| `update` `status` 非法 | `status only accepts pending / in_progress / completed` |
|
|
47
47
|
| `update`/`delete` id 不存在 | `Todo #N not found` |
|
|
48
48
|
| `delete` 缺 `ids` | `delete requires ids parameter (non-empty array)` |
|
|
49
49
|
|
|
50
|
+
schema 为扁平 `Type.Object`(OpenAI 兼容):字段全 Optional,缺失必填与双形陷阱(`text`/`texts`、`id`/`ids`)由 handler 运行时校验兜底。
|
|
51
|
+
|
|
50
52
|
## Steer 机制(延迟注入)
|
|
51
53
|
|
|
52
54
|
todo 的核心驱动力是「延迟一拍」的 steer:
|
|
@@ -56,25 +58,22 @@ agent_end 设置 pendingSteerMessage
|
|
|
56
58
|
→ 下一 turn 的 before_agent_start 消费(用户不可见,display:false)
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
两个子机制(handlers.ts):
|
|
60
62
|
|
|
61
63
|
| 机制 | 触发 | 行为 |
|
|
62
64
|
|------|------|------|
|
|
63
|
-
| **auto-clear** | 全部 completed 后再过 2 轮 | 自动清空 todos + 重置标记 |
|
|
64
65
|
| **completion-steer** | 首次全部 completed | 注入「检查交付质量」steer(一次性,`completionSteered` 防重) |
|
|
65
|
-
| **
|
|
66
|
-
| **reminder** | 无 todo 活动达 2 轮 | 温和 reminder |
|
|
66
|
+
| **auto-clear** | 全部 completed 后再过 2 轮 | 自动清空 todos + 重置标记 |
|
|
67
67
|
|
|
68
|
-
`agent_end`
|
|
68
|
+
`agent_end` 内:completion-steer **不短路**(继续往下),auto-clear 命中(`handled`)则短路 return。详见 `ARCHITECTURE.md`。
|
|
69
69
|
|
|
70
70
|
## 持久化机制
|
|
71
71
|
|
|
72
72
|
todo 扩展**自己不调用 `appendEntry`**。状态快照随 Pi 框架自动记录的 toolResult entry 落盘:
|
|
73
73
|
|
|
74
74
|
1. 每次 todo tool 调用,`execute` 返回的 `details.todos` / `details.nextId` 被 Pi 自动序列化为一条 `toolResult` entry
|
|
75
|
-
2. `session_start` / `session_tree` 时,`reconstructState` 回放**最后一条** todo toolResult
|
|
76
|
-
3.
|
|
77
|
-
4. 向后兼容:`migrateTodo` 把旧五态(`verifying→in_progress`、`failed→pending`)和极旧的 `done:boolean` 降级映射到三态
|
|
75
|
+
2. `session_start` / `session_tree` 时,`reconstructState` 回放**最后一条** todo toolResult 重建状态(纯读——Pi 的 getEntries 返回 filter-copy,splice 无效,不做 entry GC)
|
|
76
|
+
3. 向后兼容:`migrateTodo` 把旧五态(`verifying→in_progress`、`failed→pending`)和极旧的 `done:boolean` 降级映射到三态
|
|
78
77
|
|
|
79
78
|
## 三层渲染
|
|
80
79
|
|
|
@@ -93,16 +92,15 @@ todo 扩展**自己不调用 `appendEntry`**。状态快照随 Pi 框架自动
|
|
|
93
92
|
```
|
|
94
93
|
todo/
|
|
95
94
|
├── index.ts # 工厂入口(re-export src/index.ts)
|
|
96
|
-
├── PLAN.md # [SUPERSEDED] v2 历史计划,保留作决策记录
|
|
97
95
|
├── ARCHITECTURE.md # 架构详图(文件依赖 + steer 时序 + 事件流)
|
|
98
96
|
└── src/
|
|
99
|
-
├── index.ts # 工厂入口(创建 state + 注册 tool/command/event)
|
|
97
|
+
├── index.ts # 工厂入口(创建 state + 注册 tool/command/event + makeRefreshDisplay)
|
|
100
98
|
├── state.ts # TodoSessionState 会话状态接口 + 工厂
|
|
101
99
|
├── model.ts # 纯函数数据层(类型/迁移/addTodos/updateTodos/format/buildGui)
|
|
102
|
-
├── tool.ts # todo tool 注册 —
|
|
103
|
-
├── handlers.ts # 5 事件处理器 + reconstructState + steer
|
|
100
|
+
├── tool.ts # todo tool 注册 — 4 action + execute dispatcher
|
|
101
|
+
├── handlers.ts # 5 事件处理器 + reconstructState + steer 双机制(autoClear/completion)
|
|
104
102
|
├── render.ts # status line / widget / tool result 三层渲染
|
|
105
103
|
├── component.ts # /todos 的 TodoListComponent TUI 组件
|
|
106
104
|
├── commands.ts # /todos 命令注册
|
|
107
|
-
└── __tests__/ # 单测(model 纯函数 + widget 布局 +
|
|
105
|
+
└── __tests__/ # 单测(model 纯函数 + widget 布局 + steer/回放 + schema/detector/prompt 回归)
|
|
108
106
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-todo",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.7",
|
|
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",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"vitest": "^4.1.8"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@xyz-agent/extension-protocol": "0.
|
|
33
|
-
"@zhushanwen/pi-extension-logger": "0.
|
|
32
|
+
"@xyz-agent/extension-protocol": "0.8.0",
|
|
33
|
+
"@zhushanwen/pi-extension-logger": "0.4.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// verify the Correct/error STRINGS exist; these exercise the actual throw logic of
|
|
5
5
|
// handleAdd/handleDelete, so a refactor cannot silently drop the dual-form detection.
|
|
6
6
|
//
|
|
7
|
-
// Note: the schema
|
|
8
|
-
// dual-form payloads
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// Note: since the schema was flattened to a single Type.Object (OpenAI compat), all
|
|
8
|
+
// fields are Optional at the schema layer — dual-form payloads now REACH the handler
|
|
9
|
+
// in production, and these handler-level checks are the actual enforcement point
|
|
10
|
+
// (schema.test.ts pins that Value.Check passes for dual-form payloads).
|
|
11
11
|
//
|
|
12
12
|
// handleAdd/handleDelete were exported specifically to enable these tests.
|
|
13
13
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 提示词质量回归:todo tool 的 description 与 runtime 纠错文案必须是
|
|
2
|
-
// "弱模型友好"
|
|
2
|
+
// "弱模型友好"的——条件必填字段(add 缺 texts、delete 缺 ids)在 handler 运行时校验
|
|
3
|
+
// (schema 扁平化后字段全 Optional,见 tool.ts 头注释),双形陷阱
|
|
3
4
|
// (text/texts、id/ids)的 throw 要带 Correct 纠错正例让模型自我纠正。
|
|
4
5
|
//
|
|
5
6
|
// 本测试用源码文本断言锁定这些约束,防止后续重构把纠错文案删掉或弱化。读源码而非
|
package/src/commands.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { TodoListComponent } from "./component";
|
|
|
15
15
|
/** 注册 /todos 命令到 pi */
|
|
16
16
|
export function registerTodosCommand(pi: ExtensionAPI, state: TodoSessionState): void {
|
|
17
17
|
pi.registerCommand("todos", {
|
|
18
|
-
description: "View all todos for the current
|
|
18
|
+
description: "View all todos for the current session",
|
|
19
19
|
handler: async (_args: string | undefined, ctx: ExtensionCommandContext) => {
|
|
20
20
|
if (!ctx.hasUI) {
|
|
21
21
|
ctx.ui.notify("/todos requires interactive mode", "error");
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Todo Extension — 轻量三态任务清单(pending / in_progress / completed)。
|
|
3
3
|
*
|
|
4
|
-
* 设计定位:刻意不做状态机约束(与 goal 扩展的
|
|
4
|
+
* 设计定位:刻意不做状态机约束(与 goal 扩展的 6 态状态机对立),状态自由流转;
|
|
5
5
|
* 状态持久化复用 Pi 自动记录的 toolResult entry(非 appendEntry);
|
|
6
6
|
* 通过 agent_end → before_agent_start 的延迟 steer 驱动任务推进。
|
|
7
7
|
*
|
package/src/model.ts
CHANGED
|
@@ -113,10 +113,6 @@ export function buildGui(todos: Todo[]): GuiRenderResult {
|
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export function getDisplayStatus(t: Todo): string {
|
|
117
|
-
return migrateTodo(t).status;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
116
|
// ── Add 逻辑 ─────────────────────────────────────────
|
|
121
117
|
|
|
122
118
|
export interface AddResult {
|
package/src/render.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
getDisplayStatus,
|
|
10
9
|
type Todo,
|
|
11
10
|
type TodoDetails,
|
|
12
11
|
} from "./model";
|
|
@@ -42,7 +41,7 @@ function fixedWidth(text: string, width: number): string {
|
|
|
42
41
|
export function renderStatusText(todoList: Todo[], th: Theme): string {
|
|
43
42
|
if (todoList.length === 0) return "";
|
|
44
43
|
|
|
45
|
-
const completed = todoList.filter((t) =>
|
|
44
|
+
const completed = todoList.filter((t) => t.status === "completed").length;
|
|
46
45
|
const total = todoList.length;
|
|
47
46
|
|
|
48
47
|
if (completed === total) {
|
|
@@ -113,7 +112,7 @@ export function renderWidgetLines(
|
|
|
113
112
|
|
|
114
113
|
const width = termWidth ?? (process.stdout.columns || FALLBACK_TERM_WIDTH);
|
|
115
114
|
const lines: string[] = [];
|
|
116
|
-
const completed = todoList.filter((t) =>
|
|
115
|
+
const completed = todoList.filter((t) => t.status === "completed").length;
|
|
117
116
|
const total = todoList.length;
|
|
118
117
|
|
|
119
118
|
lines.push(th.fg("accent", "\u2611") + th.fg("muted", ` ${completed}/${total}`));
|
|
@@ -142,7 +141,7 @@ function buildTodoListText(todoList: Todo[], options: { expanded: boolean }, the
|
|
|
142
141
|
let listText = theme.fg("muted", `${todoList.length} todos:`);
|
|
143
142
|
const display = options.expanded ? todoList : todoList.slice(0, MAX_COLLAPSED_ITEMS);
|
|
144
143
|
for (const t of display) {
|
|
145
|
-
const status =
|
|
144
|
+
const status = t.status;
|
|
146
145
|
const mark =
|
|
147
146
|
status === "completed"
|
|
148
147
|
? theme.fg("success", "\u2713")
|