@gavin-hu/my-pi-agent 0.11.4 → 0.13.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.
Files changed (197) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +33 -23
  3. package/extensions/bytefly/README.md +381 -225
  4. package/extensions/bytefly/chrome.ts +7 -2
  5. package/extensions/bytefly/doctor/register.ts +6 -0
  6. package/extensions/bytefly/doctor/runtime.ts +52 -6
  7. package/extensions/bytefly/doctor/tools.ts +1 -3
  8. package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
  9. package/extensions/bytefly/footer/config.ts +7 -1
  10. package/extensions/bytefly/footer/delegation.ts +100 -0
  11. package/extensions/bytefly/footer/price/config.ts +54 -15
  12. package/extensions/bytefly/footer/price/price.ts +20 -7
  13. package/extensions/bytefly/footer/price/types.ts +13 -7
  14. package/extensions/bytefly/footer/register.ts +13 -2
  15. package/extensions/bytefly/footer/render/format.ts +3 -2
  16. package/extensions/bytefly/footer/render/lines.ts +2 -0
  17. package/extensions/bytefly/footer/render/segments.ts +30 -2
  18. package/extensions/bytefly/footer/types.ts +12 -0
  19. package/extensions/bytefly/header/component.ts +21 -6
  20. package/extensions/bytefly/header/config.ts +3 -0
  21. package/extensions/bytefly/header/register.ts +34 -3
  22. package/extensions/bytefly/index.ts +15 -4
  23. package/extensions/bytefly/notify/config.ts +2 -0
  24. package/extensions/bytefly/notify/format.ts +19 -1
  25. package/extensions/bytefly/notify/register.ts +37 -13
  26. package/extensions/bytefly/notify/types.ts +2 -0
  27. package/extensions/bytefly/panel/commands.ts +34 -13
  28. package/extensions/bytefly/panel/settings-runtime.ts +71 -28
  29. package/extensions/bytefly/panel/settings.ts +293 -48
  30. package/extensions/bytefly/panel/tui/entries.ts +58 -0
  31. package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
  32. package/extensions/bytefly/panel/tui/frame.ts +71 -0
  33. package/extensions/bytefly/panel/tui/nav.ts +75 -0
  34. package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
  35. package/extensions/bytefly/panel/tui.ts +205 -319
  36. package/extensions/bytefly/picker/runtime.ts +61 -2
  37. package/extensions/bytefly/picker/toggle.ts +56 -0
  38. package/extensions/bytefly/update/cache.ts +73 -0
  39. package/extensions/bytefly/update/check.ts +59 -0
  40. package/extensions/bytefly/update/format.ts +87 -0
  41. package/extensions/bytefly/update/register.ts +158 -0
  42. package/extensions/bytefly/update/state.ts +40 -0
  43. package/extensions/bytefly/update/version.ts +83 -0
  44. package/extensions/document/README.md +24 -14
  45. package/extensions/document/config.ts +4 -0
  46. package/extensions/document/deadline.ts +61 -0
  47. package/extensions/document/tool.ts +14 -5
  48. package/extensions/file-browse/README.md +12 -8
  49. package/extensions/file-browse/client.ts +6 -7
  50. package/extensions/file-browse/files.ts +0 -4
  51. package/extensions/file-browse/git.ts +9 -10
  52. package/extensions/file-browse/html.ts +17 -7
  53. package/extensions/file-browse/router.ts +24 -15
  54. package/extensions/file-open/README.md +30 -15
  55. package/extensions/file-open/index.ts +4 -3
  56. package/extensions/file-open/open.ts +52 -21
  57. package/extensions/goal/README.md +45 -13
  58. package/extensions/goal/clarify-prompt.ts +5 -36
  59. package/extensions/goal/clarify.ts +55 -86
  60. package/extensions/goal/commands.ts +41 -35
  61. package/extensions/goal/format.ts +12 -4
  62. package/extensions/goal/schema.ts +6 -2
  63. package/extensions/goal/tools.ts +28 -8
  64. package/extensions/goal/tui.ts +1 -1
  65. package/extensions/goal/types.ts +6 -2
  66. package/extensions/job/README.md +16 -12
  67. package/extensions/job/runtime.ts +23 -6
  68. package/extensions/job/status.ts +4 -2
  69. package/extensions/memory/README.md +56 -14
  70. package/extensions/memory/commands.ts +20 -2
  71. package/extensions/memory/doctor.ts +137 -0
  72. package/extensions/memory/format.ts +24 -7
  73. package/extensions/memory/runtime.ts +49 -0
  74. package/extensions/memory/store.ts +31 -0
  75. package/extensions/memory/tui.ts +136 -12
  76. package/extensions/plan/README.md +83 -36
  77. package/extensions/plan/blocks.ts +1 -35
  78. package/extensions/plan/commands.ts +84 -19
  79. package/extensions/plan/index.ts +35 -11
  80. package/extensions/plan/plan-index.ts +234 -0
  81. package/extensions/plan/plan-text.ts +95 -0
  82. package/extensions/plan/plans.ts +91 -2
  83. package/extensions/plan/policy.ts +4 -3
  84. package/extensions/plan/review.ts +246 -0
  85. package/extensions/plan/runtime.ts +73 -24
  86. package/extensions/plan/steps.ts +60 -39
  87. package/extensions/plan/todos.ts +29 -60
  88. package/extensions/plan/tools.ts +188 -244
  89. package/extensions/plan/tui.ts +18 -31
  90. package/extensions/plan/types.ts +12 -0
  91. package/extensions/plan/web-client.ts +51 -18
  92. package/extensions/plan/web-page.ts +7 -2
  93. package/extensions/plan/web-router.ts +11 -3
  94. package/extensions/plan/web.ts +106 -41
  95. package/extensions/question/README.md +1 -1
  96. package/extensions/question/tools.ts +1 -1
  97. package/extensions/rewind/README.md +28 -8
  98. package/extensions/rewind/commands.ts +1 -1
  99. package/extensions/rewind/format.ts +4 -4
  100. package/extensions/rewind/git.ts +56 -18
  101. package/extensions/rewind/index.ts +3 -5
  102. package/extensions/rewind/path-picker.ts +198 -0
  103. package/extensions/rewind/paths.ts +128 -0
  104. package/extensions/rewind/restore.ts +69 -28
  105. package/extensions/rewind/rewind.ts +80 -3
  106. package/extensions/rewind/runtime.ts +13 -5
  107. package/extensions/rewind/snapshot-run.ts +5 -2
  108. package/extensions/rewind/types.ts +2 -0
  109. package/extensions/subagent/README.md +7 -1
  110. package/extensions/subagent/accounting.ts +34 -0
  111. package/extensions/subagent/index.ts +23 -3
  112. package/extensions/subagent/stream.ts +2 -0
  113. package/extensions/subagent/types.ts +2 -0
  114. package/extensions/todo/README.md +19 -11
  115. package/extensions/todo/index.ts +5 -3
  116. package/extensions/todo/nudge.ts +58 -9
  117. package/extensions/todo/runtime.ts +28 -17
  118. package/extensions/watch/README.md +8 -3
  119. package/extensions/watch/index.ts +19 -0
  120. package/extensions/web-access/README.md +46 -16
  121. package/extensions/web-access/charset.ts +58 -0
  122. package/extensions/web-access/fetch/find.ts +11 -7
  123. package/extensions/web-access/fetch/format.ts +64 -18
  124. package/extensions/web-access/fetch/page.ts +5 -1
  125. package/extensions/web-access/fetch/tool.ts +20 -21
  126. package/extensions/web-access/http.ts +49 -8
  127. package/extensions/web-access/search/facts.ts +38 -0
  128. package/extensions/web-access/search/tool.ts +14 -3
  129. package/extensions/web-access/text.ts +50 -11
  130. package/extensions/wechat/README.md +5 -4
  131. package/extensions/wechat/tools.ts +0 -2
  132. package/extensions/worktree/README.md +74 -15
  133. package/extensions/worktree/config.ts +11 -0
  134. package/extensions/worktree/exit.ts +35 -3
  135. package/extensions/worktree/gaps.ts +74 -0
  136. package/extensions/worktree/guard.ts +50 -4
  137. package/extensions/worktree/handoff.ts +55 -0
  138. package/extensions/worktree/index.ts +12 -2
  139. package/extensions/worktree/runtime.ts +20 -0
  140. package/extensions/worktree/status.ts +15 -2
  141. package/extensions/worktree/tools.ts +3 -2
  142. package/lib/README.md +12 -9
  143. package/lib/config.ts +43 -1
  144. package/lib/env.ts +12 -0
  145. package/lib/events.ts +86 -1
  146. package/lib/goal-state.ts +95 -0
  147. package/lib/requirements.ts +340 -46
  148. package/lib/todo-state.ts +62 -0
  149. package/lib/tui/README.md +91 -0
  150. package/lib/tui/glyphs.ts +2 -0
  151. package/lib/web/layout.ts +12 -2
  152. package/package.json +5 -3
  153. package/themes/bytefly-dark.json +1 -1
  154. package/assets/bytefly.schema.json +0 -785
  155. package/assets/chrome.png +0 -0
  156. package/assets/file-browse.png +0 -0
  157. package/assets/job.png +0 -0
  158. package/assets/plan-review.png +0 -0
  159. package/assets/question.png +0 -0
  160. package/assets/rewind.png +0 -0
  161. package/assets/todo.png +0 -0
  162. package/assets/tool-renderings.png +0 -0
  163. package/assets/tools/ask_user_question-cancelled.png +0 -0
  164. package/assets/tools/ask_user_question.png +0 -0
  165. package/assets/tools/bash.png +0 -0
  166. package/assets/tools/edit.png +0 -0
  167. package/assets/tools/enter_plan_mode.png +0 -0
  168. package/assets/tools/enter_worktree.png +0 -0
  169. package/assets/tools/exit_plan_mode.png +0 -0
  170. package/assets/tools/exit_worktree.png +0 -0
  171. package/assets/tools/find.png +0 -0
  172. package/assets/tools/goal.png +0 -0
  173. package/assets/tools/grep.png +0 -0
  174. package/assets/tools/job.png +0 -0
  175. package/assets/tools/list_worktrees.png +0 -0
  176. package/assets/tools/ls.png +0 -0
  177. package/assets/tools/memory-long-subject.png +0 -0
  178. package/assets/tools/memory.png +0 -0
  179. package/assets/tools/notice.png +0 -0
  180. package/assets/tools/open_path.png +0 -0
  181. package/assets/tools/prune_worktrees.png +0 -0
  182. package/assets/tools/read.png +0 -0
  183. package/assets/tools/read_doc-running.png +0 -0
  184. package/assets/tools/read_doc.png +0 -0
  185. package/assets/tools/run_doctor.png +0 -0
  186. package/assets/tools/send_wechat.png +0 -0
  187. package/assets/tools/set_plan_steps.png +0 -0
  188. package/assets/tools/subagent.png +0 -0
  189. package/assets/tools/todo.png +0 -0
  190. package/assets/tools/turn-rule.png +0 -0
  191. package/assets/tools/user-message.png +0 -0
  192. package/assets/tools/watch.png +0 -0
  193. package/assets/tools/web_fetch-rejected.png +0 -0
  194. package/assets/tools/web_fetch.png +0 -0
  195. package/assets/tools/web_search.png +0 -0
  196. package/assets/tools/write.png +0 -0
  197. package/assets/tools/write_plan.png +0 -0
