@gavin-hu/my-pi-agent 0.17.0 → 0.19.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 (274) hide show
  1. package/README.md +61 -45
  2. package/README.zh-CN.md +54 -42
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +344 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +465 -416
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +554 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +66 -53
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
  14. package/docs/extensions/plan.md +976 -0
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +285 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +166 -119
  19. package/docs/extensions/team.md +226 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +60 -38
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
  25. package/extensions/board/commands.ts +87 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +72 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +312 -0
  32. package/extensions/board/result.ts +99 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +75 -0
  35. package/extensions/board/seat.ts +29 -0
  36. package/extensions/board/snapshot.ts +41 -0
  37. package/extensions/board/tools.ts +279 -0
  38. package/extensions/board/transport/router.ts +172 -0
  39. package/extensions/board/transport/server.ts +149 -0
  40. package/extensions/board/types.ts +54 -0
  41. package/extensions/board/view/actions.ts +306 -0
  42. package/extensions/board/view/client.ts +41 -0
  43. package/extensions/board/view/keys.ts +121 -0
  44. package/extensions/board/view/live.ts +397 -0
  45. package/extensions/board/view/page.ts +158 -0
  46. package/extensions/board/view/render.ts +496 -0
  47. package/extensions/board/view/styles.ts +211 -0
  48. package/extensions/board/view/team.ts +305 -0
  49. package/extensions/board/writes.ts +99 -0
  50. package/extensions/bytefly/notify/types.ts +1 -1
  51. package/extensions/bytefly/panel/commands.ts +40 -7
  52. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  53. package/extensions/bytefly/panel/settings.ts +11 -21
  54. package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
  55. package/extensions/bytefly/picker/discovery.ts +0 -2
  56. package/extensions/bytefly/picker/types.ts +0 -2
  57. package/extensions/bytefly/update/check.ts +39 -0
  58. package/extensions/bytefly/update/format.ts +15 -0
  59. package/extensions/bytefly/update/register.ts +73 -34
  60. package/extensions/bytefly/update/state.ts +6 -3
  61. package/extensions/context/tui.ts +1 -1
  62. package/extensions/file-browse/{config.ts → config/config.ts} +13 -4
  63. package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
  64. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  65. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  66. package/extensions/file-browse/index.ts +4 -4
  67. package/extensions/file-browse/{live.ts → live/live.ts} +54 -82
  68. package/extensions/file-browse/live/rev.ts +42 -0
  69. package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
  70. package/extensions/file-browse/transport/server.ts +110 -0
  71. package/extensions/file-browse/view/chrome.ts +83 -0
  72. package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
  73. package/extensions/file-browse/view/diff-html.ts +94 -0
  74. package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
  75. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  76. package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -1
  77. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
  78. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
  79. package/extensions/file-browse/{styles.ts → view/styles.ts} +31 -5
  80. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  81. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  82. package/extensions/file-read/schema.ts +2 -1
  83. package/extensions/goal/plan-route.ts +8 -2
  84. package/extensions/job/index.ts +15 -7
  85. package/extensions/memory/store.ts +12 -5
  86. package/extensions/plan/blocks.ts +23 -2
  87. package/extensions/plan/commands.ts +77 -21
  88. package/extensions/plan/feedback.ts +409 -63
  89. package/extensions/plan/index.ts +7 -5
  90. package/extensions/plan/plan-index.ts +44 -10
  91. package/extensions/plan/plan-text.ts +51 -24
  92. package/extensions/plan/plans.ts +162 -57
  93. package/extensions/plan/policy.ts +20 -12
  94. package/extensions/plan/review-prompt.ts +93 -0
  95. package/extensions/plan/review.ts +51 -95
  96. package/extensions/plan/runtime.ts +22 -10
  97. package/extensions/plan/sections.ts +112 -0
  98. package/extensions/plan/steps.ts +63 -95
  99. package/extensions/plan/tools/enter.ts +8 -3
  100. package/extensions/plan/tools/exit.ts +13 -19
  101. package/extensions/plan/tools/write.ts +8 -3
  102. package/extensions/plan/tools.ts +1 -1
  103. package/extensions/plan/tui.ts +29 -15
  104. package/extensions/plan/types.ts +29 -3
  105. package/extensions/plan/web-chrome.ts +2 -1
  106. package/extensions/plan/web-client.ts +122 -178
  107. package/extensions/plan/web-collab.ts +207 -0
  108. package/extensions/plan/web-comments.ts +269 -0
  109. package/extensions/plan/web-page.ts +96 -17
  110. package/extensions/plan/web-router.ts +109 -47
  111. package/extensions/plan/web-share-session.ts +191 -0
  112. package/extensions/plan/web-share.ts +169 -0
  113. package/extensions/plan/web-streams.ts +124 -0
  114. package/extensions/plan/web-tokens.ts +63 -0
  115. package/extensions/plan/web.ts +245 -151
  116. package/extensions/question/dialogs.ts +91 -1
  117. package/extensions/question/tools.ts +2 -1
  118. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  119. package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
  120. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  121. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  122. package/extensions/rewind/index.ts +3 -3
  123. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  124. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  125. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  126. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  127. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  128. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  129. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  130. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  131. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  132. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  133. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  134. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  135. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  136. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  137. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  138. package/extensions/session-bus/commands.ts +113 -0
  139. package/extensions/session-bus/format.ts +101 -0
  140. package/extensions/session-bus/index.ts +75 -0
  141. package/extensions/session-bus/join-request.ts +40 -0
  142. package/extensions/session-bus/mail-watch.ts +134 -0
  143. package/extensions/session-bus/notice.ts +86 -0
  144. package/extensions/session-bus/overlap.ts +36 -0
  145. package/extensions/session-bus/protocol.ts +104 -0
  146. package/extensions/session-bus/resolve.ts +67 -0
  147. package/extensions/session-bus/result.ts +208 -0
  148. package/extensions/session-bus/rows.ts +171 -0
  149. package/extensions/session-bus/runtime.ts +466 -0
  150. package/extensions/session-bus/store.ts +365 -0
  151. package/extensions/session-bus/tools.ts +192 -0
  152. package/extensions/session-bus/tui.ts +394 -0
  153. package/extensions/session-bus/types.ts +98 -0
  154. package/extensions/subagent/agents.ts +9 -8
  155. package/extensions/subagent/invocation.ts +4 -2
  156. package/extensions/team/commands.ts +112 -0
  157. package/extensions/team/dispatch.ts +133 -0
  158. package/extensions/team/format.ts +105 -0
  159. package/extensions/team/index.ts +33 -0
  160. package/extensions/team/schema.ts +29 -0
  161. package/extensions/team/tools.ts +246 -0
  162. package/extensions/team/types.ts +48 -0
  163. package/extensions/watch/index.ts +15 -9
  164. package/extensions/web-access/fetch/page.ts +4 -1
  165. package/extensions/web-access/fetch/structured.ts +447 -0
  166. package/extensions/web-access/fetch/tool.ts +2 -1
  167. package/extensions/web-access/search/tool.ts +2 -1
  168. package/extensions/wechat/bridge.ts +76 -92
  169. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  170. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  171. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  172. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  173. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  174. package/extensions/wechat/dialog/channels.ts +136 -0
  175. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +66 -51
  176. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
  177. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  178. package/extensions/wechat/index.ts +10 -10
  179. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  180. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  181. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +119 -56
  182. package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
  183. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
  184. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  185. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  186. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  187. package/extensions/wechat/{notices.ts → text/notices.ts} +15 -7
  188. package/extensions/wechat/tools.ts +29 -12
  189. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  190. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +104 -40
  191. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  192. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  193. package/extensions/wechat/transport/outbound.ts +142 -0
  194. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  195. package/extensions/wechat/types.ts +10 -1
  196. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  197. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  198. package/extensions/worktree/git.ts +5 -0
  199. package/extensions/worktree/guard.ts +3 -5
  200. package/extensions/worktree/registry.ts +11 -5
  201. package/extensions/worktree/status.ts +1 -3
  202. package/lib/README.md +12 -6
  203. package/lib/board/card.ts +290 -0
  204. package/lib/board/links.ts +117 -0
  205. package/lib/board/page-pointer.ts +256 -0
  206. package/lib/board/round-text.ts +72 -0
  207. package/lib/board/round.ts +89 -0
  208. package/lib/board/rules.ts +304 -0
  209. package/lib/board/session.ts +105 -0
  210. package/lib/board/store.ts +310 -0
  211. package/lib/board/view.ts +150 -0
  212. package/lib/errors.ts +12 -4
  213. package/lib/format.ts +13 -0
  214. package/lib/http/guard.ts +112 -0
  215. package/lib/http/index.ts +16 -5
  216. package/lib/http/router.ts +40 -13
  217. package/lib/http/serve.ts +234 -0
  218. package/lib/http/server.ts +29 -13
  219. package/lib/http/sink.ts +110 -0
  220. package/lib/http/token.ts +38 -0
  221. package/lib/interaction.ts +10 -0
  222. package/lib/notice.ts +29 -2
  223. package/lib/plan-entry.ts +7 -2
  224. package/lib/policy.ts +12 -2
  225. package/lib/qr.ts +64 -0
  226. package/lib/repo-key.ts +74 -0
  227. package/lib/report.ts +52 -33
  228. package/lib/requirements.ts +7 -9
  229. package/lib/session-label.ts +64 -0
  230. package/lib/session-roster.ts +130 -0
  231. package/lib/session-tool.ts +184 -0
  232. package/lib/tool-names.ts +25 -2
  233. package/lib/tui/README.md +16 -2
  234. package/lib/tui/dock-screen.ts +85 -17
  235. package/lib/tui/glyphs.ts +2 -0
  236. package/lib/tui/line-pane.ts +15 -4
  237. package/lib/tui/tool.ts +23 -4
  238. package/lib/web/README.md +5 -2
  239. package/lib/web/components.ts +6 -1
  240. package/lib/web/runtime.ts +7 -1
  241. package/lib/web/tokens.ts +25 -2
  242. package/package.json +27 -14
  243. package/skills/init/SKILL.md +80 -45
  244. package/skills/init/references/agents-md-quality.md +26 -2
  245. package/skills/init/references/interview-questions.md +36 -14
  246. package/skills/next/SKILL.md +77 -51
  247. package/skills/project-docs/SKILL.md +133 -0
  248. package/skills/project-docs/references/audit.md +131 -0
  249. package/skills/project-docs/references/changelog.md +105 -0
  250. package/skills/project-docs/references/discovery.md +127 -0
  251. package/skills/project-docs/references/readme.md +80 -0
  252. package/skills/project-docs/references/release.md +84 -0
  253. package/skills/project-docs/references/roadmap.md +93 -0
  254. package/skills/project-docs/scripts/state.ts +316 -0
  255. package/skills/refactor/SKILL.md +100 -125
  256. package/skills/refactor/references/handoff.md +35 -0
  257. package/skills/refactor/references/signals.md +54 -0
  258. package/skills/refactor/scripts/inventory.ts +587 -0
  259. package/extensions/file-browse/README.md +0 -467
  260. package/extensions/file-browse/server.ts +0 -134
  261. package/extensions/plan/README.md +0 -709
  262. package/extensions/plan/config.ts +0 -27
  263. package/extensions/wechat/outbound.ts +0 -100
  264. package/extensions/wechat/qrcode.ts +0 -46
  265. package/skills/changelog/SKILL.md +0 -148
  266. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  267. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  268. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  269. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  270. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  271. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  272. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  273. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  274. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Pure rendering and parsing for the compact remote plan review.
