@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 (200) hide show
  1. package/README.md +29 -15
  2. package/README.zh-CN.md +27 -15
  3. package/extensions/bytefly/README.md +195 -115
  4. package/extensions/bytefly/footer/collect/entries.ts +1 -6
  5. package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
  6. package/extensions/bytefly/footer/price/price.ts +12 -8
  7. package/extensions/bytefly/footer/register.ts +13 -1
  8. package/extensions/bytefly/footer/types.ts +11 -0
  9. package/extensions/bytefly/footer/unpriced.ts +36 -0
  10. package/extensions/bytefly/header/component.ts +15 -29
  11. package/extensions/bytefly/header/identity.ts +71 -0
  12. package/extensions/bytefly/header/register.ts +9 -3
  13. package/extensions/bytefly/index.ts +13 -5
  14. package/extensions/bytefly/message/format.ts +20 -13
  15. package/extensions/bytefly/message/register.ts +8 -7
  16. package/extensions/bytefly/notify/types.ts +1 -1
  17. package/extensions/bytefly/panel/commands.ts +46 -25
  18. package/extensions/bytefly/panel/format.ts +49 -14
  19. package/extensions/bytefly/panel/join.ts +35 -9
  20. package/extensions/bytefly/panel/settings.ts +29 -39
  21. package/extensions/bytefly/panel/skills.ts +51 -0
  22. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
  23. package/extensions/bytefly/panel/tui/frame.ts +9 -1
  24. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  25. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  26. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  27. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  28. package/extensions/bytefly/panel/tui.ts +223 -331
  29. package/extensions/bytefly/panel/types.ts +26 -8
  30. package/extensions/bytefly/picker/discovery.ts +32 -27
  31. package/extensions/bytefly/picker/resources.ts +16 -8
  32. package/extensions/bytefly/picker/runtime.ts +62 -34
  33. package/extensions/bytefly/picker/toggle.ts +16 -10
  34. package/extensions/bytefly/picker/types.ts +31 -13
  35. package/extensions/bytefly/update/format.ts +66 -8
  36. package/extensions/bytefly/update/patch.ts +66 -0
  37. package/extensions/bytefly/update/register.ts +4 -11
  38. package/extensions/bytefly/update/release.ts +65 -0
  39. package/extensions/bytefly/update/state.ts +14 -0
  40. package/extensions/bytefly/update/suppress.ts +47 -29
  41. package/extensions/bytefly/update/types.ts +15 -5
  42. package/extensions/check/tui.ts +2 -0
  43. package/extensions/context/README.md +21 -11
  44. package/extensions/context/format.ts +10 -8
  45. package/extensions/context/report.ts +21 -8
  46. package/extensions/context/tui.ts +26 -10
  47. package/extensions/context/types.ts +5 -3
  48. package/extensions/file-browse/README.md +89 -22
  49. package/extensions/file-browse/chrome.ts +83 -0
  50. package/extensions/file-browse/client.ts +74 -459
  51. package/extensions/file-browse/config.ts +12 -3
  52. package/extensions/file-browse/diff-html.ts +94 -0
  53. package/extensions/file-browse/html.ts +26 -152
  54. package/extensions/file-browse/live-client.ts +47 -1
  55. package/extensions/file-browse/live.ts +35 -14
  56. package/extensions/file-browse/palette.ts +224 -0
  57. package/extensions/file-browse/rev.ts +42 -0
  58. package/extensions/file-browse/router.ts +27 -6
  59. package/extensions/file-browse/server.ts +7 -0
  60. package/extensions/file-browse/styles.ts +10 -3
  61. package/extensions/file-browse/tree.ts +381 -0
  62. package/extensions/file-read/README.md +4 -1
  63. package/extensions/file-read/extract/odt.ts +7 -5
  64. package/extensions/goal/plan-route.ts +8 -2
  65. package/extensions/goal/tools.ts +6 -11
  66. package/extensions/job/README.md +7 -5
  67. package/extensions/job/index.ts +35 -52
  68. package/extensions/job/tools.ts +51 -77
  69. package/extensions/memory/README.md +3 -3
  70. package/extensions/memory/tools.ts +4 -6
  71. package/extensions/memory/tui.ts +5 -1
  72. package/extensions/plan/README.md +333 -84
  73. package/extensions/plan/commands.ts +73 -7
  74. package/extensions/plan/feedback.ts +300 -63
  75. package/extensions/plan/index.ts +1 -1
  76. package/extensions/plan/plan-index.ts +4 -2
  77. package/extensions/plan/plan-text.ts +18 -1
  78. package/extensions/plan/plans.ts +184 -58
  79. package/extensions/plan/review-prompt.ts +93 -0
  80. package/extensions/plan/review.ts +95 -95
  81. package/extensions/plan/runtime.ts +4 -2
  82. package/extensions/plan/split.ts +87 -0
  83. package/extensions/plan/steps.ts +123 -107
  84. package/extensions/plan/tools/cancel.ts +79 -0
  85. package/extensions/plan/tools/enter.ts +17 -12
  86. package/extensions/plan/tools/exit.ts +148 -59
  87. package/extensions/plan/tools/steps.ts +7 -19
  88. package/extensions/plan/tools/write.ts +29 -31
  89. package/extensions/plan/tools.ts +6 -3
  90. package/extensions/plan/tui.ts +60 -15
  91. package/extensions/plan/types.ts +45 -3
  92. package/extensions/plan/web-chrome.ts +102 -0
  93. package/extensions/plan/web-client.ts +177 -446
  94. package/extensions/plan/web-collab.ts +207 -0
  95. package/extensions/plan/web-comments.ts +269 -0
  96. package/extensions/plan/web-markdown.ts +222 -0
  97. package/extensions/plan/web-page.ts +131 -19
  98. package/extensions/plan/web-router.ts +148 -32
  99. package/extensions/plan/web-share-session.ts +197 -0
  100. package/extensions/plan/web-share.ts +169 -0
  101. package/extensions/plan/web-streams.ts +124 -0
  102. package/extensions/plan/web-tokens.ts +63 -0
  103. package/extensions/plan/web.ts +193 -45
  104. package/extensions/question/README.md +3 -0
  105. package/extensions/question/dialogs.ts +91 -1
  106. package/extensions/question/tools.ts +2 -1
  107. package/extensions/rewind/checklist.ts +4 -0
  108. package/extensions/rewind/review-tui.ts +3 -1
  109. package/extensions/rewind/rewind-tui.ts +4 -0
  110. package/extensions/session-bus/README.md +247 -0
  111. package/extensions/session-bus/commands.ts +165 -0
  112. package/extensions/session-bus/format.ts +124 -0
  113. package/extensions/session-bus/index.ts +69 -0
  114. package/extensions/session-bus/notice.ts +58 -0
  115. package/extensions/session-bus/protocol.ts +134 -0
  116. package/extensions/session-bus/result.ts +243 -0
  117. package/extensions/session-bus/rows.ts +169 -0
  118. package/extensions/session-bus/runtime.ts +575 -0
  119. package/extensions/session-bus/store.ts +329 -0
  120. package/extensions/session-bus/team.ts +102 -0
  121. package/extensions/session-bus/tools.ts +227 -0
  122. package/extensions/session-bus/tui.ts +394 -0
  123. package/extensions/session-bus/types.ts +98 -0
  124. package/extensions/subagent/README.md +26 -13
  125. package/extensions/subagent/format.ts +72 -25
  126. package/extensions/subagent/index.ts +21 -47
  127. package/extensions/subagent/orchestrate.ts +18 -41
  128. package/extensions/subagent/render.ts +29 -42
  129. package/extensions/subagent/stream.ts +24 -18
  130. package/extensions/subagent/types.ts +7 -0
  131. package/extensions/todo/tools.ts +5 -6
  132. package/extensions/todo/tui.ts +5 -1
  133. package/extensions/watch/README.md +9 -5
  134. package/extensions/watch/index.ts +25 -44
  135. package/extensions/watch/tools.ts +31 -59
  136. package/extensions/wechat/README.md +45 -36
  137. package/extensions/wechat/bridge.ts +46 -48
  138. package/extensions/wechat/client.ts +1 -4
  139. package/extensions/wechat/commands.ts +3 -3
  140. package/extensions/wechat/deliver.ts +57 -37
  141. package/extensions/wechat/interaction.ts +54 -18
  142. package/extensions/wechat/login-screen.ts +2 -3
  143. package/extensions/wechat/notices.ts +9 -7
  144. package/extensions/wechat/peer-worker.ts +19 -34
  145. package/extensions/wechat/presence.ts +3 -4
  146. package/extensions/wechat/prompt.ts +53 -23
  147. package/extensions/wechat/reply-pipeline.ts +12 -4
  148. package/extensions/wechat/slash.ts +14 -0
  149. package/extensions/wechat/status-screen.ts +2 -2
  150. package/extensions/wechat/tools.ts +3 -11
  151. package/extensions/worktree/README.md +48 -12
  152. package/extensions/worktree/commands.ts +109 -65
  153. package/extensions/worktree/enter.ts +1 -1
  154. package/extensions/worktree/exit.ts +11 -1
  155. package/extensions/worktree/git.ts +5 -0
  156. package/extensions/worktree/lifecycle.ts +9 -1
  157. package/extensions/worktree/registry.ts +11 -5
  158. package/extensions/worktree/screen.ts +263 -0
  159. package/extensions/worktree/status.ts +84 -40
  160. package/lib/README.md +12 -7
  161. package/lib/claims/overlap.ts +43 -0
  162. package/lib/config.ts +12 -0
  163. package/lib/format.ts +6 -2
  164. package/lib/http/router.ts +40 -13
  165. package/lib/http/server.ts +27 -13
  166. package/lib/interaction.ts +28 -1
  167. package/lib/notice.ts +29 -2
  168. package/lib/plan-entry.ts +7 -2
  169. package/lib/plans/sections.ts +172 -0
  170. package/lib/plans/setting.ts +52 -0
  171. package/lib/plans/split.ts +215 -0
  172. package/lib/plans/store.ts +19 -0
  173. package/lib/plans/team.ts +151 -0
  174. package/lib/qr.ts +64 -0
  175. package/lib/repo-key.ts +74 -0
  176. package/lib/report.ts +117 -25
  177. package/lib/requirements.ts +1 -2
  178. package/lib/tool-result.ts +54 -4
  179. package/lib/tui/README.md +30 -8
  180. package/lib/tui/chrome.ts +27 -6
  181. package/lib/tui/dock-screen.ts +91 -19
  182. package/lib/tui/gauge.ts +9 -5
  183. package/lib/tui/line-pane.ts +15 -4
  184. package/lib/tui/tool.ts +23 -4
  185. package/lib/usage.ts +26 -0
  186. package/lib/web/README.md +131 -0
  187. package/lib/web/base.ts +19 -2
  188. package/lib/web/components.ts +10 -6
  189. package/lib/web/layout.ts +2 -0
  190. package/lib/web/markup.ts +2 -1
  191. package/lib/web/runtime.ts +103 -25
  192. package/lib/web/tokens.ts +23 -7
  193. package/package.json +5 -2
  194. package/skills/changelog/SKILL.md +57 -45
  195. package/skills/init/SKILL.md +80 -45
  196. package/skills/init/references/agents-md-quality.md +26 -2
  197. package/skills/init/references/interview-questions.md +36 -14
  198. package/skills/next/SKILL.md +209 -0
  199. package/skills/parallel-dev/SKILL.md +150 -0
  200. package/extensions/wechat/qrcode.ts +0 -46