@@ -5,7 +5,9 @@
5
5
  * guided to write, the top-level steps that seed `todo` and drive the review
6
6
  * gauge, the heading sections the review screen navigates, and the in-place
7
7
  * checkbox edit that keeps the file current after approval. All of it is pure so
8
- * the tools, the store, and the renderer agree on one parser.
8
+ * the tools, the store, and the renderer agree on one parser. The review's
9
+ * section comparison lives in `plan-text.ts`, which owns the text normalization
10
+ * the digest is computed over.
9
11
  *
10
12
  * Only top-level list items become steps: nested sub-bullets are notes, and the
11
13
  * shallowest list level is treated as the plan. When the plan has a `Steps`
@@ -28,7 +30,7 @@ export const PLAN_OUTLINE = [
28
30
  "# <Title>",
29
31
  "",
30
32
  "## Goal",
31
- "What this change achieves, in one or two sentences.",
33
+ "What this change achieves, and what it deliberately does not.",
32
34
  "",
33
35
  "## Context",
34
36
  "The relevant code, constraints, and prior art.",
@@ -37,8 +39,8 @@ export const PLAN_OUTLINE = [
37
39
  "The chosen approach and why; alternatives considered.",
38
40
  "",
39
41
  "## Steps",
40
- "- [ ] First concrete step",
41
- "- [ ] Next step",
42
+ "- [ ] Add `parseConfig` in `src/config.ts`, with a test for a missing key",
43
+ "- [ ] Wire it into `load()` and document the failure in the README",
42
44
  "",
43
45
  "## Risks",
44
46
  "What could go wrong and how it is mitigated.",
@@ -61,6 +63,10 @@ export interface PlanSection {
61
63
  title: string;
62
64
  /** The section's markdown, including its heading line. */
63
65
  text: string;
66
+ /** 0-based source line of the section's heading (0 for the preamble). */
67
+ line: number;
68
+ /** Heading level, or 0 for the preamble before the first heading. */
69
+ level: number;
64
70
  }
65
71
 
66
72
  /** A top-level list item located in the source. */
@@ -78,10 +84,19 @@ interface PlanListItem {
78
84
  /** A top-level numbered or bulleted list item, capturing its parts. */
79
85
  const LIST_ITEM = /^([ \t]*)((?:\d+[.)]|[-*+])\s+)(.*\S)\s*$/;
80
86
  const CHECKBOX = /^\[([ xX])\]\s*(.*)$/;
81
- /** A markdown heading outside a fence, capturing its level and text. */
82
- const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
83
- /** A markdown code-fence opener or closer (backticks or tildes). */
84
- const FENCE = /^\s{0,3}(`{3,}|~{3,})/;
87
+ /**
88
+ * A markdown heading outside a fence, capturing its level and text: up to three
89
+ * leading spaces, a space after the hashes, and a closing hash run only when a
90
+ * space separates it. Shared with `blocks.ts`, so the section split and the
91
+ * browser outline read a heading line the same way, and `## C#` keeps its title.
92
+ */
93
+ export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)(?:\s+#+)?\s*$/;
94
+ /**
95
+ * A markdown code-fence opener or closer (backticks or tildes). Shared with
96
+ * `blocks.ts`: a tab-indented fence is prose for both parsers, never code for
97
+ * one and prose for the other.
98
+ */
99
+ export const FENCE = /^ {0,3}(`{3,}|~{3,})/;
85
100
  const DONE_MARKER = /\[DONE:\d+\]/gi;
86
101
 
87
102
  /**
@@ -211,6 +226,38 @@ function elide(content: string): string {
211
226
  return content.length > MAX_STEP_LENGTH ? `${content.slice(0, MAX_STEP_LENGTH - 1).trimEnd()}…` : content;
212
227
  }
213
228
 
229
+ /** A list marker with an optional checkbox, at any indent. */
230
+ const LIST_PREFIX = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/;
231
+
232
+ /**
233
+ * The plan's `Verification` section as one trailing checklist item, or
234
+ * `undefined` when the plan has none.
235
+ *
236
+ * The section is prose ("commands and checks"), and a todo item is one line, so
237
+ * the lines are stripped of list markers and inline emphasis and joined with
238
+ * `; `, prefixed with `Verify:` and elided like a long step. The item is
239
+ * deliberately *not* a plan step: it is not in the `Steps` section, so it must
240
+ * not count against the step cap or appear in `set_plan_steps` numbering.
241
+ *
242
+ * A plan with no `Steps` heading feeds its shallowest list as steps (the older
243
+ * rule), so that list already covers the verification bullets; appending a
244
+ * summary of them would duplicate them, and there is nothing to carry.
245
+ */
246
+ export function verificationItem(plan: string): PlanStep | undefined {
247
+ if (stepsSection(plan) === undefined) return undefined;
248
+ const section = extractPlanSections(plan).find(
249
+ (candidate) => candidate.level > 0 && candidate.title.toLowerCase() === "verification",
250
+ );
251
+ if (!section) return undefined;
252
+ const items = section.text
253
+ .split("\n")
254
+ .slice(1)
255
+ .map((line) => plainText(line.replace(LIST_PREFIX, "")))
256
+ .filter(Boolean);
257
+ if (items.length === 0) return undefined;
258
+ return { content: elide(`Verify: ${items.join("; ")}`), status: "pending" };
259
+ }
260
+
214
261
  /** Top-level steps from a plan, in order. */
215
262
  export function extractPlanSteps(plan: string): PlanStep[] {
216
263
  return topLevelItems(plan).map((item) => ({ content: elide(item.step.content), status: item.step.status }));
@@ -281,32 +328,6 @@ export function setStepStatuses(plan: string, updates: StepUpdate[]): string | u
281
328
  return lines.join("\n");
282
329
  }
283
330
 
284
- /**
285
- * Titles (normalized) whose section text differs between two plan revisions, or
286
- * that are new in `current`. Revision snapshots are the caller's to provide;
287
- * strip stored feedback first so a review comment does not look like an edit.
288
- * Duplicate headings are compared by occurrence, and a changed title marks every
289
- * section with that title — plans following the canonical outline have none.
290
- */
291
- export function changedSections(previous: string, current: string): Set<string> {
292
- const before = new Map<string, string[]>();
293
- for (const section of extractPlanSections(previous)) {
294
- const texts = before.get(section.title);
295
- if (texts) texts.push(section.text.trim());
296
- else before.set(section.title, [section.text.trim()]);
297
- }
298
-
299
- const seen = new Map<string, number>();
300
- const changed = new Set<string>();
301
- for (const section of extractPlanSections(current)) {
302
- const index = seen.get(section.title) ?? 0;
303
- seen.set(section.title, index + 1);
304
- const prior = before.get(section.title)?.[index];
305
- if (prior === undefined || prior !== section.text.trim()) changed.add(section.title);
306
- }
307
- return changed;
308
- }
309
-
310
331
  /**
311
332
  * Sections of a plan, split at heading lines outside code fences. The content
312
333
  * before the first heading is a section titled `""`. Pi's `Markdown` renderer
@@ -323,7 +344,7 @@ export function extractPlanSections(plan: string): PlanSection[] {
323
344
  current = undefined;
324
345
  };
325
346
 
326
- for (const line of plan.split("\n")) {
347
+ plan.split("\n").forEach((line, index) => {
327
348
  const fenceMatch = line.match(FENCE);
328
349
  if (fenceMatch) {
329
350
  if (fence === undefined) fence = fenceMatch[1][0];
@@ -332,13 +353,13 @@ export function extractPlanSections(plan: string): PlanSection[] {
332
353
  const heading = line.match(HEADING);
333
354
  if (heading) {
334
355
  flush();
335
- current = { title: plainText(heading[2]), text: line };
336
- continue;
356
+ current = { title: plainText(heading[2]), text: line, line: index, level: heading[1].length };
357
+ return;
337
358
  }
338
359
  }
339
- if (current === undefined) current = { title: "", text: line };
360
+ if (current === undefined) current = { title: "", text: line, line: 0, level: 0 };
340
361
  else current.text += `\n${line}`;
341
- }
362
+ });
342
363
 
343
364
  flush();
344
365
  return sections;
@@ -3,25 +3,15 @@
3
3
  *
4
4
  * `todo` is whole-list replacement, so seeding an approved plan straight over
5
5
  * the list would discard whatever the user was already tracking. This module
6
- * reads the last `todo` result from the session branch — the published state
7
- * channel, replayed the way the todo extension itself reconstructs it, with no
8
- * import from that extension — and merges the plan's steps into it: existing
9
- * items keep their place and status, a step whose text is already present is
10
- * skipped, and the tool's item cap is respected without dropping anything the
11
- * user had.
6
+ * merges the plan's steps into the list `lib/todo-state.ts` replays from the
7
+ * session branch: existing items keep their place and status, a step whose text
8
+ * is already present is skipped, and the tool's item cap is respected without
9
+ * dropping anything the user had.
12
10
  */
13
11
 
14
- import { TODO_TOOL } from "../../lib/tool-names.ts";
12
+ import type { ExistingTodo } from "../../lib/todo-state.ts";
15
13
  import type { PlanStep } from "./steps.ts";
16
14
 
17
- /** One item in the existing todo list, as the todo tool stores it. */
18
- export interface ExistingTodo {
19
- content: string;
20
- status: "pending" | "in_progress" | "completed";
21
- /** Present-continuous label the todo tool requires for an in-progress item. */
22
- activeForm?: string;
23
- }
24
-
25
15
  /** The merged list plus the counts the approval message reports. */
26
16
  export interface TodoMerge {
27
17
  todos: ExistingTodo[];
@@ -33,54 +23,16 @@ export interface TodoMerge {
33
23
  kept: number;
34
24
  /** Plan steps left out because the list reached its cap. */
35
25
  deferred: number;
36
- }
37
-
38
- /** Minimal shape of a session entry, used for runtime narrowing. */
39
- interface BranchEntryLike {
40
- type?: string;
41
- message?: {
42
- role?: string;
43
- toolName?: string;
44
- details?: unknown;
45
- };
26
+ /**
27
+ * What happened to the trailing checklist item (the plan's verification
28
+ * summary): appended after the steps, already present, or left out by the cap.
29
+ */
30
+ trailing?: "added" | "skipped" | "deferred";
46
31
  }
47
32
 
48
33
  /** Fallback list cap when the registered todo tool does not advertise one. */
49
34
  export const DEFAULT_TODO_MAX = 50;
50
35
 
51
- function isStatus(value: unknown): value is ExistingTodo["status"] {
52
- return value === "pending" || value === "in_progress" || value === "completed";
53
- }
54
-
55
- /**
56
- * The last valid todo list written on the branch, or an empty list. A rejected
57
- * call carries the unchanged list and is not a state write, so it is skipped;
58
- * an entry that does not re-validate is ignored rather than wiping a good list.
59
- */
60
- export function lastTodos(entries: Iterable<unknown>): ExistingTodo[] {
61
- let todos: ExistingTodo[] = [];
62
- for (const raw of entries) {
63
- const entry = raw as BranchEntryLike;
64
- if (entry?.type !== "message") continue;
65
- const message = entry.message;
66
- if (message?.role !== "toolResult" || message.toolName !== TODO_TOOL) continue;
67
- const details = message.details as { todos?: unknown; error?: unknown } | undefined;
68
- if (!details || details.error) continue;
69
- if (!Array.isArray(details.todos)) continue;
70
- todos = details.todos.flatMap((item): ExistingTodo[] => {
71
- if (typeof item !== "object" || item === null) return [];
72
- const value = item as { content?: unknown; status?: unknown; activeForm?: unknown };
73
- if (typeof value.content !== "string" || !isStatus(value.status)) return [];
74
- const content = value.content.trim();
75
- if (!content) return [];
76
- const activeForm =
77
- typeof value.activeForm === "string" && value.activeForm.trim() ? value.activeForm.trim() : undefined;
78
- return [activeForm ? { content, status: value.status, activeForm } : { content, status: value.status }];
79
- });
80
- }
81
- return todos;
82
- }
83
-
84
36
  /** Case- and whitespace-insensitive key for recognizing a repeated item. */
85
37
  function contentKey(content: string): string {
86
38
  return content.trim().replace(/\s+/g, " ").toLowerCase();
@@ -91,8 +43,12 @@ function contentKey(content: string): string {
91
43
  * keep their status, a legacy in-progress item without an `activeForm` gets its
92
44
  * content as one so the todo tool accepts it, and the appended steps stop at
93
45
  * `max` items — the rest are counted as deferred, never dropped.
46
+ *
47
+ * `trailing` is the plan's verification summary: it is appended after every step
48
+ * (still subject to the same duplicate and cap rules) but is not counted as a
49
+ * step, because it is not one.
94
50
  */
95
- export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: number): TodoMerge {
51
+ export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: number, trailing?: PlanStep): TodoMerge {
96
52
  const limit = Number.isInteger(max) && max > 0 ? max : DEFAULT_TODO_MAX;
97
53
  const todos: ExistingTodo[] = existing.map((todo) =>
98
54
  todo.status === "in_progress" && !todo.activeForm ? { ...todo, activeForm: todo.content } : todo,
@@ -116,5 +72,18 @@ export function mergeTodos(existing: ExistingTodo[], steps: PlanStep[], max: num
116
72
  todos.push({ content: step.content, status: step.status });
117
73
  added += 1;
118
74
  }
119
- return { todos, added, skipped, kept: existing.length, deferred };
75
+
76
+ let trailingOutcome: TodoMerge["trailing"];
77
+ if (trailing) {
78
+ const key = contentKey(trailing.content);
79
+ if (!key) trailingOutcome = undefined;
80
+ else if (seen.has(key)) trailingOutcome = "skipped";
81
+ else if (todos.length >= limit) trailingOutcome = "deferred";
82
+ else {
83
+ todos.push({ content: trailing.content, status: trailing.status });
84
+ trailingOutcome = "added";
85
+ }
86
+ }
87
+
88
+ return { todos, added, skipped, kept: existing.length, deferred, trailing: trailingOutcome };
120
89
  }