@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 (298) hide show
  1. package/README.md +46 -30
  2. package/README.zh-CN.md +40 -25
  3. package/assets/screens/board.png +0 -0
  4. package/assets/screens/chrome.png +0 -0
  5. package/assets/screens/file-browse.png +0 -0
  6. package/assets/screens/plan-review.png +0 -0
  7. package/assets/screens/plan.png +0 -0
  8. package/docs/extensions/README.md +22 -23
  9. package/docs/extensions/board.md +397 -168
  10. package/docs/extensions/bytefly.md +234 -197
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +57 -63
  14. package/docs/extensions/file-open.md +9 -11
  15. package/docs/extensions/file-read.md +12 -14
  16. package/docs/extensions/goal.md +15 -17
  17. package/docs/extensions/job.md +54 -42
  18. package/docs/extensions/memory.md +17 -19
  19. package/docs/extensions/plan.md +197 -140
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +145 -0
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +83 -44
  26. package/docs/extensions/todo.md +18 -18
  27. package/docs/extensions/watch.md +31 -31
  28. package/docs/extensions/web-access.md +2 -4
  29. package/docs/extensions/wechat.md +34 -35
  30. package/docs/extensions/worktree.md +56 -50
  31. package/extensions/board/commands.ts +79 -25
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +17 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +5 -20
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +65 -93
  38. package/extensions/board/result.ts +30 -39
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +49 -11
  41. package/extensions/board/snapshot.ts +8 -6
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +154 -55
  44. package/extensions/board/transport/router.ts +3 -5
  45. package/extensions/board/transport/server.ts +59 -28
  46. package/extensions/board/types.ts +36 -42
  47. package/extensions/board/view/card.ts +229 -0
  48. package/extensions/board/view/client.ts +35 -16
  49. package/extensions/board/view/dialog.ts +169 -6
  50. package/extensions/board/view/empty.ts +47 -0
  51. package/extensions/board/view/keys.ts +13 -2
  52. package/extensions/board/view/live.ts +90 -28
  53. package/extensions/board/view/names.ts +13 -4
  54. package/extensions/board/view/page.ts +38 -13
  55. package/extensions/board/view/render.ts +219 -277
  56. package/extensions/board/view/styles.ts +130 -32
  57. package/extensions/board/view/team.ts +297 -76
  58. package/extensions/bytefly/block/fallback.ts +1 -1
  59. package/extensions/bytefly/doctor/runtime.ts +5 -2
  60. package/extensions/bytefly/footer/collect/label.ts +60 -0
  61. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  62. package/extensions/bytefly/footer/config.ts +7 -8
  63. package/extensions/bytefly/footer/register.ts +33 -4
  64. package/extensions/bytefly/footer/render/footer.ts +18 -4
  65. package/extensions/bytefly/footer/render/key.ts +46 -0
  66. package/extensions/bytefly/footer/render/layout.ts +13 -3
  67. package/extensions/bytefly/footer/render/lines.ts +10 -7
  68. package/extensions/bytefly/footer/render/segments.ts +41 -38
  69. package/extensions/bytefly/footer/render/status.ts +0 -11
  70. package/extensions/bytefly/footer/types.ts +7 -0
  71. package/extensions/bytefly/header/component.ts +4 -20
  72. package/extensions/bytefly/header/config.ts +1 -1
  73. package/extensions/bytefly/index.ts +32 -6
  74. package/extensions/bytefly/mode/editor.ts +115 -0
  75. package/extensions/bytefly/panel/commands.ts +210 -120
  76. package/extensions/bytefly/panel/format.ts +4 -4
  77. package/extensions/bytefly/panel/settings.ts +73 -6
  78. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  79. package/extensions/bytefly/update/cache.ts +1 -1
  80. package/extensions/bytefly/update/register.ts +4 -4
  81. package/extensions/bytefly/working/config.ts +5 -7
  82. package/extensions/bytefly/working/format.ts +10 -13
  83. package/extensions/bytefly/working/register.ts +22 -27
  84. package/extensions/check/commands.ts +3 -2
  85. package/extensions/check/render.ts +10 -1
  86. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  87. package/extensions/context/commands.ts +2 -2
  88. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  89. package/extensions/file-browse/config/config.ts +4 -19
  90. package/extensions/file-browse/index.ts +89 -79
  91. package/extensions/file-browse/live/live.ts +1 -1
  92. package/extensions/file-browse/transport/router.ts +12 -4
  93. package/extensions/file-browse/transport/server.ts +61 -28
  94. package/extensions/file-browse/view/chrome.ts +4 -4
  95. package/extensions/file-browse/view/client.ts +3 -1
  96. package/extensions/file-browse/view/html.ts +73 -40
  97. package/extensions/file-browse/view/live-client.ts +2 -2
  98. package/extensions/file-browse/view/markdown.ts +13 -14
  99. package/extensions/file-browse/view/palette.ts +2 -2
  100. package/extensions/file-browse/view/raw-html.ts +9 -9
  101. package/extensions/file-browse/view/tree.ts +1 -1
  102. package/extensions/file-browse/view/urls.ts +20 -6
  103. package/extensions/file-read/render.ts +4 -4
  104. package/extensions/goal/plan-route.ts +2 -2
  105. package/extensions/goal/tui.ts +12 -2
  106. package/extensions/job/child.ts +527 -0
  107. package/extensions/job/clock.ts +142 -0
  108. package/extensions/job/commands.ts +4 -2
  109. package/extensions/job/config.ts +8 -1
  110. package/extensions/job/format.ts +22 -1
  111. package/extensions/job/log-writer.ts +13 -2
  112. package/extensions/job/registry.ts +63 -7
  113. package/extensions/job/render.ts +34 -0
  114. package/extensions/job/runtime.ts +296 -655
  115. package/extensions/job/session.ts +132 -19
  116. package/extensions/job/store.ts +7 -7
  117. package/extensions/job/tools.ts +21 -9
  118. package/extensions/job/types.ts +47 -0
  119. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
  120. package/extensions/job/waiters.ts +44 -1
  121. package/extensions/memory/commands.ts +4 -2
  122. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  123. package/extensions/plan/commands.ts +12 -9
  124. package/extensions/plan/feedback.ts +62 -16
  125. package/extensions/plan/format.ts +8 -2
  126. package/extensions/plan/git-bash.ts +115 -16
  127. package/extensions/plan/index.ts +58 -51
  128. package/extensions/plan/plan-index.ts +4 -2
  129. package/extensions/plan/plan-text.ts +114 -0
  130. package/extensions/plan/plans.ts +5 -54
  131. package/extensions/plan/policy.ts +8 -10
  132. package/extensions/plan/review-lazy.ts +85 -0
  133. package/extensions/plan/review.ts +2 -2
  134. package/extensions/plan/runtime.ts +61 -41
  135. package/extensions/plan/sections.ts +1 -1
  136. package/extensions/plan/steps.ts +3 -3
  137. package/extensions/plan/todos.ts +1 -1
  138. package/extensions/plan/tools/cancel.ts +27 -62
  139. package/extensions/plan/tools/enter.ts +55 -81
  140. package/extensions/plan/tools/exit.ts +221 -238
  141. package/extensions/plan/tools/refusal.ts +18 -0
  142. package/extensions/plan/tools/steps.ts +75 -92
  143. package/extensions/plan/tools/write.ts +77 -120
  144. package/extensions/plan/tools.ts +188 -30
  145. package/extensions/plan/tui.ts +5 -5
  146. package/extensions/plan/types.ts +48 -5
  147. package/extensions/plan/web-blocks.ts +76 -5
  148. package/extensions/plan/web-chrome.ts +4 -0
  149. package/extensions/plan/web-client.ts +102 -2
  150. package/extensions/plan/web-collab.ts +4 -4
  151. package/extensions/plan/web-comments.ts +39 -8
  152. package/extensions/plan/web-composer.ts +14 -3
  153. package/extensions/plan/web-keys.ts +149 -25
  154. package/extensions/plan/web-lazy.ts +53 -0
  155. package/extensions/plan/web-markdown.ts +10 -68
  156. package/extensions/plan/web-outline.ts +22 -2
  157. package/extensions/plan/web-page.ts +56 -86
  158. package/extensions/plan/web-payload.ts +188 -0
  159. package/extensions/plan/web-plan.ts +23 -13
  160. package/extensions/plan/web-router.ts +18 -9
  161. package/extensions/plan/web-settle.ts +14 -7
  162. package/extensions/plan/web-share-session.ts +24 -55
  163. package/extensions/plan/web-share.ts +19 -37
  164. package/extensions/plan/web-write.ts +112 -0
  165. package/extensions/plan/web.ts +67 -131
  166. package/extensions/question/tools.ts +6 -2
  167. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  168. package/extensions/rewind/commands/commands.ts +6 -3
  169. package/extensions/rewind/state/runtime.ts +1 -1
  170. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  171. package/extensions/rewind/view/review-tui.ts +4 -4
  172. package/extensions/serve/config.ts +39 -0
  173. package/extensions/serve/index.ts +85 -0
  174. package/extensions/serve/registry.ts +260 -0
  175. package/extensions/session-bus/commands.ts +11 -6
  176. package/extensions/session-bus/join-request.ts +17 -9
  177. package/extensions/session-bus/mail-watch.ts +26 -17
  178. package/extensions/session-bus/protocol.ts +15 -3
  179. package/extensions/session-bus/resolve.ts +5 -7
  180. package/extensions/session-bus/result.ts +1 -27
  181. package/extensions/session-bus/runtime.ts +40 -12
  182. package/extensions/session-bus/store.ts +101 -19
  183. package/extensions/session-bus/tools.ts +5 -4
  184. package/extensions/session-bus/types.ts +19 -0
  185. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  186. package/extensions/subagent/agents.ts +1 -1
  187. package/extensions/subagent/commands.ts +4 -2
  188. package/extensions/subagent/format.ts +15 -15
  189. package/extensions/subagent/index.ts +1 -1
  190. package/extensions/subagent/render.ts +3 -3
  191. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  192. package/extensions/team/commands.ts +24 -5
  193. package/extensions/team/dispatch.ts +18 -4
  194. package/extensions/team/format.ts +17 -2
  195. package/extensions/team/index.ts +9 -8
  196. package/extensions/team/schema.ts +6 -5
  197. package/extensions/team/tools.ts +107 -45
  198. package/extensions/team/types.ts +3 -2
  199. package/extensions/todo/commands.ts +4 -2
  200. package/extensions/todo/format.ts +11 -0
  201. package/extensions/todo/render.ts +57 -0
  202. package/extensions/todo/runtime.ts +1 -1
  203. package/extensions/todo/tools.ts +1 -1
  204. package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
  205. package/extensions/todo/widget.ts +47 -0
  206. package/extensions/watch/commands.ts +4 -2
  207. package/extensions/watch/format.ts +36 -7
  208. package/extensions/watch/render.ts +57 -0
  209. package/extensions/watch/runtime.ts +27 -22
  210. package/extensions/watch/tools.ts +4 -4
  211. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  212. package/extensions/web-access/fetch/tool.ts +12 -4
  213. package/extensions/wechat/commands/commands.ts +18 -14
  214. package/extensions/wechat/index.ts +72 -37
  215. package/extensions/wechat/state/credentials.ts +1 -1
  216. package/extensions/wechat/state/state.ts +1 -1
  217. package/extensions/wechat/text/status.ts +112 -0
  218. package/extensions/wechat/types.ts +2 -2
  219. package/extensions/wechat/view/status-screen.ts +4 -103
  220. package/extensions/worktree/commands.ts +11 -9
  221. package/extensions/worktree/enter.ts +2 -2
  222. package/extensions/worktree/exit.ts +2 -2
  223. package/extensions/worktree/index.ts +5 -5
  224. package/extensions/worktree/screen.ts +3 -3
  225. package/extensions/worktree/status.ts +2 -2
  226. package/extensions/worktree/tools.ts +113 -116
  227. package/lib/README.md +28 -13
  228. package/lib/board/actions.ts +111 -0
  229. package/lib/board/artifact.ts +149 -0
  230. package/lib/board/card.ts +163 -52
  231. package/lib/board/config.ts +58 -0
  232. package/lib/board/contract.ts +113 -0
  233. package/lib/board/join.ts +38 -0
  234. package/lib/board/lanes.ts +182 -0
  235. package/lib/board/lead.ts +130 -0
  236. package/lib/board/limits.ts +78 -0
  237. package/lib/board/links.ts +31 -10
  238. package/lib/board/page-pointer.ts +30 -5
  239. package/lib/board/read.ts +83 -0
  240. package/lib/board/reason.ts +102 -0
  241. package/lib/board/round-text.ts +68 -6
  242. package/lib/board/round.ts +31 -15
  243. package/lib/board/rules.ts +151 -307
  244. package/lib/board/seat.ts +35 -0
  245. package/lib/board/session.ts +199 -51
  246. package/lib/board/skipped.ts +22 -0
  247. package/lib/board/steps.ts +487 -0
  248. package/lib/board/store.ts +119 -36
  249. package/lib/board/tone.ts +92 -0
  250. package/lib/board/view.ts +81 -63
  251. package/lib/bus-request.ts +83 -0
  252. package/{extensions/watch → lib}/child.ts +40 -21
  253. package/lib/env.ts +65 -7
  254. package/lib/format.ts +13 -0
  255. package/lib/http/index.ts +17 -11
  256. package/lib/http/mount.ts +142 -0
  257. package/lib/http/port.ts +30 -0
  258. package/lib/http/serve.ts +170 -46
  259. package/lib/http/server.ts +3 -3
  260. package/lib/lazy.ts +45 -0
  261. package/lib/plan-entry.ts +46 -21
  262. package/lib/plans/paths.ts +82 -0
  263. package/lib/policy.ts +9 -6
  264. package/lib/report.ts +3 -2
  265. package/lib/requirements.ts +10 -10
  266. package/lib/session-label.ts +129 -7
  267. package/lib/session-roster.ts +43 -1
  268. package/lib/session-tool.ts +39 -22
  269. package/lib/stray-fields.ts +7 -2
  270. package/lib/tool-names.ts +5 -10
  271. package/lib/tui/README.md +29 -11
  272. package/lib/tui/chrome.ts +16 -15
  273. package/lib/tui/cursor.ts +18 -3
  274. package/lib/tui/glyphs.ts +11 -11
  275. package/lib/tui/render-cache.ts +29 -0
  276. package/lib/tui/tokens.ts +33 -2
  277. package/lib/tui/tool.ts +33 -4
  278. package/lib/web/README.md +21 -0
  279. package/lib/web/client-runtime.ts +27 -1
  280. package/lib/web/components.ts +5 -1
  281. package/lib/web/index.ts +1 -0
  282. package/lib/web/markdown.ts +83 -0
  283. package/lib/web/markup.ts +20 -2
  284. package/lib/web/runtime.ts +9 -10
  285. package/package.json +8 -2
  286. package/skills/init/SKILL.md +5 -5
  287. package/skills/next/SKILL.md +3 -5
  288. package/extensions/board/config.ts +0 -36
  289. package/extensions/board/seat.ts +0 -28
  290. package/lib/tui/index.ts +0 -34
  291. package/skills/project-docs/SKILL.md +0 -133
  292. package/skills/project-docs/references/audit.md +0 -131
  293. package/skills/project-docs/references/changelog.md +0 -105
  294. package/skills/project-docs/references/discovery.md +0 -127
  295. package/skills/project-docs/references/readme.md +0 -80
  296. package/skills/project-docs/references/release.md +0 -84
  297. package/skills/project-docs/references/roadmap.md +0 -93
  298. package/skills/project-docs/scripts/state.ts +0 -316