@@ -10,73 +10,111 @@
10
10
  * the digest is computed over.
11
11
  *
12
12
  * Only top-level list items become steps: nested sub-bullets are notes, and the
13
- * shallowest list level is treated as the plan. When the plan has a `Steps`
14
- * section (the canonical outline's heading), only that section's list items are
15
- * steps, so prose bullets elsewhere never seed the todo list or consume the
16
- * step cap. List items inside fenced code blocks are ignored. A `- [ ]` / `- [x]`
13
+ * shallowest list level is treated as the plan. An indented, marker-less line
14
+ * continues the item above it, so a wrapped step keeps its full text. When the
15
+ * plan has a `Steps` section (the canonical outline's heading), only that
16
+ * section's list items are steps, so prose bullets elsewhere never seed the todo
17
+ * list or consume the step cap. List items inside fenced code blocks are ignored. A `- [ ]` / `- [x]`
17
18
  * checkbox or a `[DONE:n]` marker sets the status.
18
19
  */
19
20
 
21
+ import {
22
+ extractSections as extractPlanSections,
23
+ FENCE,
24
+ HEADING,
25
+ normalizeHeadingText,
26
+ } from "../../lib/plans/sections.ts";
27
+
28
+ /**
29
+ * The section machinery is `lib/plans/sections.ts`, because the split writes
30
+ * `## Team` with the same fence-aware scan this module navigates with. These
31
+ * re-exports keep the plan extension's public surface one import.
32
+ */
33
+ export {
34
+ extractSections as extractPlanSections,
35
+ FENCE,
36
+ HEADING,
37
+ normalizeHeadingText,
38
+ replaceSection,
39
+ } from "../../lib/plans/sections.ts";
40
+ export type { PlanSection } from "../../lib/plans/sections.ts";
41
+
20
42
  /** Most steps seeded from one plan. */
21
43
  export const MAX_STEPS = 50;
22
44
  /** Longest seeded step text; longer lines are elided. */
23
45
  export const MAX_STEP_LENGTH = 200;
24
46
 
25
47
  /**
26
- * The outline a plan is guided to follow. Guidance only: `write_plan` never
27
- * rejects or rewrites a plan that omits a section.
48
+ * The outline a plan is guided to follow: one job per section, and every example
49
+ * is the shape wanted rather than a description of it. `Visual` is the one
50
+ * section that carries the end state — the same surface before and after when the
51
+ * change alters one that already exists, one frame when it is new — in a fence
52
+ * that stays untagged unless every line is code, so the reviewer sees the result
53
+ * before the steps that build it. Guidance only: `write_plan` never rejects or
54
+ * rewrites a plan that omits a section, and a change with no visible surface drops
55
+ * `Visual`.
28
56
  */
29
57
  export const PLAN_OUTLINE = [
30
58
  "# <Title>",
31
59
  "",
32
60
  "## Goal",
33
- "What this change achieves, and what it deliberately does not.",
61
+ "One sentence: the outcome this change delivers, and what it deliberately does not do.",
34
62
  "",
35
63
  "## Context",
36
- "The relevant code, constraints, and prior art.",
64
+ "- `src/load.ts:42` reads the config once at startup; that is the only caller.",
65
+ "- The file is user-editable, so a bad key must name the file and line.",
37
66
  "",
38
67
  "## Approach",
39
- "The chosen approach and why; alternatives considered.",
68
+ "- Parse into a typed record in `parseConfig`; `load()` keeps the file read.",
69
+ "- Rejected: validating inside `load()`, because the test would need a filesystem.",
70
+ "",
71
+ "## Visual",
72
+ "A bad key at startup, before and after:",
73
+ "```",
74
+ "$ bun start",
75
+ 'config.json:12: ignoring unknown key "timeout_ms"',
76
+ "```",
77
+ "",
78
+ "```",
79
+ "$ bun start",
80
+ 'config.json:12: unknown key "timeout_ms" — did you mean "timeoutMs"?',
81
+ "```",
40
82
  "",
41
83
  "## Steps",
42
84
  "- [ ] 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",
85
+ " - `load()` is the only caller today; it keeps the file read.",
86
+ "- [ ] Report a bad key with its file and line, and note the rule in the README",
44
87
  "",
45
88
  "## Risks",
46
- "What could go wrong and how it is mitigated.",
89
+ "- A missing key now fails at startup rather than on first use: name that in the README.",
90
+ "- Mitigated by the test in the first step.",
47
91
  "",
48
92
  "## Verification",
49
- "How to prove it works: commands and checks.",
93
+ "```bash",
94
+ "bun test src/config.test.ts",
95
+ "bun run check",
96
+ "```",
50
97
  ].join("\n");
51
98
 
52
99
  /** The outline's section titles, in order, for documentation and tests. */
53
- export const PLAN_SECTIONS = ["Goal", "Context", "Approach", "Steps", "Risks", "Verification"] as const;
100
+ export const PLAN_SECTIONS = ["Goal", "Context", "Approach", "Visual", "Steps", "Risks", "Verification"] as const;
54
101
 
55
102
  export interface PlanStep {
56
103
  content: string;
57
104
  status: "pending" | "completed";
58
105
  }
59
106
 
60
- /** A heading-delimited chunk of the plan, for the review screen's navigation. */
61
- export interface PlanSection {
62
- /** Plain heading text, or `""` for the content before the first heading. */
63
- title: string;
64
- /** The section's markdown, including its heading line. */
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;
70
- }
71
-
72
107
  /** A top-level list item located in the source. */
73
108
  interface PlanListItem {
74
- /** Zero-based source line index. */
109
+ /** Zero-based source line index of the item's first line. */
75
110
  line: number;
76
111
  indent: number;
77
112
  /** The list marker with its trailing whitespace, e.g. `1. ` or `- `. */
78
113
  lead: string;
79
- /** Item text after the marker, right-trimmed (an optional checkbox included). */
114
+ /**
115
+ * Item text after the marker on its first line; indented continuation lines
116
+ * are folded into {@link step}, so a checkbox edit rewrites only this line.
117
+ */
80
118
  raw: string;
81
119
  step: PlanStep;
82
120
  }
@@ -84,39 +122,10 @@ interface PlanListItem {
84
122
  /** A top-level numbered or bulleted list item, capturing its parts. */
85
123
  const LIST_ITEM = /^([ \t]*)((?:\d+[.)]|[-*+])\s+)(.*\S)\s*$/;
86
124
  const CHECKBOX = /^\[([ xX])\]\s*(.*)$/;
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,})/;
100
125
  const DONE_MARKER = /\[DONE:\d+\]/gi;
