@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-todo",
3
- "version": "0.8.7",
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.0",
33
- "@zhushanwen/pi-extension-logger": "0.4.0"
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
- const text = r.content[0];
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
- const text = r.content[0];
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
- const text = r.content[0];
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
+ }