@@ -1,43 +1,201 @@
1
1
  /**
2
- * Model-facing registration for the plan-mode tools.
2
+ * Model-facing registration for the `plan` tool.
3
3
  *
4
- * `enter_plan_mode` lets the model ask to plan before touching code; it needs
5
- * user confirmation. `write_plan` saves the plan artifact in the plans directory and
6
- * returns its path. `cancel_plan_mode` leaves planning without approving, so a
7
- * phone user is never trapped in read-only mode. `exit_plan_mode` reads that file
8
- * back, presents it for approval (a scrollable review screen in the TUI, a select dialog elsewhere),
9
- * and on approval seeds the `todo` tool with the plan's steps (through
10
- * `ctx.executeTool`, so the todo widget and validation run normally).
11
- * `set_plan_steps` then keeps the plan current: after approval the model ticks
12
- * each step off in place as it finishes it, and the todo item approval seeded
13
- * from that step is ticked with it. The file is the source of truth: the model
14
- * executes from it.
4
+ * One tool with five actions, like `board` and `memory`: `enter` asks the user to
5
+ * plan before touching code, `write` saves the plan artifact in the plans
6
+ * directory and returns its path, `exit` reads that file back and presents it for
7
+ * approval, `steps` keeps an approved plan current, and `cancel` leaves planning
8
+ * without approving, so a phone user is never trapped in read-only mode.
15
9
  *
16
- * Each registration lives in `tools/`: its schema, execute handler, and
17
- * transcripts stay together, and the approval-time helpers they share live next
18
- * to them. This module only composes them, in the order the session sees them.
10
+ * `exit` presents the plan through `review.ts` (a scrollable review screen in the
11
+ * TUI, a select dialog elsewhere) and, on approval, seeds the `todo` tool with the
12
+ * plan's steps through `ctx.executeTool`, so the todo widget and validation run
13
+ * normally. After approval the model ticks each step off in place with `steps`, and
14
+ * the todo item approval seeded from that step is ticked with it. The file is the
15
+ * source of truth: the model executes from it.
19
16
  *
20
- * Without an interactive UI the approval tools fail with an actionable message
21
- * instead of deciding for the user.
17
+ * Each action lives in `tools/`: its execute handler and the block it draws stay
18
+ * together, and the approval-time helpers they share live next to them. This
19
+ * module only composes them, in the order the description names them.
20
+ *
21
+ * Without an interactive UI the two actions that need one (`enter`, `exit`) fail
22
+ * with an actionable message instead of deciding for the user.
22
23
  */