101
126
 
102
- /**
103
- * A heading or step label as the review surfaces compare it: inline emphasis and
104
- * code removed, whitespace collapsed. Exported so `blocks.ts` labels an outline
105
- * entry the same way `extractPlanSections` labels its section, which is what
106
- * lets the browser mark a changed section.
107
- */
108
- export function normalizeHeadingText(text: string): string {
109
- return text
110
- .replace(/\*\*([^*]+)\*\*/g, "$1")
111
- .replace(/\*([^*]+)\*/g, "$1")
112
- .replace(/(?<![A-Za-z0-9_])_([^_]+)_(?![A-Za-z0-9_])/g, "$1")
113
- .replace(/`([^`]+)`/g, "$1")
114
- .replace(/\s+/g, " ")
115
- .trim();
116
- }
117
-
118
- /** Strip inline markdown emphasis and code, then collapse to one clean line. */
119
- function plainText(text: string): string {
127
+ /** A label with its inline markers and any done marker removed. */
128
+ function cleanLabel(text: string): string {
120
129
  return normalizeHeadingText(text.replace(DONE_MARKER, ""));
121
130
  }
122
131
 
@@ -132,7 +141,12 @@ function parseStep(raw: string): PlanStep {
132
141
  }
133
142
  if (/\[DONE:\d+\]/i.test(text)) done = true;
134
143
 
135
- return { content: plainText(text), status: done ? "completed" : "pending" };
144
+ return { content: cleanLabel(text), status: done ? "completed" : "pending" };
145
+ }
146
+
147
+ /** Leading whitespace width of a line, for continuation indent checks. */
148
+ function indentOf(line: string): number {
149
+ return line.length - line.trimStart().length;
136
150
  }
137
151
 
138
152
  /** Every top-level list item, in order, before de-duplication and capping. */
@@ -140,22 +154,57 @@ function scanListItems(plan: string): PlanListItem[] {
140
154
  const items: PlanListItem[] = [];
141
155
  // Fences hide example lists (`- name: foo` in a YAML block) from extraction.
142
156
  let fence: string | undefined;
157
+ // The item still collecting indented continuation lines, and those lines.
158
+ let current: { item: PlanListItem; continuation: string[] } | undefined;
159
+
160
+ /** Fold the collected continuation into the item, then close it. */
161
+ const finish = (): void => {
162
+ if (current === undefined) return;
163
+ if (current.continuation.length > 0) {
164
+ current.item.step = parseStep(`${current.item.raw} ${current.continuation.join(" ")}`);
165
+ }
166
+ current = undefined;
167
+ };
143
168
 
144
169
  plan.split("\n").forEach((line, index) => {
145
170
  const fenceMatch = line.match(FENCE);
146
171
  if (fenceMatch) {
172
+ finish();
147
173
  if (fence === undefined) fence = fenceMatch[1][0];
148
174
  else if (fence === fenceMatch[1][0]) fence = undefined;
149
175
  return;
150
176
  }
151
177
  if (fence !== undefined) return;
152
178
 
179
+ if (line.trim() === "") {
180
+ finish();
181
+ return;
182
+ }
183
+
153
184
  const match = line.match(LIST_ITEM);
154
- if (!match) return;
155
- const step = parseStep(match[3]);
156
- if (!step.content) return;
157
- items.push({ line: index, indent: match[1].length, lead: match[2], raw: match[3], step });
185
+ if (match) {
186
+ finish();
187
+ const step = parseStep(match[3]);
188
+ if (!step.content) return;
189
+ const item: PlanListItem = { line: index, indent: match[1].length, lead: match[2], raw: match[3], step };
190
+ items.push(item);
191
+ current = { item, continuation: [] };
192
+ return;
193
+ }
194
+
195
+ // A deeper, marker-less line continues the step's paragraph; a heading,
196
+ // a shallow line, or a blank line ends it. `raw` stays the first line so
197
+ // `setStepStatuses` edits the checkbox without touching these lines.
198
+ if (current !== undefined && indentOf(line) > current.item.indent && !HEADING.test(line)) {
199
+ const text = line.trim();
200
+ if (text) current.continuation.push(text);
201
+ return;
202
+ }
203
+
204
+ finish();
158
205
  });
206
+
207
+ finish();
159
208
  return items;
160
209
  }
161
210
 
@@ -183,7 +232,7 @@ function stepsSection(plan: string): { start: number; end: number } | undefined
183
232
  if (!heading) continue;
184
233
  const depth = heading[1].length;
185
234
  if (start < 0) {
186
- if (plainText(heading[2]).toLowerCase() !== "steps") continue;
235
+ if (normalizeHeadingText(heading[2]).toLowerCase() !== "steps") continue;
187
236
  start = index;
188
237
  level = depth;
189
238
  continue;
@@ -233,11 +282,12 @@ const LIST_PREFIX = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/;
233
282
  * The plan's `Verification` section as one trailing checklist item, or
234
283
  * `undefined` when the plan has none.
235
284
  *
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.
285
+ * The section is prose ("commands and checks") or a fenced block of them, and a
286
+ * todo item is one line, so the fence lines are dropped, the rest is stripped of
287
+ * list markers and inline emphasis, and the lines are joined with `; `, prefixed
288
+ * with `Verify:` and elided like a long step. The item is deliberately *not* a
289
+ * plan step: it is not in the `Steps` section, so it must not count against the
290
+ * step cap or appear in `set_plan_steps` numbering.
241
291
  *
242
292
  * A plan with no `Steps` heading feeds its shallowest list as steps (the older
243
293
  * rule), so that list already covers the verification bullets; appending a
@@ -252,7 +302,10 @@ export function verificationItem(plan: string): PlanStep | undefined {
252
302
  const items = section.text
253
303
  .split("\n")
254
304
  .slice(1)
255
- .map((line) => plainText(line.replace(LIST_PREFIX, "")))
305
+ // A fenced block is how the outline asks for commands; its fence lines are
306
+ // punctuation, not a check, so only the commands inside it are carried.
307
+ .filter((line) => !FENCE.test(line))
308
+ .map((line) => cleanLabel(line.replace(LIST_PREFIX, "")))
256
309
  .filter(Boolean);
257
310
  if (items.length === 0) return undefined;
258
311
  return { content: elide(`Verify: ${items.join("; ")}`), status: "pending" };
@@ -332,43 +385,6 @@ export function setStepStatuses(plan: string, updates: readonly StepEdit[]): str
332
385
  return lines.join("\n");
333
386
  }
334
387
 
335
- /**
336
- * Sections of a plan, split at heading lines outside code fences. The content
337
- * before the first heading is a section titled `""`. Pi's `Markdown` renderer
338
- * exposes no source-line map, so the review screen renders these chunks
339
- * separately to know where each section begins.
340
- */
341
- export function extractPlanSections(plan: string): PlanSection[] {
342
- const sections: PlanSection[] = [];
343
- let current: PlanSection | undefined;
344
- let fence: string | undefined;
345
-
346
- const flush = (): void => {
347
- if (current && (current.title !== "" || current.text.trim() !== "")) sections.push(current);
348
- current = undefined;
349
- };
350
-
351
- plan.split("\n").forEach((line, index) => {
352
- const fenceMatch = line.match(FENCE);
353
- if (fenceMatch) {
354
- if (fence === undefined) fence = fenceMatch[1][0];
355
- else if (fence === fenceMatch[1][0]) fence = undefined;
356
- } else if (fence === undefined) {
357
- const heading = line.match(HEADING);
358
- if (heading) {
359
- flush();
360
- current = { title: plainText(heading[2]), text: line, line: index, level: heading[1].length };
361
- return;
362
- }
363
- }
364
- if (current === undefined) current = { title: "", text: line, line: 0, level: 0 };
365
- else current.text += `\n${line}`;
366
- });
367
-
368
- flush();
369
- return sections;
370
- }
371
-
372
388
  /**
373
389
  * The plan's own objective: the first paragraph of its `## Goal` section, with
