@zhushanwen/pi-todo 0.1.6 → 0.2.0

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/src/commands.ts CHANGED
@@ -1,17 +1,15 @@
1
1
  /**
2
- * /todos 命令注册 — 进入 TodoListComponent TUI 视图。
3
- * registerMessageRenderer for todo-context — 渲染注入的 todo 上下文消息。
2
+ * /todos 命令注册 — 进入 TodoListComponent TUI 视图(双列布局)。
4
3
  *
5
- * 拆分理由:原 src/index.ts 的 command 注册块与 message renderer 注册块
6
- * 混合了 TUI 组件定义与注册逻辑。抽出后 index.ts 工厂只需调用本文件
7
- * 导出的注册函数。
4
+ * todo-context 消息不再需要 registerMessageRenderer,
5
+ * 因为所有 context 通过 before_agent_start 的 display:false 注入,
6
+ * 用户在 TUI 中不可见。
8
7
  */
9
8
 
10
9
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
11
- import { Text } from "@mariozechner/pi-tui";
12
10
 
13
- import { TodoListComponent } from "./component";
14
11
  import type { TodoSessionState } from "./state";
12
+ import { TodoListComponent } from "./component";
15
13
 
16
14
  /** 注册 /todos 命令到 pi */
17
15
  export function registerTodosCommand(pi: ExtensionAPI, state: TodoSessionState): void {
@@ -29,22 +27,3 @@ export function registerTodosCommand(pi: ExtensionAPI, state: TodoSessionState):
29
27
  },
30
28
  });
31
29
  }
32
-
33
- /** 注册 todo-context 消息渲染器(用于 before_agent_start 注入的 <todo_context> 消息) */
34
- export function registerTodoContextRenderer(pi: ExtensionAPI): void {
35
- pi.registerMessageRenderer("todo-context", (message: Record<string, unknown>, _options: unknown, theme: Theme) => {
36
- const content = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
37
- const match = content.match(/\[TODO\]\s*(?:Turn \d+ — )?(\d+)\s*tasks?\s*(pending|completed)/);
38
- let displayText: string;
39
- if (match) {
40
- const count = match[1];
41
- displayText = match[2] === "completed"
42
- ? theme.fg("success", `[TODO] All ${count} tasks completed \u2713`)
43
- : theme.fg("warning", `[TODO] ${count} tasks pending`);
44
- } else {
45
- const firstLine = content.split("\n").find((l: string) => l.includes("[TODO]")) || "[TODO]";
46
- displayText = theme.fg("accent", firstLine.trim());
47
- }
48
- return new Text(displayText, 0, 0);
49
- });
50
- }
package/src/component.ts CHANGED
@@ -1,19 +1,16 @@
1
1
  /**
2
- * /todos 命令的 TUI 组件 — 独立可关闭的 todo 列表视图。
3
- *
4
- * 拆分理由:原 src/index.ts 的 TodoListComponent 占用约 85 行,与渲染函数、
5
- * tool 注册、事件注册混在一起。独立后 commands.ts 只需引用本组件。
2
+ * /todos 命令的 TUI 组件 — 独立可关闭的 todo 列表视图(双列布局)。
6
3
  */
7
4
 
8
5
  import type { Theme } from "@mariozechner/pi-coding-agent";
9
6
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
10
7
 
11
8
  import type { Todo } from "./model";
9
+ import { FALLBACK_TERM_WIDTH, renderDualColumn } from "./render";
12
10
 
13
11
  const HEADER_PREFIX_DASHES = 3;
14
12
  const HEADER_RESERVED_WIDTH = 10;
15
13
 