23
24
 
24
25
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
25
- import type { PlanRuntime } from "./runtime.ts";
26
- import { registerCancelTool } from "./tools/cancel.ts";
27
- import { registerEnterTool } from "./tools/enter.ts";
28
- import { registerExitTool } from "./tools/exit.ts";
29
- import { registerSetPlanStepsTool } from "./tools/steps.ts";
30
- import { registerWriteTool } from "./tools/write.ts";
31
- import { createPlanWebReview, type PlanWebReview } from "./web.ts";
26
+ import { StringEnum } from "@earendil-works/pi-ai";
27
+ import { Type } from "typebox";
28
+ import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
29
+ import { paint } from "../../lib/tui/tokens.ts";
30
+ import { detailsOrLine, messageText, callLine } from "../../lib/tui/tool.ts";
31
+ import { MAX_STEPS, PLAN_OUTLINE } from "./steps.ts";
32
+ import { PLAN_ACTIONS, PLAN_TOOL, type PlanAction, type PlanRuntime } from "./runtime.ts";
33
+ import { callCancel, executeCancel, textCancel } from "./tools/cancel.ts";
34
+ import { callEnter, executeEnter, textEnter } from "./tools/enter.ts";
35
+ import { callExit, executeExit, textExit } from "./tools/exit.ts";
36
+ import { callSteps, executeSteps, StepUpdateParams, textSteps } from "./tools/steps.ts";
37
+ import { callWrite, executeWrite, textWrite } from "./tools/write.ts";
38
+ import type { PlanDetails } from "./types.ts";
39
+ import { createLazyPlanWebReview, NO_SERVER_DEPS } from "./web-lazy.ts";
40
+ import type { PlanWebReview } from "./web.ts";
41
+
42
+ const PlanParams = Type.Object({
43
+ action: StringEnum(PLAN_ACTIONS, {
44
+ description:
45
+ "`enter` asks to plan, `write` saves the plan file, `exit` presents it for approval, `steps` ticks its steps, `cancel` leaves without approving.",
46
+ }),
47
+ title: Type.Optional(
48
+ Type.String({
49
+ description: '`write`: short title for the plan, used as the file-name slug, e.g. "Add rate limiting".',
50
+ }),
51
+ ),
52
+ content: Type.Optional(
53
+ Type.String({
54
+ description: "`write`: the complete plan as markdown in the documented outline, including every step.",
55
+ }),
56
+ ),
57
+ plan_path: Type.Optional(
58
+ Type.String({
59
+ description:
60
+ "`write`: overwrite an existing plan file (one an earlier `write` returned) to refine it instead of creating a new one. " +
61
+ "`exit`: the file `write` returned, which the user reviews. " +
62
+ "`steps`: the approved plan file, as `exit` returned it.",
63
+ }),
64
+ ),
65
+ updates: Type.Optional(
66
+ Type.Array(StepUpdateParams, {
67
+ minItems: 1,
68
+ description:
69
+ "`steps`: the steps to set; each entry sets one step's checkbox, and the last entry wins for a duplicate step.",
70
+ }),
71
+ ),
72
+ });
73
+
74
+ const DESCRIPTION =
75
+ "Plan mode and the plan file it is built around, in one tool. `enter` asks the user to enter read-only plan mode; " +
76
+ "`write` saves the plan you designed as markdown in the plans directory; `exit` presents the saved plan for approval; " +
77
+ "`steps` ticks its steps off in place; `cancel` leaves plan mode without approving. Each action refuses outside its " +
78
+ "mode, and every field but `action` says which action reads it.\n\n" +
79
+ "`write` takes the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
80
+ `${PLAN_OUTLINE}\n\n` +
81
+ "Each section has one job: lead it with its outcome, keep a prose section to a few bullets or two or three " +
82
+ "sentences, give each step one verifiable outcome with the detail in a nested note, and put the sketch, files, " +
83
+ "and commands in inline code or a fence rather than pasted prose. " +
84
+ "When the change alters a surface that already exists, Visual shows it before and after in one fence each; " +
85
+ "tag a fence only when every line in it is code. " +
86
+ "The outline is a guide, not a form: drop Context, Approach, Visual, or Risks when there is nothing to say. " +
87
+ "Keep Steps and Verification — the steps are read from that section (without it, the shallowest list " +
88
+ "anywhere becomes the steps), and Verification becomes the last todo item. " +
89
+ "In Steps, each top-level bullet is one outcome in the order you will do them; " +
90
+ "an indented line continues that outcome, and a deeper bullet is a note. " +
91
+ `A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it ` +
92
+ "cannot track. `write` returns the path to pass to `exit`; plan_path overwrites an existing file when refining.\n\n" +
93
+ "`steps` sets the checkboxes of the given top-level steps of an approved plan in one write, and their todo items " +
94
+ "with them. Each step number matches the order the plan was approved in; a step can be marked completed or reopened. " +
95
+ "Only the checkboxes change; the step text is left as written. The file is rewritten once, and nothing changes when " +
96
+ "any step is out of range or an entry's expected text no longer matches the file.\n\n" +
97
+ "`exit` reads the plan file back and presents it with approve, keep, refine, and exit choices; on approval plan mode " +
98
+ "ends, write access returns, and the plan's steps go into the todo list. The plan file stays the source of truth. " +
99
+ "`cancel` is the escape hatch that needs no plan file: write access returns and nothing is recorded.";
100
+
101
+ /** The verb a call names, or undefined for a transcript recorded before the fold. */
102
+ function planActionOf(raw: unknown): PlanAction | undefined {
103
+ return typeof raw === "string" ? PLAN_ACTIONS.find((action) => action === raw) : undefined;
104
+ }
32
105
 