374
390
  * inline markdown and a leading list marker removed.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The `cancel_plan_mode` tool registration.
3
+ *
4
+ * This is the escape hatch that does not need a plan file or a review dialog:
5
+ * when the user asks to leave planning, the model can restore write access
6
+ * without approving anything. The plan store is untouched; a later `/plan` can
7
+ * still review the saved artifact.
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import { Type } from "typebox";
12
+ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
13
+ import { paint } from "../../../lib/tui/tokens.ts";
14
+ import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
15
+ import { failedResult } from "../../../lib/tool-result.ts";
16
+ import { CANCEL_TOOL, type PlanRuntime } from "../runtime.ts";
17
+ import type { CancelPlanModeDetails } from "../types.ts";
18
+
19
+ const CANCELLED_TEXT =
20
+ "Plan mode is off and write access is restored. The plan was not approved; no todo items or goal were recorded. " +
21
+ "Any saved plan file remains for a later review.";
22
+
23
+ export function registerCancelTool(pi: ExtensionAPI, runtime: PlanRuntime): void {
24
+ pi.registerTool({
25
+ name: CANCEL_TOOL,
26
+ label: "Cancel plan mode",
27
+ description:
28
+ "Leave plan mode without approving a plan. Restores write access; no todo items or goal are recorded. " +
29
+ "Use when the user asks to exit, cancel, or abandon planning.",
30
+ promptSnippet: "Leave plan mode without approving the plan.",
31
+ promptGuidelines: [
32
+ "Call cancel_plan_mode when the user asks to leave or abandon planning without approving a plan.",
33
+ ],
34
+ parameters: Type.Object({}),
35
+ defaultActive: false,
36
+ executionMode: "sequential",
37
+ // The mode change touches no files; like enter_plan_mode, it needs no
38
+ // destructive annotation.
39
+ annotations: { readOnlyHint: true, openWorldHint: false },
40
+
41
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
42
+ if (!runtime.isEnabled()) {
43
+ return failedResult("Not in plan mode; there is nothing to cancel.", {
44
+ cancelled: false,
45
+ } satisfies CancelPlanModeDetails);
46
+ }
47
+ runtime.disable(ctx);
48
+ ctx.ui.notify("Plan mode off — full access restored. The plan was not approved.", "info");
49
+ return {
50
+ content: [{ type: "text", text: CANCELLED_TEXT }],
51
+ details: { cancelled: true } satisfies CancelPlanModeDetails,
52
+ };
53
+ },
54
+
55
+ renderCall(_args, theme, context) {
56
+ return callLine(theme, context?.lastComponent, CANCEL_TOOL, "left plan mode");
57
+ },
58
+
59
+ renderResult(result, options, theme, context) {
60
+ const text = reuseText(context?.lastComponent);
61
+ const outcome = detailsOrLine(
62
+ theme,
63
+ result,
64
+ options.expanded,
65
+ toolDetails<CancelPlanModeDetails>(result.details),
66
+ );
67
+ if ("line" in outcome) {
68
+ text.setText(outcome.line);
69
+ return text;
70
+ }
71
+ text.setText(
72
+ outcome.details.cancelled
73
+ ? paint(theme, "warn", "Plan mode off — plan not approved")
74
+ : paint(theme, "meta", "Not in plan mode"),
75
+ );
76
+ return text;
77
+ },
78
+ });
79
+ }
@@ -11,7 +11,9 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
11
11
  import { GLYPHS } from "../../../lib/tui/glyphs.ts";
