@zhushanwen/pi-todo 0.1.5 → 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/model.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Todo 数据模型 — 纯函数,不依赖 Pi 运行时。
3
- * index.ts 提取,便于单元测试。
3
+ * 三态: pending → in_progress → completed
4
4
  */
5
5
 
6
6
  // ── 数据模型 ─────────────────────────────────────────
@@ -8,11 +8,7 @@
8
8
  export interface Todo {
9
9
  id: number;
10
10
  text: string;
11
- verifyText?: string;
12
- status: "pending" | "in_progress" | "verifying" | "completed" | "failed";
13
- verifyAttempts: number;
14
- /** 验证结论,verifying/completed 时写入 */
15
- evidence?: string;
11
+ status: "pending" | "in_progress" | "completed";
16
12
  }
17
13
 
18
14
  export interface TodoDetails {
@@ -30,50 +26,49 @@ export interface TodoDetails {
30
26
  };
31
27
  }
32
28
 
33
- export const VALID_STATUSES = ["pending", "in_progress", "verifying", "completed", "failed"] as const;
29
+ export const VALID_STATUSES = ["pending", "in_progress", "completed"] as const;
34
30
 
35
31
  export type ValidStatus = (typeof VALID_STATUSES)[number];
36
32
 
37
33
  // ── 迁移/兼容 ───────────────────────────────────────
38
34
 
39
- // ── Stale Context Detection ──────────────────────────
40
-
41
35
  const STALE_CONTEXT_PATTERNS = ["aborted", "context canceled", "stale context", "stalecontext", "extension context no longer active"];
42
36
 
43
- /** 检查错误是否表示 stale / canceled context(如 session 重建 / compact 后) */
37
+ /** 检查错误是否表示 stale / canceled context */
44
38
  export function isStaleContextError(error: Error | unknown): boolean {
45
39
  const msg = error instanceof Error ? error.message : String(error);
46
40
  const lower = msg.toLowerCase();
47
41
  return STALE_CONTEXT_PATTERNS.some((p) => lower.includes(p));
48
42
  }
49
43
 
50
-
51
- /** 兼容旧格式:旧 entry 可能有 done: boolean,转换为 status;旧数据缺少 verifyText/verifyAttempts 时填充默认值 */
44
+ /** 旧格式迁移:verifying → in_progress,failed → pending,done:boolean → status */
52
45
  export function migrateTodo(raw: Todo): Todo {
53
46
  const record = raw as unknown as Record<string, unknown>;
54
47
  const hasValidStatus =
55
48
  typeof record.status === "string" &&
56
49
  VALID_STATUSES.includes(record.status as ValidStatus);
57
50
 
58
- const status: ValidStatus = hasValidStatus
59
- ? (record.status as ValidStatus)
60
- : (() => {
61
- const { done } = record as { done?: boolean };
62
- return done === true ? "completed" : "pending";
63
- })();
51
+ let status: ValidStatus;
52
+ if (hasValidStatus) {
53
+ status = record.status as ValidStatus;
54
+ } else {
55
+ // 极旧格式 done: boolean
56
+ const { done } = record as { done?: boolean };
57
+ status = done === true ? "completed" : "pending";
58
+ }
59
+
60
+ // 旧版五态映射(先转 string 避免类型收窄后无法比较)
61
+ const rawStatus = record.status as string | undefined;
62
+ if (rawStatus === "verifying") status = "in_progress";
63
+ if (rawStatus === "failed") status = "pending";
64
64
 
65
65
  return {
66
66
  id: record.id as number,
67
67
  text: record.text as string,
68
68
  status,
69
- verifyText: typeof record.verifyText === "string" ? (record.verifyText as string) : undefined,
70
- verifyAttempts: typeof record.verifyAttempts === "number" ? (record.verifyAttempts as number) : 0,
71
- evidence: typeof record.evidence === "string" ? (record.evidence as string) : undefined,
72
69
  };
73
70
  }
74
71
 
75
-
76
-
77
72
  // ── 渲染辅助 ─────────────────────────────────────────
78
73
 