33
106
  export function registerTools(
34
107
  pi: ExtensionAPI,
35
108
  runtime: PlanRuntime,
36
- web: PlanWebReview = createPlanWebReview(runtime.plans),
109
+ web: PlanWebReview = createLazyPlanWebReview(runtime.plans, NO_SERVER_DEPS),
37
110
  ): void {
38
- registerEnterTool(pi, runtime);
39
- registerCancelTool(pi, runtime);
40
- registerWriteTool(pi, runtime);
41
- registerExitTool(pi, runtime, web);
42
- registerSetPlanStepsTool(pi, runtime);
111
+ pi.registerTool<typeof PlanParams, PlanDetails>({
112
+ name: PLAN_TOOL,
113
+ label: "Plan",
114
+ description: DESCRIPTION,
115
+ promptSnippet: "Enter read-only plan mode, save the plan, present it for approval, tick its steps, or leave it.",
116
+ promptGuidelines: [
117
+ "Call plan with action enter before a non-trivial implementation task; skip it for small, obvious changes.",
118
+ "While plan mode is active, write the full plan with action write, then call action exit with the returned plan_path.",
119
+ "When refining with plan_path, keep every section heading as it was: the review marks changed sections by heading, " +
120
+ "so a rename reads as an edit.",
121
+ "After a plan is approved, call action steps as you complete steps, passing each step's text as expected so a " +
122
+ "reordered plan is refused instead of mis-ticked.",
123
+ "Call action cancel when the user asks to leave or abandon planning without approving a plan.",
124
+ ],
125
+ parameters: PlanParams,
126
+ executionMode: "sequential",
127
+ // One annotation set for five verbs, so it takes the strictest of them: `write`
128
+ // overwrites a plan file with plan_path. The actions that only change the mode
129
+ // are exempted by the guard in `index.ts`, which is the only place that can
130
+ // still tell them apart.
131
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
132
+
133
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
134
+ switch (params.action) {
135
+ case "enter":
136
+ return await executeEnter(runtime, ctx);
137
+ case "write":
138
+ return await executeWrite(runtime, params, ctx);
139
+ case "exit":
140
+ return await executeExit(pi, runtime, web, params, ctx, signal);
141
+ case "steps":
142
+ return await executeSteps(runtime, params, ctx);
143
+ case "cancel":
144
+ return await executeCancel(runtime, ctx);
145
+ }
146
+ },
147
+
148
+ renderCall(args, theme, context) {
149
+ const last = context?.lastComponent;
150
+ switch (planActionOf(args.action)) {
151
+ case "enter":
152
+ return callEnter(theme, last);
153
+ case "write":
154
+ return callWrite(args, theme, last);
155
+ case "exit":
156
+ return callExit(args, theme, last);
157
+ case "steps":
158
+ return callSteps(args, theme, last);
159
+ case "cancel":
160
+ return callCancel(theme, last);
161
+ case undefined:
162
+ return callLine(theme, last, PLAN_TOOL, "");
163
+ }
164
+ },
165
+
166
+ renderResult(result, options, theme, context) {
167
+ const text = reuseText(context?.lastComponent);
168
+ const details = toolDetails<PlanDetails>(result.details);
169
+ const outcome = detailsOrLine(theme, result, options.expanded, details);
170
+ if ("line" in outcome) {
171
+ text.setText(outcome.line);
172
+ return text;
173
+ }
174
+ const found = outcome.details;
175
+ // No `action` means a refusal (which reports only its verb and reason), or a
176
+ // result recorded before the tool was folded; its own text is what is left.
177
+ if (!("action" in found)) {
178
+ text.setText(paint(theme, "toolOutput", messageText(result)));
179
+ return text;
180
+ }
181
+ switch (found.action) {
182
+ case "enter":
183
+ text.setText(textEnter(found, theme));
184
+ break;
185
+ case "write":
186
+ text.setText(textWrite(found, theme));
187
+ break;
188
+ case "exit":
189
+ text.setText(textExit(found, options.expanded, theme));
190
+ break;
191
+ case "steps":
192
+ text.setText(textSteps(found, theme));
193
+ break;
194
+ case "cancel":
195
+ text.setText(textCancel(found, theme));
196
+ break;
197
+ }
198
+ return text;
199
+ },
200
+ });
43
201
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Terminal rendering for the plan review screen.
3
3
  *