12
12
  import { paint } from "../../../lib/tui/tokens.ts";
13
13
  import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
14
- import { PLAN_CONSENT, READ_ONLY_SUMMARY } from "../../../lib/plan-entry.ts";
14
+ import { failedResult } from "../../../lib/tool-result.ts";
15
+ import { isRemoteTurn, remoteLanguage } from "../../../lib/interaction.ts";
16
+ import { PLAN_CONSENT, PLAN_CONSENT_SHORT, PLAN_CONSENT_SHORT_ZH, READ_ONLY_SUMMARY } from "../../../lib/plan-entry.ts";
15
17
  import { ENTER_TOOL, type PlanRuntime } from "../runtime.ts";
16
18
  import type { EnterPlanModeDetails } from "../types.ts";
17
19
 
@@ -45,19 +47,18 @@ export function registerEnterTool(pi: ExtensionAPI, runtime: PlanRuntime): void
45
47
  };
46
48
  }
47
49
  if (!ctx.hasUI) {
48
- return {
49
- content: [
50
- {
51
- type: "text",
52
- text: "No interactive UI is available to confirm entering plan mode. Ask the user whether they want a plan.",
53
- },
54
- ],
55
- details: { entered: false, unavailable: true } satisfies EnterPlanModeDetails,
56
- isError: true,
57
- };
50
+ return failedResult(
51
+ "No interactive UI is available to confirm entering plan mode. Ask the user whether they want a plan.",
52
+ { entered: false, unavailable: true } satisfies EnterPlanModeDetails,
53
+ );
58
54
  }