3
+ *
4
+ * The prompt and its reply share one action table, so changing a number or a
5
+ * localized label cannot leave the parser behind. File delivery and the review
6
+ * lifecycle stay in `review.ts`.
7
+ */
8
+
9
+ import type { InteractionLanguage } from "../../lib/interaction.ts";
10
+ import { extractPlanSteps } from "./steps.ts";
11
+
12
+ /** The actions the compact prompt can resolve without a refinement. */
13
+ export type PlanReviewAction = "approve" | "keep" | "cancel";
14
+
15
+ /** One editor reply: an action, or free text the model should apply. */
16
+ export type PlanReviewReply =
17
+ | { action: "approve" }
18
+ | { action: "keep" }
19
+ | { action: "cancel" }
20
+ | { action: "refine"; refinement: string };
21
+
22
+ interface ReviewAction {
23
+ key: string;
24
+ action: PlanReviewAction;
25
+ zh: string;
26
+ en: string;
27
+ words: readonly string[];
28
+ }
29
+
30
+ /** The action line and the parser read this one table, in display order. */
31
+ const REVIEW_ACTIONS: readonly ReviewAction[] = [
32
+ { key: "1", action: "approve", zh: "批准", en: "approve", words: ["approve", "批准", "同意"] },
33
+ {
34
+ key: "2",
35
+ action: "keep",
36
+ zh: "继续规划",
37
+ en: "keep planning",
38
+ words: ["keep", "keep planning", "继续", "继续规划"],
39
+ },
40
+ { key: "3", action: "cancel", zh: "退出", en: "exit", words: ["exit", "cancel", "quit", "退出", "取消"] },
41
+ ];
42
+
43
+ /** How much of one step the remote digest shows. */
44
+ const DIGEST_STEP_CHARS = 90;
45
+
46
+ /**
47
+ * One compact remote prompt: title, first steps, mode status, and the action
48
+ * line. The plan file is sent before it; a non-action reply refines the plan.
49
+ */
50
+ export function planReviewPrompt(content: string, language: InteractionLanguage, attached: boolean): string {
51
+ const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim();
52
+ const steps = extractPlanSteps(content);
53
+ const count = steps.length;
54
+ const head =
55
+ language === "zh"
56
+ ? `计划${title ? `:${title}` : ""}${count > 0 ? `(${count} 步)` : ""}`
57
+ : `Plan${title ? `: ${title}` : ""}${count > 0 ? ` (${count} step${count === 1 ? "" : "s"})` : ""}`;
58
+ const preview = steps.slice(0, 3).map((step, index) => {
59
+ const text = step.content.replace(/\s+/g, " ").trim();
60
+ const shown = text.length > DIGEST_STEP_CHARS ? `${text.slice(0, DIGEST_STEP_CHARS - 1)}…` : text;
61
+ return `${index + 1}. ${shown}`;
62
+ });
63
+ const more =
64
+ count > preview.length
65
+ ? language === "zh"
66
+ ? `… 还有 ${count - preview.length} 步`
67
+ : `… (${count - preview.length} more)`
68
+ : "";
69
+ const status = language === "zh" ? "Plan 模式开启,写入已关闭。" : "Plan mode is on — write access is off.";
70
+ const attachment = attached ? (language === "zh" ? "计划已在上方。" : "The plan is above.") : "";
71
+ const choices = REVIEW_ACTIONS.map((action) => `${action.key} ${action[language]}`).join(
72
+ language === "zh" ? "," : ", ",
73
+ );
74
+ const actions =
75
+ language === "zh"
76
+ ? `回复 ${choices};或直接输入修改意见(想做别的先退出或批准)。`
77
+ : `Reply ${choices} — or type feedback to revise. To ask something else, exit or approve first.`;
78
+ return [head, ...preview, more, status, attachment, actions].filter((line) => line !== "").join("\n");
79
+ }
80
+
81
+ /**
82
+ * Parse the one editor reply. Only an exact number or action word is an action;
83
+ * any other non-empty text is a refinement request.
84
+ */
85
+ export function parsePlanReviewReply(raw: string): PlanReviewReply {
86
+ const text = raw.trim();
87
+ if (text === "") return { action: "cancel" };
88
+ const key = text.toLowerCase();
89
+ for (const action of REVIEW_ACTIONS) {
90
+ if (key === action.key || action.words.includes(key)) return { action: action.action };
91
+ }
92
+ return { action: "refine", refinement: text };
93
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The plan review gate: ask the human about a plan file and return one decision.
3
3
  *
4
- * A review has three presentations — the scrollable TUI screen, a select plus
5
- * editor for dialog-only modes, and a remote turn's plan file plus digest — and
6
- * one optional browser server. This module owns the loop that switches between
4
+ * A review has three presentations — the scrollable TUI screen, a dialog-only
5
+ * editor, and a remote turn's compact editor prompt — and one optional browser
6
+ * server. This module owns the loop that switches between
7
7
  * them, so `exit_plan_mode` and `/plan review` share one lifecycle and only map
8
8
  * the returned {@link ReviewDecision} to their own result.
9
9
  *
@@ -12,14 +12,14 @@
12
12
  * signal reaches the browser server so an aborted turn closes it.
13
13
  */
14
14
 
15
- import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
16
15
  import { basename } from "node:path";
17
- import { askHuman } from "../../lib/interaction.ts";
16
+ import type { ExtensionToolContext } from "@earendil-works/pi-coding-agent";
17
+ import { askHuman, isRemoteTurn, remoteLanguage } from "../../lib/interaction.ts";
18
18
  import { FULL_SCREEN_OVERLAY } from "../../lib/tui/chrome.ts";
19
19
  import { feedbackSummary, type PlanAnnotation } from "./feedback.ts";
20
20
  import type { PlanStore, StoredPlan } from "./plans.ts";
21
21
  import type { SectionDigest } from "./plan-text.ts";
22
- import { extractPlanSteps } from "./steps.ts";
22
+ import { planReviewPrompt, parsePlanReviewReply } from "./review-prompt.ts";
23
23
  import { PlanViewComponent, type PlanViewAction } from "./tui.ts";
24
24
  import type { PlanReviewResult, PlanReviewSession, PlanWebReview } from "./web.ts";
25
25
 
@@ -29,9 +29,6 @@ interface PlanReviewChoice {
29
29
  refinement?: string;
30
30
  }
31
31
 
32
- /** A dialog-only choice: post the full plan text, then ask again. Never returned. */
33
- type ReviewChoice = PlanReviewChoice | { action: "send" };
34
-
35
32
  /** What a review decided, and the plan state it decided on. */
36
33
  export type ReviewDecision =
37
34
  | { action: "approve"; plan: StoredPlan; comments: PlanAnnotation[] }
@@ -78,98 +75,45 @@ const REFINE_PROMPT = "Refine the plan — what should change? (Enter to save, E
78
75
  */
79
76
  export const PLAN_REVIEW_TIMEOUT_MS = 600_000;
80
77
 
81
- /** How much of one step the remote digest shows. */
82
- const DIGEST_STEP_CHARS = 90;
83
-
84
- /**
85
- * A one-message digest of the plan for a remote review.
86
- *
87
- * The file is the primary artifact, but a phone client may never render or open
88
- * it, and the peer still has to choose approve / refine. The digest carries the
89
- * title, the step count, and the first few steps; the full text follows as a
90
- * file or as the fallback message.
91
- */
92
- function planReviewDigest(content: string): string {
93
- const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim();
94
- const steps = extractPlanSteps(content);
95
- const count = steps.length;
96
- const head = `Plan${title ? `: ${title}` : ""}${count > 0 ? ` (${count} step${count === 1 ? "" : "s"})` : ""}`;
97
- const preview = steps.slice(0, 3).map((step, index) => {
98
- const text = step.content.replace(/\s+/g, " ").trim();
99
- const shown = text.length > DIGEST_STEP_CHARS ? `${text.slice(0, DIGEST_STEP_CHARS - 1)}…` : text;
100
- return `${index + 1}. ${shown}`;
101
- });
102
- const more = count > preview.length ? `… (${count - preview.length} more)` : "";
103
- return [
104
- head,
105
- ...preview,
106
- more,
107
- "Plan mode is on — write access is off. Choose “Exit plan mode” to leave without approving.",
108
- "The full plan is attached above, or sent as text if the file failed.",
109
- ]
110
- .filter((line) => line !== "")
111
- .join("\n");
112
- }
113
-
114
78
  /**
115
- * Ask the user to review the plan file. The TUI opens the scrollable review
116
- * screen (with an inline refine editor); dialog-capable modes (RPC) fall back to
117
- * a select plus the refine editor; a remote turn gets the plan file and a digest
118
- * first. Asking for the full plan re-enters with the text only, then shows the
119
- * choices again.
79
+ * Ask the user to review the plan file. The TUI opens the scrollable screen;
80
+ * dialog-capable modes (RPC/WeChat) get one editor prompt. A reply that is not an
81
+ * action is a refinement.
120
82
  */
121
83
  async function askPlanReview(
122
84
  ctx: ExtensionToolContext,
123
85
  plan: StoredPlan,
124
86
  baseline: SectionDigest[] | undefined,
125
87
  ): Promise<PlanReviewChoice> {
126
- let sentFullText = false;
127
- for (;;) {
128
- const outcome = await askHuman<ReviewChoice>(ctx, {
129
- custom: async () => {
130
- const choice = await ctx.ui.custom<PlanReviewChoice | undefined>(
131
- (tui, theme, _keybindings, done) =>
132
- new PlanViewComponent({
133
- plan,
134
- baseline,
135
- theme,
136
- onClose: (action, refinement) => done({ action, refinement }),
137
- requestRender: () => tui.requestRender(),
138
- viewportRows: () => tui.terminal?.rows,
139
- tui,
140
- }),
141
- FULL_SCREEN_OVERLAY,
142
- );
143
- return choice ?? { action: "keep" };
144
- },
145
- dialogs: async () => {
146
- const choice = await ctx.ui.select("Plan mode — what next?", [
147
- "Approve and execute",
148
- "Send the full plan",
149
- "Refine the plan",
150
- "Keep planning (stay in plan mode)",
151
- "Exit plan mode (discard)",
152
- ]);
153
- if (choice === "Approve and execute") return { action: "approve" };
154
- if (choice === "Send the full plan") return { action: "send" };
155
- if (choice === "Refine the plan") return { action: "refine" };
156
- if (choice === "Keep planning (stay in plan mode)") return { action: "keep" };
157
- // Dismissing the plan gate means leaving plan mode, not silently staying
158
- // in it. The explicit Keep choice above is the only way to stay.
159
- return { action: "cancel" };
160
- },
161
- // A remote turn gets the plan file up front, falling back to the full text;
162
- // a local TUI turn ignores both and renders the component. The digest goes
163
- // out either way so the choices stay answerable when a phone client does not
164
- // render the file bubble.
165
- file: sentFullText ? undefined : { path: plan.path, name: basename(plan.relativePath) },
166
- body: plan.content,
167
- note: sentFullText ? undefined : planReviewDigest(plan.content),
168
- timeoutMs: PLAN_REVIEW_TIMEOUT_MS,
169
- });
170
- if (outcome.action !== "send") return outcome;
171
- sentFullText = true;
172
- }
88
+ const language = remoteLanguage(ctx) ?? "en";
89
+ const remote = isRemoteTurn(ctx);
90
+ return askHuman<PlanReviewChoice>(ctx, {
91
+ custom: async () => {
92
+ const choice = await ctx.ui.custom<PlanReviewChoice | undefined>(
93
+ (tui, theme, _keybindings, done) =>
94
+ new PlanViewComponent({
95
+ plan,
96
+ baseline,
97
+ theme,
98
+ onClose: (action, refinement) => done({ action, refinement }),
99
+ requestRender: () => tui.requestRender(),
100
+ viewportRows: () => tui.terminal?.rows,
101
+ tui,
102
+ }),
103
+ FULL_SCREEN_OVERLAY,
104
+ );
105
+ return choice ?? { action: "keep" };
106
+ },
107
+ dialogs: async () => {
108
+ const answer = await ctx.ui.editor(planReviewPrompt(plan.content, language, remote), "");
109
+ return parsePlanReviewReply(answer ?? "");
110
+ },
111
+ // The full plan is the artifact under review, so it goes out before the
112
+ // choices; if the upload fails, the same text is the fallback.
113
+ file: { path: plan.path, name: basename(plan.relativePath) },
114
+ body: plan.content,
115
+ timeoutMs: PLAN_REVIEW_TIMEOUT_MS,
116
+ });
173
117
  }
174
118
 
175
119
  /** How a browser review may decide, and what to report when it opens. */
@@ -182,6 +126,11 @@ export interface BrowserReviewOptions {
182
126
  signal?: AbortSignal;
183
127
  /** Called once the server is listening and the browser was opened. */
184
128
  onOpened?: (session: PlanReviewSession) => void;
129
+ /**
130
+ * Called when a shared reviewer's post lands, so the owner learns about it where
131
+ * they are waiting: the page updates itself, the terminal has to be told.
132
+ */
133
+ onReviewerComment?: (author: string, count: number) => void;
185
134
  }
186
135
 
187
136
  /**
@@ -196,7 +145,12 @@ export async function runBrowserReview(
196
145
  ): Promise<PlanReviewResult | undefined> {
197
146
  if (options.signal?.aborted) return { status: "cancelled" };
198
147
  const session = await web
199
- .start(cwd, plan, { canApprove: options.canApprove, baseline: options.baseline, signal: options.signal })
148
+ .start(cwd, plan, {
149
+ canApprove: options.canApprove,
150
+ baseline: options.baseline,
151
+ signal: options.signal,
152
+ ...(options.onReviewerComment === undefined ? {} : { onReviewerComment: options.onReviewerComment }),
153
+ })
200
154
  .catch(() => undefined);
201
155
  if (!session) return undefined;
202
156
  options.onOpened?.(session);
@@ -242,6 +196,8 @@ export async function reviewPlan(options: ReviewPlanOptions): Promise<ReviewDeci
242
196
  baseline,
243
197
  signal,
244
198
  onOpened: (session) => ctx.ui.notify(`Plan review: opened ${session.url}`, "info"),
199
+ onReviewerComment: (author, count) =>
200
+ ctx.ui.notify(`Plan review: @${author} posted ${count === 1 ? "a comment" : `${count} comments`}.`, "info"),
245
201
  });
246
202
  if (!result) {
247
203
  ctx.ui.notify("Plan review: could not start the browser review server.", "error");
@@ -4,15 +4,15 @@
4
4
  * Enabling plan mode removes every tool the policy does not consider read-only
5
5
  * from the active set and activates the plan-mode control tools: `exit_plan_mode`
6
6
  * (present the plan for approval) and `write_plan` (save the plan artifact under
7
- * `.pi/plans`). Disabling restores exactly what it hid. The enabled flag is
7
+ * the plans directory). Disabling restores exactly what it hid. The enabled flag is
8
8
  * persisted as a custom session entry so `/resume` and `/tree` follow the
9
9
  * branch, and it is also mirrored into the footer status. The review baseline
10
10
  * travels the same way, as one digest per heading section, so the changed-section
11
11
  * markers survive a resume too.
12
12
  *
13
13
  * `write_plan` is the one sanctioned write while planning. The read-only guard
14
- * in `index.ts` permits a control tool only when the registered tool is this
15
- * extension's own (matched by source path), so a same-named tool from another
14
+ * in `index.ts` permits plan's own tools only when the registered tool is this
15
+ * extension's (matched by source path), so a same-named tool from another
16
16
  * extension cannot borrow the exemption.
17
17
  */
18
18
 
@@ -48,9 +48,17 @@ export const BASELINE_TYPE = "plan-baseline";
48
48
  /** Longest plan-file basename shown in the footer chip. */
49
49
  const STATUS_NAME_LENGTH = 28;
50
50
 
51
- /** Tools plan mode activates itself; the guard permits only this extension's own. */
51
+ /** Tools plan mode activates itself. */
52
52
  const CONTROL_TOOLS = [EXIT_TOOL, CANCEL_TOOL, WRITE_PLAN_TOOL];
53
53
 
54
+ /**
55
+ * Tools the read-only guard exempts when this extension registered them.
56
+ * `enter_plan_mode` is here only so a call already in flight when plan mode came on
57
+ * is answered by the tool ("already in plan mode"); the policy still denies it, so
58
+ * it stays hidden while planning.
59
+ */
60
+ const OWNED_TOOLS = [...CONTROL_TOOLS, ENTER_TOOL];
61
+
54
62
  export interface PlanRuntime {
55
63
  /** Whether plan mode is currently on. */
56
64
  isEnabled(): boolean;
@@ -61,8 +69,8 @@ export interface PlanRuntime {
61
69
  restore(ctx: ExtensionContext): void;
62
70
  /** Plan-file storage for `write_plan` and `exit_plan_mode`. */
63
71
  plans: PlanStore;
64
- /** Whether `toolName` is a plan-owned control tool (not a same-named impostor). */
65
- isControlTool(toolName: string): boolean;
72
+ /** Whether `toolName` is one of plan's own tools (not a same-named impostor) the read-only guard exempts. */
73
+ isOwnTool(toolName: string): boolean;
66
74
  /** Path of the most recently written plan, for the footer chip. */
67
75
  lastPlanPath(): string | undefined;
68
76
  /** Remember a written plan and repaint the footer. */
@@ -159,11 +167,15 @@ export function createPlanRuntime(
159
167
 
160
168
  const plans = createPlanStore(pi);
161
169
 
162
- const isControlTool = (toolName: string): boolean => {
163
- if (!CONTROL_TOOLS.includes(toolName)) return false;
170
+ /**
171
+ * Whether `toolName` is plan's own registration, matched by the entry path the
172
+ * host recorded. Older hosts may not record a source; only a recorded mismatch
173
+ * is rejected.
174
+ */
175
+ const isOwnTool = (toolName: string): boolean => {
176
+ if (!OWNED_TOOLS.includes(toolName)) return false;
164
177
  const info = pi.getAllTools().find((tool) => tool.name === toolName);
165
178
  const source = info?.sourceInfo?.path;
166
- // Older hosts may not record a source; only reject a recorded mismatch.
167
179
  return source === undefined || options.entryPath === undefined || samePath(source, options.entryPath);
168
180
  };
169
181
 
@@ -241,7 +253,7 @@ export function createPlanRuntime(
241
253
  if (ctx) setStatus(ctx);
242
254
  },
243
255
  plans,
244
- isControlTool,
256
+ isOwnTool,
245
257
  lastPlanPath: () => lastPlanPath,
246
258
  setLastPlan: (ctx, path) => {
247
259
  lastPlanPath = path;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Plan-document sections (pure).
3
+ *
4
+ * One fence-aware scan of a plan's markdown: the review screen navigates by
5
+ * section, `set_plan_steps` looks up `## Steps`, a carried comment finds the
6
+ * block it belongs to, and the browser outline lists the same headings. A second
7
+ * scanner would disagree about a heading inside a fence, so every reader goes
8
+ * through this module.
9
+ *
10
+ * The scan is deliberately smaller than CommonMark: a heading is up to three
11
+ * leading spaces, a space after the hashes, and an optional closing run; a fence
12
+ * is three backticks or tildes anywhere a fence may start. Inside a fence
13
+ * nothing is a heading, which is what keeps a `# comment` in a shell example out
14
+ * of the outline. Every section keeps its raw source lines, so a caller can edit
15
+ * exactly what it read.
16
+ */
17
+
18
+ /** A heading-delimited chunk of a plan, for navigation and in-place edits. */
19
+ export interface PlanSection {
20
+ /** Plain heading text, or `""` for the content before the first heading. */
21
+ title: string;
22
+ /** The section's markdown, including its heading line. */
23
+ text: string;
24
+ /** 0-based source line of the section's heading (0 for the preamble). */
25
+ line: number;
26
+ /** Heading level, or 0 for the preamble before the first heading. */
27
+ level: number;
28
+ }
29
+
30
+ /**
31
+ * A markdown heading outside a fence, capturing its level and text: up to three
32
+ * leading spaces, a space after the hashes, and a closing hash run only when a
33
+ * space separates it. Shared with the block model, so the section split and the
34
+ * browser outline read a heading line the same way, and `## C#` keeps its title.
35
+ */
36
+ export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)(?:\s+#+)?\s*$/;
37
+
38
+ /**
39
+ * A markdown code-fence opener or closer (backticks or tildes). Shared with the
40
+ * block model: a tab-indented fence is prose for both parsers, never code for one
41
+ * and prose for the other.
42
+ */
43
+ export const FENCE = /^ {0,3}(`{3,}|~{3,})/;
44
+
45
+ /**
46
+ * A heading or step label as the review surfaces compare it: inline emphasis and
47
+ * code removed, whitespace collapsed. Exported so a renderer labels an outline
48
+ * entry the same way {@link extractSections} labels its section, which is what
49
+ * lets the browser mark a changed section.
50
+ */
51
+ export function normalizeHeadingText(text: string): string {
52
+ return text
53
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
54
+ .replace(/\*([^*]+)\*/g, "$1")
55
+ .replace(/(?<![A-Za-z0-9_])_([^_]+)_(?![A-Za-z0-9_])/g, "$1")
56
+ .replace(/`([^`]+)`/g, "$1")
57
+ .replace(/\s+/g, " ")
58
+ .trim();
59
+ }
60
+
61
+ /**
62
+ * Sections of a plan, split at heading lines outside code fences. The content
63
+ * before the first heading is a section titled `""`. Pi's `Markdown` renderer
64
+ * exposes no source-line map, so the review screen renders these chunks
65
+ * separately to know where each section begins.
66
+ */
67
+ export function extractSections(plan: string): PlanSection[] {
68
+ const sections: PlanSection[] = [];
69
+ let current: PlanSection | undefined;
70
+ let fence: string | undefined;
71
+
72
+ const flush = (): void => {
73
+ if (current && (current.title !== "" || current.text.trim() !== "")) sections.push(current);
74
+ current = undefined;
75
+ };
76
+
77
+ plan.split("\n").forEach((line, index) => {
78
+ const fenceMatch = line.match(FENCE);
79
+ if (fenceMatch) {
80
+ if (fence === undefined) fence = fenceMatch[1][0];
81
+ else if (fence === fenceMatch[1][0]) fence = undefined;
82
+ } else if (fence === undefined) {
83
+ const heading = line.match(HEADING);
84
+ if (heading) {
85
+ flush();
86
+ current = { title: normalizeHeadingText(heading[2]), text: line, line: index, level: heading[1].length };
87
+ return;
88
+ }
89
+ }
90
+ if (current === undefined) current = { title: "", text: line, line: 0, level: 0 };
91
+ else current.text += `\n${line}`;
92
+ });
93
+
94
+ flush();
95
+ return sections;
96
+ }
97
+
98
+ /**
99
+ * One key per section that tells two identically titled headings apart: the title
100
+ * and which occurrence of it this is, in document order. The review pairs a
101
+ * baseline digest with the section it belongs to by it, and the feedback codec
102
+ * matches a comment's section across a rewrite, so a second `## Notes` never
103
+ * collects the first one's lines.
104
+ */
105
+ export function sectionKeys(sections: readonly { title: string }[]): string[] {
106
+ const seen = new Map<string, number>();
107
+ return sections.map((section) => {
108
+ const occurrence = seen.get(section.title) ?? 0;
109
+ seen.set(section.title, occurrence + 1);
110
+ return `${occurrence}\u0000${section.title}`;
111
+ });
112
+ }