4
- * `PlanViewComponent` is the scrollable review screen `exit_plan_mode` opens
4
+ * `PlanViewComponent` is the scrollable review screen the `exit` action opens
5
5
  * for approval. The plan body is rendered by Pi's own `Markdown` component with
6
6
  * the active Markdown theme, so headings, lists, code, links, and quotes match
7
7
  * the rest of the application; plan-authored control characters are stripped
@@ -100,10 +100,10 @@ const GAUGE_SEGMENTS = 10;
100
100
  /** Footer hints shared by the review and the inline editor's primary actions. */
101
101
  const PRIMARY_HINTS: ScreenHint[] = [
102
102
  "Esc keep plan",
103
- { text: "a approve", color: "success" },
103
+ { text: "a approve", color: "ok" },
104
104
  { text: "r refine", color: "accent" },
105
105
  { text: "w browser", color: "accent" },
106
- { text: "x exit plan", color: "warning" },
106
+ { text: "x exit plan", color: "warn" },
107
107
  ];
108
108
  /** Scroll keys appended to the review footer while the plan overflows. */
109
109
  const SCROLL_HINTS: ScreenHint[] = ["j/k scroll", "space page", "g/G ends"];
@@ -168,7 +168,7 @@ function scrollRow(
168
168
  return truncateToWidth(` ${head}`, width);
169
169
  }