59
55
 
60
- const approved = await ctx.ui.confirm("Enter plan mode?", PLAN_CONSENT);
56
+ const consent = isRemoteTurn(ctx)
57
+ ? remoteLanguage(ctx) === "zh"
58
+ ? PLAN_CONSENT_SHORT_ZH
59
+ : PLAN_CONSENT_SHORT
60
+ : PLAN_CONSENT;
61
+ const approved = await ctx.ui.confirm("Enter plan mode?", consent);
61
62
  if (!approved) {
62
63
  return {
63
64
  content: [{ type: "text", text: "Stayed in normal mode. Proceed, or ask the user how to continue." }],
@@ -65,6 +66,10 @@ export function registerEnterTool(pi: ExtensionAPI, runtime: PlanRuntime): void
65
66
  };
66
67
  }
67
68
  runtime.enable(ctx);
69
+ if (isRemoteTurn(ctx)) {
70
+ // The peer UI has no footer chip; say the mode and its exit in the chat.
71
+ ctx.ui.notify("Plan mode on — write access is off. Say “exit plan mode” to leave without approving.", "info");
72
+ }
68
73
  return {
69
74
  content: [{ type: "text", text: ENTERED_TEXT }],
70
75
  details: { entered: true } satisfies EnterPlanModeDetails,