@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/package.json +1 -1
- package/src/__tests__/todo.test.ts +199 -594
- package/src/commands.ts +5 -26
- package/src/component.ts +11 -34
- package/src/handlers.ts +72 -108
- package/src/index.ts +2 -3
- package/src/model.ts +25 -137
- package/src/render.ts +101 -66
- package/src/state.ts +7 -1
- package/src/tool.ts +28 -130
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
|
-
*
|
|
6
|
-
*
|
|
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,
|
|
48
|
-
lines.push(truncateToWidth(headerLine,
|
|
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(
|
|
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(
|
|
55
|
+
lines.push(truncateToWidth(`${indent}${th.fg("muted", `${completed}/${total} completed`)}`, termWidth));
|
|
57
56
|
lines.push("");
|
|
58
57
|
|
|
59
|
-
for (const
|
|
60
|
-
|
|
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(
|
|
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
|
-
// ──
|
|
14
|
+
// ── 常量 ────────────────────────────────────────────
|
|
22
15
|
|
|
23
|
-
/**
|
|
16
|
+
/** 全部完成后保留的轮数,之后再自动 clear */
|
|
24
17
|
const AUTO_CLEAR_DELAY_ROUNDS = 2;
|
|
25
|
-
/**
|
|
18
|
+
/** Stall 检测阈值(无 todo 活动轮数 → stall 提醒) */
|
|
26
19
|
const STALL_THRESHOLD = 5;
|
|
27
|
-
/**
|
|
28
|
-
const REMINDER_INTERVAL =
|
|
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
|
-
/**
|
|
38
|
-
function
|
|
27
|
+
/** 构建极简提醒:只含下一个推荐任务 */
|
|
28
|
+
function buildMinimalReminder(state: TodoSessionState): string {
|
|
39
29
|
const pendingTodos = state.todos.filter((t) => t.status !== "completed");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
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.
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
184
|
-
function handleStallDetection(state: TodoSessionState
|
|
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
|
-
|
|
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.
|
|
197
|
-
function handleReminder(state: TodoSessionState
|
|
150
|
+
/** 4. 提醒 — 设置延迟 steer */
|
|
151
|
+
function handleReminder(state: TodoSessionState): boolean {
|
|
198
152
|
if (state.userMessageCount - state.lastTodoCallCount >= REMINDER_INTERVAL) {
|
|
199
|
-
|
|
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
|
-
|
|
249
|
-
if (handleStallDetection(state
|
|
250
|
-
handleReminder(state
|
|
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 命令
|
|
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 {
|
|
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
|
}
|