@zhushanwen/pi-todo 0.8.7 → 0.8.8
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/package.json +3 -3
- package/src/__tests__/render.test.ts +102 -0
- package/src/render.ts +43 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-todo",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
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.8.
|
|
33
|
-
"@zhushanwen/pi-extension-logger": "0.4.
|
|
32
|
+
"@xyz-agent/extension-protocol": "0.8.1",
|
|
33
|
+
"@zhushanwen/pi-extension-logger": "0.4.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import type { Todo, TodoDetails } from "../model";
|
|
5
|
+
import { renderTodoResult } from "../render";
|
|
6
|
+
|
|
7
|
+
// ── mock theme(fg 直通,便于断言纯文本)────────────
|
|
8
|
+
|
|
9
|
+
const mockTheme = {
|
|
10
|
+
fg: (_color: string, text: string) => text,
|
|
11
|
+
bg: (_color: string, text: string) => text,
|
|
12
|
+
bold: (text: string) => text,
|
|
13
|
+
italic: (text: string) => text,
|
|
14
|
+
underline: (text: string) => text,
|
|
15
|
+
inverse: (text: string) => text,
|
|
16
|
+
strikethrough: (text: string) => text,
|
|
17
|
+
getFgAnsi: (_color: string) => "",
|
|
18
|
+
getBgAnsi: (_color: string) => "",
|
|
19
|
+
getColorMode: () => "truecolor" as const,
|
|
20
|
+
getThinkingBorderColor: () => (text: string) => text,
|
|
21
|
+
getBashModeBorderColor: () => (text: string) => text,
|
|
22
|
+
} as unknown as Theme;
|
|
23
|
+
|
|
24
|
+
// ── fixture ─────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const todos: Todo[] = [
|
|
27
|
+
{ id: 1, text: "Write docs", status: "completed" },
|
|
28
|
+
{ id: 2, text: "Ship it", status: "in_progress" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const listDetails: TodoDetails = { action: "list", todos, nextId: 3 };
|
|
32
|
+
|
|
33
|
+
/** Text 实例的渲染行(width 足够宽避免 wrap;每行被 pad,断言用 toContain) */
|
|
34
|
+
function renderedText(result: unknown, expanded = false): string {
|
|
35
|
+
return renderTodoResult(result, { expanded }, mockTheme).render(400).join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── renderTodoResult ────────────────────────────────
|
|
39
|
+
|
|
40
|
+
describe("renderTodoResult", () => {
|
|
41
|
+
it("无 details → 渲染 content[0] 的 text", () => {
|
|
42
|
+
const result = { content: [{ type: "text", text: "Unknown payload" }] };
|
|
43
|
+
expect(renderedText(result)).toContain("Unknown payload");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("无 details 且 content[0] 非 text 类型 → 空文本(渲染为空)", () => {
|
|
47
|
+
const result = { content: [{ type: "image", text: "ignored" }] };
|
|
48
|
+
expect(renderTodoResult(result, { expanded: false }, mockTheme).render(400)).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("无 details 且 content[0].text 缺失 → 空文本", () => {
|
|
52
|
+
const result = { content: [{ type: "text" }] };
|
|
53
|
+
expect(renderTodoResult(result, { expanded: false }, mockTheme).render(400)).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("action=list → 渲染全量列表(标题 + 每条 mark/#id/文本)", () => {
|
|
57
|
+
const result = { content: [{ type: "text", text: "ignored" }], details: listDetails };
|
|
58
|
+
const out = renderedText(result);
|
|
59
|
+
expect(out).toContain("2 todos:");
|
|
60
|
+
expect(out).toContain("✓ #1 Write docs");
|
|
61
|
+
expect(out).toContain("● #2 Ship it");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each(["add", "update", "delete"] as const)(
|
|
65
|
+
"action=%s → 确认消息 + 空行 + 全量列表",
|
|
66
|
+
(action) => {
|
|
67
|
+
const result = {
|
|
68
|
+
content: [{ type: "text", text: "Done thing" }],
|
|
69
|
+
details: { action, todos, nextId: 3 },
|
|
70
|
+
};
|
|
71
|
+
const out = renderedText(result);
|
|
72
|
+
expect(out).toContain("✓ Done thing");
|
|
73
|
+
expect(out).toContain("2 todos:");
|
|
74
|
+
expect(out).toContain("#1 Write docs");
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
it("action=delete 时 details 有 text 缺失 → 确认段为空串,列表仍在", () => {
|
|
79
|
+
const result = {
|
|
80
|
+
content: [{ type: "text" }],
|
|
81
|
+
details: { action: "delete", todos, nextId: 3 },
|
|
82
|
+
};
|
|
83
|
+
const out = renderedText(result);
|
|
84
|
+
expect(out).toContain("2 todos:");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("未知 action + 有消息 → dim 渲染消息原文", () => {
|
|
88
|
+
const result = {
|
|
89
|
+
content: [{ type: "text", text: "Archived" }],
|
|
90
|
+
details: { action: "archive", todos: [], nextId: 1 } as unknown as TodoDetails,
|
|
91
|
+
};
|
|
92
|
+
expect(renderedText(result)).toContain("Archived");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("未知 action + 无消息 → 兜底 Done", () => {
|
|
96
|
+
const result = {
|
|
97
|
+
content: [{ type: "text" }],
|
|
98
|
+
details: { action: "archive", todos: [], nextId: 1 } as unknown as TodoDetails,
|
|
99
|
+
};
|
|
100
|
+
expect(renderedText(result)).toContain("Done");
|
|
101
|
+
});
|
|
102
|
+
});
|
package/src/render.ts
CHANGED
|
@@ -161,14 +161,28 @@ function buildTodoListText(todoList: Todo[], options: { expanded: boolean }, the
|
|
|
161
161
|
|
|
162
162
|
import { Text } from "@earendil-works/pi-tui";
|
|
163
163
|
|
|
164
|
+
/** content[0] 提取 text(缺失/非 text 类型 → 空串) */
|
|
165
|
+
function firstContentText(r: { content: Array<{ type: string; text?: string }> }): string {
|
|
166
|
+
const text = r.content[0];
|
|
167
|
+
return text?.type === "text" ? (text.text ?? "") : "";
|
|
168
|
+
}
|
|
169
|
+
|
|
164
170
|
export function renderTodoResult(result: unknown, options: { expanded: boolean }, theme: Theme): Text {
|
|
165
171
|
const r = result as { content: Array<{ type: string; text?: string }>; details?: unknown };
|
|
166
172
|
const details = r.details as TodoDetails | undefined;
|
|
167
173
|
if (!details) {
|
|
168
|
-
|
|
169
|
-
return new Text(text?.type === "text" ? (text.text ?? "") : "", 0, 0);
|
|
174
|
+
return new Text(firstContentText(r), 0, 0);
|
|
170
175
|
}
|
|
176
|
+
return renderDetailedTodoResult(r, options, theme, details);
|
|
177
|
+
}
|
|
171
178
|
|
|
179
|
+
/** 有 details 时按 action 分发渲染(switch 每个 case 体独立成函数) */
|
|
180
|
+
function renderDetailedTodoResult(
|
|
181
|
+
r: { content: Array<{ type: string; text?: string }> },
|
|
182
|
+
options: { expanded: boolean },
|
|
183
|
+
theme: Theme,
|
|
184
|
+
details: TodoDetails,
|
|
185
|
+
): Text {
|
|
172
186
|
const todoList = details.todos;
|
|
173
187
|
|
|
174
188
|
switch (details.action) {
|
|
@@ -179,20 +193,36 @@ export function renderTodoResult(result: unknown, options: { expanded: boolean }
|
|
|
179
193
|
case "add":
|
|
180
194
|
case "update":
|
|
181
195
|
case "delete": {
|
|
182
|
-
|
|
183
|
-
const msg = text?.type === "text" ? (text.text ?? "") : "";
|
|
184
|
-
const listText = buildTodoListText(todoList, options, theme);
|
|
185
|
-
return new Text(
|
|
186
|
-
theme.fg("success", "\u2713 ") + theme.fg("muted", msg) + "\n\n" + listText,
|
|
187
|
-
0,
|
|
188
|
-
0,
|
|
189
|
-
);
|
|
196
|
+
return renderMutationTodoResult(r, options, theme, todoList);
|
|
190
197
|
}
|
|
191
198
|
|
|
192
199
|
default: {
|
|
193
|
-
|
|
194
|
-
const msg = text?.type === "text" ? (text.text ?? "") : "";
|
|
195
|
-
return new Text(theme.fg("dim", msg || "Done"), 0, 0);
|
|
200
|
+
return renderFallbackTodoResult(r, theme);
|
|
196
201
|
}
|
|
197
202
|
}
|
|
198
203
|
}
|
|
204
|
+
|
|
205
|
+
/** add/update/delete:确认消息 + 空行 + 全量列表 */
|
|
206
|
+
function renderMutationTodoResult(
|
|
207
|
+
r: { content: Array<{ type: string; text?: string }> },
|
|
208
|
+
options: { expanded: boolean },
|
|
209
|
+
theme: Theme,
|
|
210
|
+
todoList: Todo[],
|
|
211
|
+
): Text {
|
|
212
|
+
const msg = firstContentText(r);
|
|
213
|
+
const listText = buildTodoListText(todoList, options, theme);
|
|
214
|
+
return new Text(
|
|
215
|
+
theme.fg("success", "\u2713 ") + theme.fg("muted", msg) + "\n\n" + listText,
|
|
216
|
+
0,
|
|
217
|
+
0,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 其余/未知 action:dim 文案,空文案兜底 "Done" */
|
|
222
|
+
function renderFallbackTodoResult(
|
|
223
|
+
r: { content: Array<{ type: string; text?: string }> },
|
|
224
|
+
theme: Theme,
|
|
225
|
+
): Text {
|
|
226
|
+
const msg = firstContentText(r);
|
|
227
|
+
return new Text(theme.fg("dim", msg || "Done"), 0, 0);
|
|
228
|
+
}
|