170
170
 
171
- /** Scrollable plan review screen opened by `exit_plan_mode`. */
171
+ /** Scrollable plan review screen opened by the `exit` action. */
172
172
  export class PlanViewComponent implements Component {
173
173
  private offset = 0;
174
174
  private visible = DEFAULT_ROWS;
@@ -407,7 +407,7 @@ export class PlanViewComponent implements Component {
407
407
  lines.push(truncateToWidth(` ${paint(theme, "meta", "Refine the plan:")}`, w));
408
408
  for (const line of editorLines) lines.push(truncateToWidth(` ${line}`, w));
409
409
  lines.push("");
410
- lines.push(screenHint(theme, w, ["Esc back to plan", { text: "Enter submit", color: "success" }]));
410
+ lines.push(screenHint(theme, w, ["Esc back to plan", { text: "Enter submit", color: "ok" }]));
411
411
  } else {
412
412
  if (this.maxOffset > 0) lines.push(scrollRow(theme, w, this.offset, end, body.length, sectionLabel));
413
413
  else lines.push("");
@@ -4,6 +4,9 @@
4
4
 
5
5
  import type { PlanAnnotation } from "./feedback.ts";
6
6
  import type { SectionDigest } from "./plan-text.ts";
7
+ import type { PlanAction } from "./runtime.ts";
8
+
9
+ export type { PlanAction };
7
10
  import type { StepUpdate } from "./steps.ts";
8
11
 
9
12
  /**
@@ -65,8 +68,10 @@ export interface SeededStepsEntry {
65
68
  steps: string[];
66
69
  }
67
70
 
68
- /** Structured result carried in `enter_plan_mode`'s `details`. */
71
+ /** Structured result carried in the `plan` tool's `details` for `action: "enter"`. */
69
72
  export interface EnterPlanModeDetails {
73
+ /** The verb this result answers, so a renderer reads the result instead of the call. */
74
+ action: "enter";
70
75
  /** Whether the user agreed to enter plan mode. */
71
76
  entered: boolean;
72
77
  /** True when plan mode was already enabled before this call. */
@@ -75,8 +80,10 @@ export interface EnterPlanModeDetails {
75
80
  unavailable?: boolean;
76
81
  }
77
82
 
78
- /** Structured result carried in `write_plan`'s `details`. */
83
+ /** Structured result carried in the `plan` tool's `details` for `action: "write"`. */
79
84
  export interface WritePlanDetails {
85
+ /** The verb this result answers. */
86
+ action: "write";
80
87
  /** Absolute path of the written plan file. */
81
88
  path: string;
82
89
  /** How the path is shown: `~`-shortened, since plans live outside the project. */
@@ -85,8 +92,10 @@ export interface WritePlanDetails {
85
92
  bytes: number;
86
93
  }
87
94
 
88
- /** Structured result carried in `set_plan_steps`'s `details`. */
95
+ /** Structured result carried in the `plan` tool's `details` for `action: "steps"`. */
89
96
  export interface SetPlanStepsDetails {
97
+ /** The verb this result answers. */
98
+ action: "steps";
90
99
  /** Absolute path of the plan file that was updated. */
91
100
  path: string;
92
101
  /** How the path is shown: `~`-shortened. */
@@ -99,8 +108,10 @@ export interface SetPlanStepsDetails {
99
108
  total: number;
100
109
  }
101
110
 
102
- /** Structured result carried in `exit_plan_mode`'s `details`. */
111
+ /** Structured result carried in the `plan` tool's `details` for `action: "exit"`. */
103
112
  export interface ExitPlanModeDetails {
113
+ /** The verb this result answers. */
114
+ action: "exit";
104
115
  /** Whether the user approved the plan. */
105
116
  approved: boolean;
106
117
  /** The plan the model submitted, read from its file. */
@@ -121,8 +132,40 @@ export interface ExitPlanModeDetails {
121
132
  unavailable?: boolean;
122
133
  }
123
134
 
124
- /** Structured result carried in `cancel_plan_mode`'s `details`. */
135
+ /** Structured result carried in the `plan` tool's `details` for `action: "cancel"`. */
125
136
  export interface CancelPlanModeDetails {
137
+ /** The verb this result answers. */
138
+ action: "cancel";
126
139
  /** True when plan mode was on and has now been turned off. */
127
140
  cancelled: boolean;
128
141
  }
142
+
143
+ /**
144
+ * A refused call: the reason, and the verb it named. It carries no `action`
145
+ * field on purpose — a refusal has none of that verb's state to report, and the
146
+ * absent discriminant is what tells a renderer it is looking at a rejection.
147
+ */
148
+ export interface RefusedPlanDetails {
149
+ /** The verb the call named. */
150
+ refused: PlanAction;
151
+ /** Why the call was refused, as the model reads it. */
152
+ error: string;
153
+ }
154
+
155
+ /**
156
+ * What the `plan` tool's result carries: one of the five verbs, told apart by
157
+ * `action`, or a refusal that carried no state at all.
158
+ *
159
+ * The details name their own verb so a renderer draws the result it was given
160
+ * rather than re-reading the call's arguments, which a resumed transcript may
161
+ * no longer spell the same way. A details block recorded before the fold has no
162
+ * `action` at all, and a refusal never reaches the per-verb renderers; both take
163
+ * the same fallback.
164
+ */
165
+ export type PlanDetails =
166
+ | EnterPlanModeDetails
167
+ | WritePlanDetails
168
+ | ExitPlanModeDetails
169
+ | SetPlanStepsDetails
170
+ | CancelPlanModeDetails
171
+ | RefusedPlanDetails;
@@ -59,13 +59,20 @@ export function setActive(planEl: ClientNode | null, index: number): number | nu
59
59
  * The index one step from `activeIndex`, or `null` when there is nothing to walk.
60
60
  *
61
61
  * With nothing active yet, `j` starts at the top and `k` at the bottom, which is what a
62
- * reader pressing either key on a fresh page expects.
62
+ * reader pressing either key on a fresh page expects. A view can hide blocks, and a
63
+ * hidden block is not a place the reader can be sent: the step lands on the next block
64
+ * they can actually see, or nowhere when the filter left none.
63
65
  */
64
66
  export function moveActive(planEl: ClientNode | null, activeIndex: number, delta: number): number | null {
65
67
  const blocks = blockElements(planEl);
66
68
  if (blocks.length === 0) return null;
67
- const next = activeIndex < 0 ? (delta < 0 ? blocks.length - 1 : 0) : activeIndex + delta;
68
- return setActive(planEl, next);
69
+ const step = delta < 0 ? -1 : 1;
70
+ const first =
71
+ activeIndex < 0 ? (step < 0 ? blocks.length - 1 : 0) : Math.min(Math.max(activeIndex + step, 0), blocks.length - 1);
72
+ for (let index = first; index >= 0 && index < blocks.length; index += step) {
73
+ if (blocks[index]?.hidden !== true) return setActive(planEl, index);
74
+ }
75
+ return null;
69
76
  }
70
77
 
71
78
  /**
@@ -85,5 +92,69 @@ export function blockAtLine(
85
92
  return wrap && block ? { wrap, block } : null;
86
93
  }
87
94
 
88
- /** The block rules as source, for the client bundle. */
89
- export const WEB_BLOCKS_JS = [blockElements, setActive, moveActive, blockAtLine].map(String).join("\n\n");
95
+ /** What the reader asked the plan to show. */
96
+ export type ReviewView = "all" | "changed" | "comments";
97
+
98
+ /** How much of the plan a view kept, so the page can say what it is hiding. */
99
+ export interface ViewCounts {
100
+ shown: number;
101
+ hidden: number;
102
+ }
103
+
104
+ /**
105
+ * Keep the blocks a view asks for, and hide the rest.
106
+ *
107
+ * A view is a filter over what is drawn, not a second document: `All` restores every
108
+ * block, so the reader is one click from the whole plan, and the page's status line says
109
+ * how many were hidden so a filtered document never reads as a short one. Headings stay
110
+ * in every view — a paragraph without its section is a sentence without a subject — and a
111
+ * view with nothing to show keeps the document whole rather than blanking it.
112
+ */
113
+ /**
114
+ * Keep the blocks a view asks for, and hide the rest.
115
+ *
116
+ * A view is a filter over what is drawn, not a second document: `All` restores every
117
+ * block, so the reader is one click from the whole plan, and the page's status line says
118
+ * how many were hidden so a filtered document never reads as a short one. Headings stay
119
+ * in every view — a paragraph without its section is a sentence without a subject — and a
120
+ * view with nothing to show keeps the document whole rather than blanking it. A comment
121
+ * thread is hidden with the block it hangs under: a note with no text it refers to reads
122
+ * as a comment on whatever happens to be above it.
123
+ */
124
+ export function applyView(planEl: ClientNode | null, view: ReviewView): ViewCounts {
125
+ if (!planEl) return { shown: 0, hidden: 0 };
126
+ const blocks = blockElements(planEl);
127
+ const keep = (block: ClientNode): boolean =>
128
+ view === "all" ||
129
+ block.getAttribute("data-kind") === "heading" ||
130
+ (view === "changed"
131
+ ? block.getAttribute("data-changed") === "1"
132
+ : (block.className || "").split(" ").indexOf("bf-block--commented") >= 0);
133
+ // A filter that matches nothing but the headings is not a view of the plan: showing an
134
+ // empty page is worse than showing the whole document beside the count.
135
+ const matched = blocks.filter((block) => block.getAttribute("data-kind") !== "heading" && keep(block)).length;
136
+ const whole = view !== "all" && blocks.length > 0 && matched === 0;
137
+ const visible: Record<string, boolean> = {};
138
+ for (const block of blocks) {
139
+ block.hidden = !whole && !keep(block);
140
+ if (!block.hidden) visible[block.getAttribute("data-line") ?? ""] = true;
141
+ }
142
+ const threads = planEl.querySelectorAll(".bf-comments");
143
+ for (let i = 0; i < threads.length; i++) {
144
+ const thread = threads[i];
145
+ if (!thread) continue;
146
+ const line = thread.getAttribute("data-line");
147
+ if (line !== null) thread.hidden = visible[line] !== true;
148
+ }
149
+ const shown = whole ? blocks.length : blocks.filter(keep).length;
150
+ return { shown, hidden: blocks.length - shown };
151
+ }
152
+
153
+ /**
154
+ * The block rules as source, for the client bundle.
155
+ *
156
+ * `applyView` travels with them because the page's view control calls it on every render:
157
+ * a name the bundle does not carry is a `ReferenceError` at the first repaint, which
158
+ * aborts the boot before the composer and the progress bar are wired.
159
+ */
160
+ export const WEB_BLOCKS_JS = [blockElements, setActive, moveActive, blockAtLine, applyView].map(String).join("\n\n");
@@ -76,6 +76,10 @@ export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
76
76
  if (saveButton) saveButton.disabled = true;
77
77
  if (postButton) postButton.disabled = true;
78
78
  if (cancelButton) cancelButton.disabled = true;
79
+ // The rail's comment controls go with them: a settled review takes no new comment, and
80
+ // the frame renderer is the one rule that decides that, so this asks for a repaint
81
+ // rather than removing the buttons itself.
82
+ renderAll();
79
83
  }
80
84
 
81
85
  // A conflict means the file moved under the page; only a reload can re-anchor.