79
74
  export function buildRender(todoList: Todo[]): TodoDetails["_render"] {
@@ -93,7 +88,7 @@ export function getDisplayStatus(t: Todo): string {
93
88
  return migrateTodo(t).status;
94
89
  }
95
90
 
96
- // ── Add 逻辑(纯函数,可测试) ─────────────────────
91
+ // ── Add 逻辑 ─────────────────────────────────────────
97
92
 
98
93
  export interface AddResult {
99
94
  newTodos: Todo[];
@@ -102,18 +97,10 @@ export interface AddResult {
102
97
  resultText?: string;
103
98
  }
104
99
 
105
- /**
106
- * 处理 todo add 的核心逻辑。
107
- * @param currentTodos 当前 todo 列表
108
- * @param currentNextId 当前 nextId
109
- * @param texts 要添加的文本列表
110
- * @param verifyTexts 可选的验证文本列表
111
- */
112
100
  export function addTodos(
113
101
  currentTodos: Todo[],
114
102
  currentNextId: number,
115
103
  texts: string[],
116
- verifyTexts?: string[],
117
104
  ): AddResult {
118
105
  if (!texts || texts.length === 0) {
119
106
  return {
@@ -134,15 +121,6 @@ export function addTodos(
134
121
  };
135
122
  }
136
123
 
137
- if (verifyTexts !== undefined && verifyTexts.length > trimmed.length) {
138
- return {
139
- newTodos: currentTodos,
140
- newNextId: currentNextId,
141
- error: "verifyTexts too long",
142
- resultText: "Error: verifyTexts length cannot exceed texts length",
143
- };
144
- }
145
-
146
124
  const startId = currentNextId;
147
125
  const newTodos = [...currentTodos];
148
126
  let nextId = currentNextId;
@@ -151,8 +129,6 @@ export function addTodos(
151
129
  id: nextId++,
152
130
  text: trimmed[i],
153
131
  status: "pending" as const,
154
- verifyText: verifyTexts?.[i],
155
- verifyAttempts: 0,
156
132
  });
157
133
  }
158
134
  const endId = nextId - 1;
@@ -164,36 +140,18 @@ export function addTodos(
164
140
  };
165
141
  }
166
142
 
167
- // ── Update 逻辑(纯函数,可测试) ──────────────────
143
+ // ── Update 逻辑 ──────────────────────────────────────
168
144
 
169
145
  export interface UpdateResult {
170
146
  updatedTodos: Todo[];
171
147
  error?: string;
172
148
  resultText?: string;
173
- /** 拦截的任务 ID 及其原因 */
174
- blocked?: Array<{ id: number; text: string; reason: string }>;
175
149
  }
176
150
 
177
- /** evidence 最小长度 */
178
- const MIN_EVIDENCE_LENGTH = 10;
179
- /** 最大验证重试次数 */
180
- const MAX_VERIFY_ATTEMPTS = 2;
181
-
182
- /**
183
- * 处理 todo batch update 的核心逻辑。
184
- * All-or-nothing: 任一验证失败,所有变更不生效。
185
- *
186
- * 状态转换规则:
187
- * - 无 verifyText: in_progress → completed 直接通过
188
- * - 有 verifyText: in_progress → verifying (需 evidence) → completed (需 evidence)
189
- * - 有 verifyText: in_progress → completed 需 verified=true + evidence(跳过 verifying)
190
- * - verifying → in_progress: verifyAttempts++
191
- */
192
151
  export function updateTodos(
193
152
  currentTodos: Todo[],
194
- updates: Array<{ id: number; status?: string; text?: string; verified?: boolean; evidence?: string }>,
153
+ updates: Array<{ id: number; status?: string; text?: string }>,
195
154
  ): UpdateResult {
196
- // 验证: no duplicate ids
197
155
  const ids = updates.map((u) => u.id);
198
156
  if (new Set(ids).size !== ids.length) {
199
157
  return {
@@ -202,7 +160,6 @@ export function updateTodos(
202
160
  resultText: "Error: duplicate ids in updates",
203
161
  };
204
162
  }
205
- // 验证: all ids exist and each has at least one change, and valid status
206
163
  for (const u of updates) {
207
164
  const todo = currentTodos.find((t) => t.id === u.id);
208
165
  if (!todo) {
@@ -228,68 +185,12 @@ export function updateTodos(
228
185
  }
229
186
  }
230
187
 
231
- // 状态转换拦截
232
- const blockedItems: Array<{ id: number; text: string; reason: string }> = [];
233
-
234
- for (const u of updates) {
235
- if (!u.status) continue;
236
- const todo = currentTodos.find((t) => t.id === u.id)!;
237
-
238
- if (u.status === "verifying") {
239
- // → verifying: 必须有 verifyText + evidence
240
- if (!todo.verifyText) {
241
- blockedItems.push({ id: todo.id, text: todo.text, reason: "无 verifyText 的任务不能进入 verifying 状态" });
242
- } else if (!u.evidence || u.evidence.trim().length < MIN_EVIDENCE_LENGTH) {
243
- blockedItems.push({ id: todo.id, text: todo.text, reason: `进入 verifying 需要 evidence(≥${MIN_EVIDENCE_LENGTH} 字符),说明当前验证进度` });
244
- }
245
- } else if (u.status === "completed") {
246
- if (todo.verifyText && todo.status !== "verifying") {
247
- // 有 verifyText 但未经过 verifying → 需要 verified=true + evidence
248
- if (u.verified !== true) {
249
- blockedItems.push({ id: todo.id, text: todo.text, reason: `有验证要求,请先标 verifying 并传 evidence,或传 verified=true + evidence 跳过` });
250
- } else if (!u.evidence || u.evidence.trim().length < MIN_EVIDENCE_LENGTH) {
251
- blockedItems.push({ id: todo.id, text: todo.text, reason: `跳过 verifying 直接 completed 时需要 evidence(≥${MIN_EVIDENCE_LENGTH} 字符)` });
252
- }
253
- } else if (todo.status === "verifying") {
254
- // verifying → completed: 需要 evidence
255
- if (!u.evidence || u.evidence.trim().length < MIN_EVIDENCE_LENGTH) {
256
- blockedItems.push({ id: todo.id, text: todo.text, reason: `从 verifying 到 completed 需要 evidence(≥${MIN_EVIDENCE_LENGTH} 字符),说明验证结论` });
257
- }
258
- }
259
- }
260
- }
261
-
262
- if (blockedItems.length > 0) {
263
- const lines = blockedItems.map(
264
- (b) => ` #${b.id}: ${b.text} — ${b.reason}`,
265
- );
266
- return {
267
- updatedTodos: currentTodos,
268
- resultText: `⚠️ 以下任务被拦截:\n${lines.join("\n")}`,
269
- blocked: blockedItems,
270
- };
271
- }
272
-
273
- // Apply all (safe since all validated)
274
188
  const updated = currentTodos.map((t) => {
275
189
  const u = updates.find((u) => u.id === t.id);
276
190
  if (!u) return t;
277
191
  const patch: Partial<Todo> = {};
278
192
  if (u.status) patch.status = u.status as Todo["status"];
279
193
  if (u.text) patch.text = u.text;
280
- // verifying 或 completed 时写入 evidence
281
- if (u.evidence && (u.status === "verifying" || u.status === "completed")) {
282
- patch.evidence = u.evidence.trim();
283
- }
284
- // verifying/completed → in_progress: verifyAttempts++(验证失败回退)
285
- if (
286
- u.status === "in_progress" &&
287
- t.verifyText &&
288
- t.verifyAttempts < MAX_VERIFY_ATTEMPTS &&
289
- (t.status === "completed" || t.status === "verifying")
290
- ) {
291
- patch.verifyAttempts = t.verifyAttempts + 1;
292
- }
293
194
  return { ...t, ...patch };
294
195
  });
295
196
  return {
@@ -300,25 +201,12 @@ export function updateTodos(
300
201
 
301
202
  // ── 格式化辅助 ───────────────────────────────────────
302
203
 
303
- /** 格式化单条 todo 为纯文本行(AI 可读),含 verifyText 原文 */
304
204
  export function formatTodoLine(t: Todo): string {
305
205
  const mark =
306
206
  t.status === "completed"
307
207
  ? "x"
308
- : t.status === "verifying"
309
- ? "v"
310
- : t.status === "in_progress"
311
- ? "~"
312
- : t.status === "failed"
313
- ? "!"
314
- : " ";
315
- let line = `[${mark}] #${t.id}: ${t.text}`;
316
- if (t.status === "verifying" && t.evidence) {
317
- line += ` | 验证中: ${t.evidence}`;
318
- } else if (t.status === "completed" && t.evidence) {
319
- line += ` | 已验证: ${t.evidence}`;
320
- } else if (t.verifyText) {
321
- line += ` | 验证: ${t.verifyText}`;
322
- }
323
- return line;
208
+ : t.status === "in_progress"
209
+ ? "~"
210
+ : " ";
211
+ return `[${mark}] #${t.id}: ${t.text}`;
324
212
  }
package/src/render.ts CHANGED
@@ -1,13 +1,9 @@
1
1
  /**
2
- * Todo 渲染函数 — 状态栏、widgettool result 渲染。
3
- *
4
- * 拆分理由:原 src/index.ts 把渲染逻辑与业务逻辑混在一起。提取后 index.ts
5
- * 工厂只需调用 register* 函数;这些纯函数接受 Todo[] / theme 等入参,
6
- * 不依赖闭包状态,可独立测试。
2
+ * Todo 渲染函数 — 状态栏、widget(双列)、tool result 渲染。
7
3
  */
8
4
 
9
5
  import type { Theme } from "@mariozechner/pi-coding-agent";
10
- import { Text } from "@mariozechner/pi-tui";
6
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
11
7
 
12
8
  import {
13
9
  buildRender,
@@ -16,59 +12,123 @@ import {
16
12
  type TodoDetails,
17
13
  } from "./model";
18
14
 
15
+ // ── 常量 ────────────────────────────────────────────
16
+
19
17
  const MAX_COLLAPSED_ITEMS = 5;
18
+ export const FALLBACK_TERM_WIDTH = 80;
19
+
20
+ /**
21
+ * Pi 对单个 extension widget 的最大字符串行数为 10(InteractiveMode.MAX_WIDGET_LINES)。
22
+ * 扩展侧保守使用 max - 1 = 9 行作为阈值,超过时切换为双列布局,避免触发截断。
23
+ */
24
+ const WIDGET_MAX_LINES = 9;
25
+ const SINGLE_COLUMN_BUDGET = WIDGET_MAX_LINES - 1;
26
+
27
+ /** 垂直分割线视觉宽度(" │ ") */
28
+ const DIVIDER_VISUAL_WIDTH = 3;
29
+ const ELLIPSIS_MIN_WIDTH = 3;
30
+
31
+ /** 截断或补齐到精确视觉宽度,截断时追加 "..." */
32
+ function fixedWidth(text: string, width: number): string {
33
+ const len = visibleWidth(text);
34
+ if (len <= width) {
35
+ return text + " ".repeat(width - len);
36
+ }
37
+ if (width <= ELLIPSIS_MIN_WIDTH) return "...".slice(0, width);
38
+ return truncateToWidth(text, width - ELLIPSIS_MIN_WIDTH) + "...";
39
+ }
20
40
 
21
41
  // ── 状态栏 ────────────────────────────────────────────
22
42
 
23
- /** 渲染状态栏文本 */
24
43
  export function renderStatusText(todoList: Todo[], th: Theme): string {
25
44
  if (todoList.length === 0) return "";
26
45
 
27
46
  const completed = todoList.filter((t) => getDisplayStatus(t) === "completed").length;
28
47
  const total = todoList.length;
29
48
 
30
- // 全部完成
31
49
  if (completed === total) {
32
50
  return th.fg("success", `\u2713 ${completed}/${total}`);
33
51
  }
34
- // 有未完成
35
52
  return th.fg("accent", "\u2611") + th.fg("muted", ` ${completed}/${total}`);
36
53
  }
37
54
 
38
- /** 渲染 widget */
39
- export function renderWidgetLines(todoList: Todo[], th: Theme): string[] {
55
+ // ── Widget 双列渲染 ──────────────────────────────────
56
+
57
+ /** 渲染单条 todo 的 widget 行(不含缩进),供 component.ts 复用 */
58
+ export function renderWidgetItem(t: Todo, th: Theme): string {
59
+ const mark =
60
+ t.status === "completed"
61
+ ? th.fg("success", "\u2713")
62
+ : t.status === "in_progress"
63
+ ? th.fg("warning", "\u25cf")
64
+ : th.fg("dim", "\u25cb");
65
+ const id = th.fg("accent", `#${t.id}`);
66
+ const text = t.status === "completed" ? th.fg("dim", t.text) : th.fg("text", t.text);
67
+ return `${mark} ${id} ${text}`;
68
+ }
69
+
70
+ /** 单列布局渲染(widget 少量任务时使用) */
71
+ const PI_TEXT_PADDING = 2;
72
+
73
+ export function renderSingleColumn(
74
+ todos: Todo[],
75
+ th: Theme,
76
+ termWidth: number,
77
+ indent: string,
78
+ ): string[] {
79
+ const maxWidth = Math.max(1, termWidth - PI_TEXT_PADDING);
80
+ return todos.map((t) => truncateToWidth(indent + renderWidgetItem(t, th), maxWidth));
81
+ }
82
+
83
+ /** 双列布局渲染,供 widget 和 component 复用 */
84
+ const COLUMN_COUNT = 2;
85
+
86
+ export function renderDualColumn(
87
+ todos: Todo[],
88
+ th: Theme,
89
+ termWidth: number,
90
+ indent: string,
91
+ ): string[] {
92
+ const colWidth = Math.floor((termWidth - indent.length - DIVIDER_VISUAL_WIDTH) / COLUMN_COUNT);
93
+ const lines: string[] = [];
94
+ const half = Math.ceil(todos.length / COLUMN_COUNT);
95
+ const divider = " " + th.fg("borderMuted", "\u2502") + " ";
96
+ for (let row = 0; row < half; row++) {
97
+ const left = fixedWidth(indent + renderWidgetItem(todos[row], th), colWidth);
98
+ const rightIdx = row + half;
99
+ const right = rightIdx < todos.length
100
+ ? fixedWidth(renderWidgetItem(todos[rightIdx], th), colWidth)
101
+ : " ".repeat(colWidth);
102
+ lines.push(left + divider + right);
103
+ }
104
+ return lines;
105
+ }
106
+
107
+ /** 渲染 widget 行(根据任务数自动选择单列或双列布局) */
108
+ export function renderWidgetLines(
109
+ todoList: Todo[],
110
+ th: Theme,
111
+ termWidth?: number,
112
+ ): string[] {
40
113
  if (todoList.length === 0) return [];
41
114
 
115
+ const width = termWidth ?? (process.stdout.columns || FALLBACK_TERM_WIDTH);
42
116
  const lines: string[] = [];
43
117
  const completed = todoList.filter((t) => getDisplayStatus(t) === "completed").length;
44
118
  const total = todoList.length;
45
119
 
46
120
  lines.push(th.fg("accent", "\u2611") + th.fg("muted", ` ${completed}/${total}`));
47
121
 
48
- for (const t of todoList) {
49
- const mark =
50
- t.status === "completed"
51
- ? th.fg("success", "\u2713")
52
- : t.status === "verifying"
53
- ? th.fg("warning", "\u25d0")
54
- : t.status === "in_progress"
55
- ? th.fg("warning", "\u25cf")
56
- : t.status === "failed"
57
- ? th.fg("error", "\u2717")
58
- : th.fg("dim", "\u25cb");
59
- const id = th.fg("accent", `#${t.id}`);
60
- const text = t.status === "completed" ? th.fg("dim", t.text) : th.fg("text", t.text);
61
- let verifyTag = "";
62
- if (t.status === "verifying") {
63
- verifyTag = th.fg("warning", ` [验证中${t.evidence ? ": " + t.evidence.slice(0, 30) : ""}]`);
64
- } else if (t.verifyText && t.status !== "completed") {
65
- verifyTag = th.fg("warning", " [待验证]");
66
- } else if (t.status === "completed" && t.verifyText) {
67
- verifyTag = th.fg("success", " [已验证]");
68
- } else if (t.verifyText === undefined) {
69
- verifyTag = th.fg("dim", " [无需验证]");
122
+ // 标题占 1 行;任务部分超过 WIDGET_MAX_LINES - 1 时启用双列
123
+ const indent = " ";
124
+ if (todoList.length <= SINGLE_COLUMN_BUDGET) {
125
+ for (const line of renderSingleColumn(todoList, th, width, indent)) {
126
+ lines.push(line);
127
+ }
128
+ } else {
129
+ for (const line of renderDualColumn(todoList, th, width, indent)) {
130
+ lines.push(line);
70
131
  }
71
- lines.push(` ${mark} ${id} ${text}${verifyTag}`);
72
132
  }
73
133
 
74
134
  return lines;
@@ -76,7 +136,6 @@ export function renderWidgetLines(todoList: Todo[], th: Theme): string[] {
76
136
 
77
137
  // ── 列表渲染辅助函数 ─────────────────────────────────
78
138
 
79
- /** 拼装 todo 列表的纯文本表示(AI/工具结果消费) */
80
139
  export function buildTodoListText(todoList: Todo[], options: { expanded: boolean }, theme: Theme): string {
81
140
  if (todoList.length === 0) {
82
141
  return theme.fg("dim", "No todos");
@@ -88,26 +147,12 @@ export function buildTodoListText(todoList: Todo[], options: { expanded: boolean
88
147
  const mark =
89
148
  status === "completed"
90
149
  ? theme.fg("success", "\u2713")
91
- : status === "verifying"
92
- ? theme.fg("warning", "\u25d0")
93
- : status === "in_progress"
94
- ? theme.fg("warning", "\u25cf")
95
- : status === "failed"
96
- ? theme.fg("error", "\u2717")
97
- : theme.fg("dim", "\u25cb");
150
+ : status === "in_progress"
151
+ ? theme.fg("warning", "\u25cf")
152
+ : theme.fg("dim", "\u25cb");
98
153
  const itemText =
99
154
  status === "completed" ? theme.fg("dim", t.text) : theme.fg("muted", t.text);
100
- let verifyTag = "";
101
- if (status === "verifying") {
102
- verifyTag = theme.fg("warning", ` [验证中${t.evidence ? ": " + t.evidence.slice(0, 30) : ""}]`);
103
- } else if (t.verifyText && status !== "completed") {
104
- verifyTag = theme.fg("warning", " [待验证]");
105
- } else if (status === "completed" && t.verifyText) {
106
- verifyTag = theme.fg("success", " [已验证]");
107
- } else if (t.verifyText === undefined) {
108
- verifyTag = theme.fg("dim", " [无需验证]");
109
- }
110
- listText += `\n${mark} ${theme.fg("accent", `#${t.id}`)} ${itemText}${verifyTag}`;
155
+ listText += `\n${mark} ${theme.fg("accent", `#${t.id}`)} ${itemText}`;
111
156
  }
112
157
  if (!options.expanded && todoList.length > MAX_COLLAPSED_ITEMS) {
113
158
  listText += `\n${theme.fg("dim", `... ${todoList.length - MAX_COLLAPSED_ITEMS} more`)}`;
@@ -117,7 +162,8 @@ export function buildTodoListText(todoList: Todo[], options: { expanded: boolean
117
162
 
118
163
  // ── Tool renderResult handler ────────────────────────
119
164
 
120
- /** 渲染 tool execute 返回结果 */
165
+ import { Text } from "@mariozechner/pi-tui";
166
+
121
167
  export function renderTodoResult(result: unknown, options: { expanded: boolean }, theme: Theme): Text {
122
168
  const r = result as { content: Array<{ type: string; text?: string }>; details?: unknown };
123
169
  const details = r.details as TodoDetails | undefined;
@@ -137,17 +183,7 @@ export function renderTodoResult(result: unknown, options: { expanded: boolean }
137
183
  return new Text(buildTodoListText(todoList, options, theme), 0, 0);
138
184
  }
139
185
 
140
- case "add": {
141
- const text = r.content[0];
142
- const msg = text?.type === "text" ? (text.text ?? "") : "";
143
- const listText = buildTodoListText(todoList, options, theme);
144
- return new Text(
145
- theme.fg("success", "\u2713 ") + theme.fg("muted", msg) + "\n\n" + listText,
146
- 0,
147
- 0,
148
- );
149
- }
150
-
186
+ case "add":
151
187
  case "update":
152
188
  case "delete":
153
189
  case "clear": {
@@ -169,5 +205,4 @@ export function renderTodoResult(result: unknown, options: { expanded: boolean }
169
205
  }
170
206
  }
171
207
 
172
- // 重新导出 buildRender 供 tool.ts 使用(避免循环引用)
173
208
  export { buildRender };
package/src/state.ts CHANGED
@@ -12,11 +12,15 @@ import type { Todo } from "./model";
12
12
  export interface TodoSessionState {
13
13
  todos: Todo[];
14
14
  nextId: number;
15
- // v3: 用户消息轮数与提醒追踪
15
+ // 用户消息轮数与提醒追踪
16
16
  userMessageCount: number;
17
17
  lastTodoCallCount: number;
18
18
  stallNotified: boolean;
19
19
  allCompletedAtCount: number | null;
20
+ /** 全部 completed 时已注入 steer,防止重复 */
21
+ completionSteered: boolean;
22
+ /** agent_end 设置、before_agent_start 消费的延迟 steer 消息 */
23
+ pendingSteerMessage: string | null;
20
24
  }
21
25
 
22
26
  export function createTodoSessionState(): TodoSessionState {
@@ -27,5 +31,7 @@ export function createTodoSessionState(): TodoSessionState {
27
31
  lastTodoCallCount: 0,
28
32
  stallNotified: false,
29
33
  allCompletedAtCount: null,
34
+ completionSteered: false,
35
+ pendingSteerMessage: null,
30
36
  };
31
37
  }