16
- /** /todos 命令的 TUI 组件 — 独立可关闭的 todo 列表视图 */
17
14
  export class TodoListComponent {
18
15
  private todos: Todo[];
19
16
  private theme: Theme;
@@ -40,51 +37,31 @@ export class TodoListComponent {
40
37
 
41
38
  const lines: string[] = [];
42
39
  const th = this.theme;
40
+ const termWidth = width || FALLBACK_TERM_WIDTH;
41
+ const indent = " ";
43
42
 
44
43
  lines.push("");
45
44
  const title = th.fg("accent", " Todos ");
46
45
  const headerLine =
47
- th.fg("borderMuted", "\u2500".repeat(HEADER_PREFIX_DASHES)) + title + th.fg("borderMuted", "\u2500".repeat(Math.max(0, width - HEADER_RESERVED_WIDTH)));
48
- lines.push(truncateToWidth(headerLine, width));
46
+ th.fg("borderMuted", "\u2500".repeat(HEADER_PREFIX_DASHES)) + title + th.fg("borderMuted", "\u2500".repeat(Math.max(0, termWidth - HEADER_RESERVED_WIDTH)));
47
+ lines.push(truncateToWidth(headerLine, termWidth));
49
48
  lines.push("");
50
49
 
51
50
  if (this.todos.length === 0) {
52
- lines.push(truncateToWidth(` ${th.fg("dim", "No todos yet. Ask the agent to add some!")}`, width));
51
+ lines.push(truncateToWidth(`${indent}${th.fg("dim", "No todos yet. Ask the agent to add some!")}`, termWidth));
53
52
  } else {
54
53
  const completed = this.todos.filter((t) => t.status === "completed").length;
55
54
  const total = this.todos.length;
56
- lines.push(truncateToWidth(` ${th.fg("muted", `${completed}/${total} completed`)}`, width));
55
+ lines.push(truncateToWidth(`${indent}${th.fg("muted", `${completed}/${total} completed`)}`, termWidth));
57
56
  lines.push("");
58
57
 
59
- for (const todo of this.todos) {
60
- const mark =
61
- todo.status === "completed"
62
- ? th.fg("success", "\u2713")
63
- : todo.status === "verifying"
64
- ? th.fg("warning", "\u25d0")
65
- : todo.status === "in_progress"
66
- ? th.fg("warning", "\u25cf")
67
- : todo.status === "failed"
68
- ? th.fg("error", "\u2717")
69
- : th.fg("dim", "\u25cb");
70
- const id = th.fg("accent", `#${todo.id}`);
71
- const text = todo.status === "completed" ? th.fg("dim", todo.text) : th.fg("text", todo.text);
72
- let verifyTag = "";
73
- if (todo.status === "verifying") {
74
- verifyTag = th.fg("warning", ` [验证中${todo.evidence ? ": " + todo.evidence.slice(0, 30) : ""}]`);
75
- } else if (todo.verifyText && todo.status !== "completed") {
76
- verifyTag = th.fg("warning", " [待验证]");
77
- } else if (todo.status === "completed" && todo.verifyText) {
78
- verifyTag = th.fg("success", " [已验证]");
79
- } else if (todo.verifyText === undefined) {
80
- verifyTag = th.fg("dim", " [无需验证]");
81
- }
82
- lines.push(truncateToWidth(` ${mark} ${id} ${text}${verifyTag}`, width));
58
+ for (const line of renderDualColumn(this.todos, th, termWidth, indent)) {
59
+ lines.push(line);
83
60
  }
84
61
  }
85
62
 
86
63
  lines.push("");
87
- lines.push(truncateToWidth(` ${th.fg("dim", "Press Escape to close")}`, width));
64
+ lines.push(truncateToWidth(`${indent}${th.fg("dim", "Press Escape to close")}`, termWidth));
88
65
  lines.push("");
89
66
 
90
67
  this.cachedWidth = width;
package/src/handlers.ts CHANGED
@@ -1,13 +1,6 @@
1
1
  /**
2
2
  * Todo 事件处理器 — session_start / session_tree / agent_start /
3
3
  * before_agent_start / agent_end。
4
- *
5
- * 拆分理由:原 src/index.ts 中 agent_end 处理器约 60 行、before_agent_start
6
- * 约 40 行,均超过 §11 "事件处理器 ≤ 20 行" 限制。本文件将每个事件
7
- * handler 拆为 ≤ 20 行的 orchestrator + 职责单一的子函数。
8
- *
9
- * 行为契约:所有 handler 行为与原 index.ts 内的 pi.on 回调完全一致;
10
- * 任何与原代码的偏差都属于 bug。
11
4
  */
12
5
 
13
6
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
@@ -18,52 +11,59 @@ import {
18
11
  } from "./model";
19
12
  import type { TodoSessionState } from "./state";
20
13
 
21
- // ── 常量(与原 index.ts 内联常量保持一致) ──────────
14
+ // ── 常量 ────────────────────────────────────────────
22
15
 
23
- /** v3: 全部完成后保留的轮数,之后再自动 clear */
16
+ /** 全部完成后保留的轮数,之后再自动 clear */
24
17
  const AUTO_CLEAR_DELAY_ROUNDS = 2;
25
- /** v3: Stall 检测阈值(无 todo 活动轮数 → stall 提醒) */
18
+ /** Stall 检测阈值(无 todo 活动轮数 → stall 提醒) */
26
19
  const STALL_THRESHOLD = 5;
27
- /** v3: 提醒间隔(上次 todo 调用后轮数 → 提醒) */
28
- const REMINDER_INTERVAL = 3;
29
- /** v3: 最大验证失败次数 */
30
- const MAX_VERIFY_ATTEMPTS = 2;
20
+ /** 提醒间隔(上次 todo 调用后轮数 → 提醒) */
21
+ const REMINDER_INTERVAL = 2;
31
22
 
32
23
  // ── 辅助函数 ────────────────────────────────────────
33
24
 
34
- /** 刷新状态栏 + widget(由 index.ts 注入,闭包共享 state) */
35
25
  export type RefreshDisplayFn = (ctx: ExtensionContext) => void;
36
26
 
37
- /** 构建 pending 任务的 <todo_context> 字符串(agent_end stall/reminder 共用) */
38
- function buildPendingContext(state: TodoSessionState, turnCount: number): string {
27
+ /** 构建极简提醒:只含下一个推荐任务 */
28
+ function buildMinimalReminder(state: TodoSessionState): string {
39
29
  const pendingTodos = state.todos.filter((t) => t.status !== "completed");
40
- const pendingCount = pendingTodos.length;
41
- const completedCount = state.todos.filter((t) => t.status === "completed").length;
42
- const lines = pendingTodos
43
- .map((t) => {
44
- let verifyTag = "";
45
- if (t.status === "verifying") {
46
- verifyTag = ` [验证中${t.evidence ? ": " + t.evidence : ""}] → 需要 evidence 完成验证`;
47
- } else if (t.verifyText) {
48
- verifyTag = ` [待验证: ${t.verifyText}]`;
49
- }
50
- return `#${t.id}: ${t.text}${verifyTag}`;
51
- })
52
- .join("\n");
53
- return `<todo_context>\n[TODO] Turn ${turnCount} ${pendingCount} tasks pending, ${completedCount} completed\n${lines}\n\nRules:\n- 优先使用 updates[] 批量更新\n- 有 verifyText 的任务: 先标 verifying(evidence="验证进度") → 再标 completed(evidence="验证结论")\n- 无 verifyText 的任务可直接 completed\n- 全部完成后工具自动闭合\n</todo_context>`;
30
+ if (pendingTodos.length === 0) return "";
31
+
32
+ const next = pendingTodos[0];
33
+ return `<todo_context>\n[TODO] 你有 ${pendingTodos.length} 个未完成任务。下一个应处理:#${next.id} ${next.text}\n</todo_context>`;
34
+ }
35
+
36
+ /** 构建 before_agent_start todo context */
37
+ function buildBeforeAgentStartMessage(state: TodoSessionState): { message: { customType: string; content: string; display: boolean } } | undefined {
38
+ if (state.todos.length === 0) return undefined;
39
+
40
+ const pendingTodos = state.todos.filter((t) => t.status !== "completed");
41
+ if (pendingTodos.length === 0) return undefined;
42
+
43
+ const lines = pendingTodos.map((t) => `#${t.id}: ${t.text}`);
44
+ const contextStr =
45
+ `<todo_context>\n[TODO] ${pendingTodos.length} tasks pending\n${lines.join("\n")}\n</todo_context>`;
46
+
47
+ return {
48
+ message: {
49
+ customType: "todo-context",
50
+ content: contextStr,
51
+ display: false,
52
+ },
53
+ };
54
54
  }
55
55
 
56
56
  // ── 状态重建 ────────────────────────────────────────
57
57
 
58
- /** 从 session entries 重建 state(兼容旧 done:boolean 格式 + entry GC) */
59
58
  export function reconstructState(state: TodoSessionState, ctx: ExtensionContext): void {
60
59
  state.todos = [];
61
60
  state.nextId = 1;
62
- // v3: 重置提醒追踪状态
63
61
  state.userMessageCount = 0;
64
62
  state.lastTodoCallCount = 0;
65
63
  state.stallNotified = false;
66
64
  state.allCompletedAtCount = null;
65
+ state.completionSteered = false;
66
+ state.pendingSteerMessage = null;
67
67
 
68
68
  const entries = ctx.sessionManager.getEntries();
69
69
  let latestIdx = -1;
@@ -96,49 +96,11 @@ export function reconstructState(state: TodoSessionState, ctx: ExtensionContext)
96
96
  entries.splice(staleIndices[j], 1);
97
97
  }
98
98
  }
99
-
100
- // auto-clear 由 agent_end 延迟处理(AUTO_CLEAR_DELAY_ROUNDS),不再在此立即清空
101
99
  }
102
100
 
103
- // ── before_agent_start 子函数 ───────────────────────
104
-
105
- /** 构造 before_agent_start 返回的 todo context 消息 */
106
- function buildBeforeAgentStartMessage(state: TodoSessionState): { message: { customType: string; content: string; display: boolean } } | undefined {
107
- if (state.todos.length === 0) return undefined;
101
+ // ── agent_end 子函数 ────────────────────────────────
108
102
 
109
- const pendingTodos = state.todos.filter((t) => t.status !== "completed");
110
- if (pendingTodos.length === 0) return undefined;
111
-
112
- // 格式化 pending 任务 (含 verifying 状态和 verifyText)
113
- const lines = pendingTodos.map((t) => {
114
- let verifyTag = "";
115
- if (t.status === "verifying") {
116
- verifyTag = ` [验证中${t.evidence ? ": " + t.evidence : ""}] → 需要 evidence 完成验证`;
117
- } else if (t.verifyText) {
118
- verifyTag = ` [待验证: ${t.verifyText}]`;
119
- }
120
- return `#${t.id}: ${t.text}${verifyTag}`;
121
- });
122
-
123
- const contextStr =
124
- `<todo_context>\n[TODO] ${pendingTodos.length} tasks pending\n${lines.join("\n")}\n\nRules:\n- 有 verifyText 的任务: 先标 verifying(evidence="验证进度") → 再标 completed(evidence="验证结论")\n- 无 verifyText 的任务可直接 completed\n- 全部完成后工具自动闭合\n</todo_context>`;
125
-
126
- return {
127
- message: {
128
- customType: "todo-context",
129
- content: contextStr,
130
- display: false,
131
- },
132
- };
133
- }
134
-
135
- // ── agent_end 4 个子函数 ───────────────────────────
136
-
137
- /** 1. Auto-clear: 所有 todo 都 completed → 延迟 N 轮后 clear
138
- * 返回 { handled, cleared }:
139
- * - handled=true → orchestrator 应直接 return(allCompleted 路径或实际已清空)
140
- * - cleared=true → 实际清空了 todos,orchestrator 需要 refreshDisplay
141
- */
103
+ /** 1. Auto-clear */
142
104
  function handleAutoClear(state: TodoSessionState): { handled: boolean; cleared: boolean } {
143
105
  const allCompleted = state.todos.every((t) => t.status === "completed");
144
106
  if (!allCompleted) {
@@ -152,51 +114,46 @@ function handleAutoClear(state: TodoSessionState): { handled: boolean; cleared:
152
114
  state.todos = [];
153
115
  state.nextId = 1;
154
116
  state.allCompletedAtCount = null;
117
+ state.completionSteered = false;
155
118
  return { handled: true, cleared: true };
156
119
  }
157
120
  return { handled: true, cleared: false };
158
121
  }
159
122
 
160
- /** 2. Verify 失败处理: verifyAttempts >= MAX 且仍为 completed → 设 failed */
161
- function handleVerifyFailure(state: TodoSessionState, pi: ExtensionAPI): boolean {
162
- const failedIds: number[] = [];
163
- for (const t of state.todos) {
164
- if (
165
- t.status === "completed" &&
166
- t.verifyText &&
167
- t.verifyAttempts >= MAX_VERIFY_ATTEMPTS
168
- ) {
169
- t.status = "failed";
170
- failedIds.push(t.id);
171
- }
172
- }
173
- if (failedIds.length > 0) {
174
- pi.sendUserMessage(
175
- `<todo_context>\n[TODO] 验证失败: Task ${failedIds.map((id) => "#" + id).join(", ")} 已重试 ${MAX_VERIFY_ATTEMPTS} 次仍未通过,已标记为 failed。请决定是否手动 override。\n</todo_context>`,
176
- { deliverAs: "steer", customType: "todo-context" },
177
- );
178
- return true;
179
- }
180
- return false;
123
+ /** 2. 全部 completed 时设置延迟 steer(仅一次),由 before_agent_start 消费 */
124
+ function handleCompletionSteer(state: TodoSessionState): boolean {
125
+ if (state.completionSteered) return false;
126
+ const allCompleted = state.todos.length > 0 && state.todos.every((t) => t.status === "completed");
127
+ if (!allCompleted) return false;
128
+
129
+ state.completionSteered = true;
130
+ state.pendingSteerMessage = `<todo_context>\n[TODO] 所有任务已完成。请快速检查每项任务的交付质量。\n</todo_context>`;
131
+ return true;
181
132
  }
182
133
 
183
- /** 3. Stall 检测: STALL_THRESHOLD 轮未调用 todo 且还有未完成任务 */
184
- function handleStallDetection(state: TodoSessionState, pi: ExtensionAPI): boolean {
134
+ /** 3. Stall 检测 设置延迟 steer */
135
+ function handleStallDetection(state: TodoSessionState): boolean {
185
136
  if (
186
137
  !state.stallNotified &&
187
138
  state.userMessageCount - state.lastTodoCallCount >= STALL_THRESHOLD
188
139
  ) {
189
140
  state.stallNotified = true;
190
- pi.sendUserMessage(buildPendingContext(state, state.userMessageCount), { deliverAs: "steer", customType: "todo-context" });
141
+ const reminder = buildMinimalReminder(state);
142
+ if (reminder) {
143
+ state.pendingSteerMessage = reminder;
144
+ }
191
145
  return true;
192
146
  }
193
147
  return false;
194
148
  }
195
149
 
196
- /** 4. 提醒: REMINDER_INTERVAL 轮未调用 todo */
197
- function handleReminder(state: TodoSessionState, pi: ExtensionAPI): boolean {
150
+ /** 4. 提醒 设置延迟 steer */
151
+ function handleReminder(state: TodoSessionState): boolean {
198
152
  if (state.userMessageCount - state.lastTodoCallCount >= REMINDER_INTERVAL) {
199
- pi.sendUserMessage(buildPendingContext(state, state.userMessageCount), { deliverAs: "steer", customType: "todo-context" });
153
+ const reminder = buildMinimalReminder(state);
154
+ if (reminder) {
155
+ state.pendingSteerMessage = reminder;
156
+ }
200
157
  return true;
201
158
  }
202
159
  return false;
@@ -204,13 +161,11 @@ function handleReminder(state: TodoSessionState, pi: ExtensionAPI): boolean {
204
161
 
205
162
  // ── Event handler 注册入口 ──────────────────────────
206
163
 
207
- /** 注册所有 todo 事件处理器到 pi */
208
164
  export function registerTodoEventHandlers(
209
165
  pi: ExtensionAPI,
210
166
  state: TodoSessionState,
211
167
  refreshDisplay: RefreshDisplayFn,
212
168
  ): void {
213
- // session_start / session_tree: 重建 state + 刷新显示
214
169
  pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
215
170
  reconstructState(state, ctx);
216
171
  refreshDisplay(ctx);
@@ -220,18 +175,23 @@ export function registerTodoEventHandlers(
220
175
  refreshDisplay(ctx);
221
176
  });
222
177
 
223
- // v3: 追踪用户消息轮数
224
178
  pi.on("agent_start", async (_event: unknown, _ctx: ExtensionContext) => {
225
179
  state.userMessageCount++;
226
180
  });
227
181
 
228
- // v3: Task 6 - before_agent_start 注入 todo context (display: false)
229
182
  pi.on("before_agent_start", async (_event: unknown, ctx: ExtensionContext) => {
230
183
  try {
231
184
  const pendingTodos = state.todos.filter((t) => t.status !== "completed");
232
185
  if (pendingTodos.length > 0) {
233
186
  ctx.ui.setStatus("todo", `📋 ${pendingTodos.length} pending`);
234
187
  }
188
+ // 优先级 1: agent_end 设置的延迟 steer
189
+ if (state.pendingSteerMessage) {
190
+ const msg = state.pendingSteerMessage;
191
+ state.pendingSteerMessage = null;
192
+ return { message: { customType: "todo-context", content: msg, display: false } };
193
+ }
194
+
235
195
  return buildBeforeAgentStartMessage(state);
236
196
  } catch (e) {
237
197
  console.debug("[todo] before_agent_start error:", e);
@@ -239,15 +199,19 @@ export function registerTodoEventHandlers(
239
199
  }
240
200
  });
241
201
 
242
- // agent_end: auto-clear + verify-failed + stall + reminder (≤ 20 行 orchestrator)
243
202
  pi.on("agent_end", async (_event: unknown, ctx: ExtensionContext) => {
244
203
  try {
245
204
  if (state.todos.length === 0) return;
205
+
206
+ // 全部 completed → 总检查 steer(仅一次)
207
+ handleCompletionSteer(state);
208
+
209
+ // auto-clear
246
210
  const ac = handleAutoClear(state);
247
211
  if (ac.handled) { if (ac.cleared) refreshDisplay(ctx); return; }
248
- if (handleVerifyFailure(state, pi)) { refreshDisplay(ctx); return; }
249
- if (handleStallDetection(state, pi)) return;
250
- handleReminder(state, pi);
212
+
213
+ if (handleStallDetection(state)) return;
214
+ handleReminder(state);
251
215
  } catch (e) {
252
216
  console.debug("[todo] agent_end error:", e);
253
217
  }
package/src/index.ts CHANGED
@@ -17,13 +17,13 @@
17
17
  * - component.ts: TodoListComponent TUI 组件
18
18
  * - tool.ts: TodoParams schema + 5 个 action handler + execute dispatcher + registerTodoTool
19
19
  * - handlers.ts: 5 个事件处理器 + reconstructState + buildPendingContext
20
- * - commands.ts: /todos 命令 + todo-context 消息渲染器
20
+ * - commands.ts: /todos 命令
21
21
  * - index.ts(本文件): 工厂入口(创建 state + 注册所有 handler)
22
22
  */
23
23
 
24
24
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
25
25
 
26
- import { registerTodoContextRenderer,registerTodosCommand } from "./commands";
26
+ import { registerTodosCommand } from "./commands";
27
27
  import { registerTodoEventHandlers } from "./handlers";
28
28
  import { renderStatusText, renderWidgetLines } from "./render";
29
29
  import { createTodoSessionState } from "./state";
@@ -50,5 +50,4 @@ export default function (pi: ExtensionAPI) {
50
50
  registerTodoEventHandlers(pi, state, refreshDisplay);
51
51
  registerTodoTool(pi, state, refreshDisplay);
52
52
  registerTodosCommand(pi, state);
53
- registerTodoContextRenderer(pi);
54
53
  }