@gavin-hu/my-pi-agent 0.21.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 (254) hide show
  1. package/README.md +46 -31
  2. package/README.zh-CN.md +40 -26
  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 -24
  9. package/docs/extensions/board.md +361 -147
  10. package/docs/extensions/bytefly.md +152 -111
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +30 -31
  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 +166 -126
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +34 -18
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +72 -33
  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 +56 -15
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +5 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +4 -40
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +13 -11
  38. package/extensions/board/result.ts +30 -16
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +42 -10
  41. package/extensions/board/snapshot.ts +2 -1
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +131 -44
  44. package/extensions/board/types.ts +36 -32
  45. package/extensions/board/view/card.ts +229 -0
  46. package/extensions/board/view/client.ts +33 -15
  47. package/extensions/board/view/dialog.ts +169 -6
  48. package/extensions/board/view/empty.ts +47 -0
  49. package/extensions/board/view/keys.ts +13 -2
  50. package/extensions/board/view/live.ts +89 -21
  51. package/extensions/board/view/names.ts +13 -4
  52. package/extensions/board/view/page.ts +23 -9
  53. package/extensions/board/view/render.ts +208 -271
  54. package/extensions/board/view/styles.ts +123 -31
  55. package/extensions/board/view/team.ts +297 -76
  56. package/extensions/bytefly/block/fallback.ts +1 -1
  57. package/extensions/bytefly/doctor/runtime.ts +5 -2
  58. package/extensions/bytefly/footer/collect/label.ts +60 -0
  59. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  60. package/extensions/bytefly/footer/config.ts +4 -2
  61. package/extensions/bytefly/footer/register.ts +33 -4
  62. package/extensions/bytefly/footer/render/lines.ts +7 -4
  63. package/extensions/bytefly/footer/render/segments.ts +37 -10
  64. package/extensions/bytefly/footer/render/status.ts +0 -11
  65. package/extensions/bytefly/footer/types.ts +7 -0
  66. package/extensions/bytefly/header/config.ts +1 -1
  67. package/extensions/bytefly/index.ts +32 -6
  68. package/extensions/bytefly/mode/editor.ts +115 -0
  69. package/extensions/bytefly/panel/commands.ts +210 -120
  70. package/extensions/bytefly/panel/format.ts +4 -4
  71. package/extensions/bytefly/panel/settings.ts +73 -6
  72. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  73. package/extensions/bytefly/update/cache.ts +1 -1
  74. package/extensions/bytefly/update/register.ts +4 -4
  75. package/extensions/bytefly/working/config.ts +5 -7
  76. package/extensions/bytefly/working/format.ts +10 -13
  77. package/extensions/bytefly/working/register.ts +22 -27
  78. package/extensions/check/commands.ts +3 -2
  79. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  80. package/extensions/context/commands.ts +2 -2
  81. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  82. package/extensions/file-browse/index.ts +67 -36
  83. package/extensions/file-read/render.ts +4 -4
  84. package/extensions/goal/plan-route.ts +2 -2
  85. package/extensions/job/child.ts +527 -0
  86. package/extensions/job/clock.ts +142 -0
  87. package/extensions/job/commands.ts +4 -2
  88. package/extensions/job/config.ts +8 -1
  89. package/extensions/job/format.ts +22 -1
  90. package/extensions/job/log-writer.ts +13 -2
  91. package/extensions/job/registry.ts +63 -7
  92. package/extensions/job/render.ts +34 -0
  93. package/extensions/job/runtime.ts +296 -655
  94. package/extensions/job/session.ts +132 -19
  95. package/extensions/job/store.ts +7 -7
  96. package/extensions/job/tools.ts +3 -3
  97. package/extensions/job/types.ts +47 -0
  98. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
  99. package/extensions/job/waiters.ts +44 -1
  100. package/extensions/memory/commands.ts +4 -2
  101. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  102. package/extensions/plan/commands.ts +12 -9
  103. package/extensions/plan/format.ts +8 -2
  104. package/extensions/plan/git-bash.ts +115 -16
  105. package/extensions/plan/index.ts +53 -52
  106. package/extensions/plan/plan-index.ts +4 -2
  107. package/extensions/plan/plans.ts +5 -54
  108. package/extensions/plan/policy.ts +8 -10
  109. package/extensions/plan/review-lazy.ts +85 -0
  110. package/extensions/plan/review.ts +2 -2
  111. package/extensions/plan/runtime.ts +49 -39
  112. package/extensions/plan/sections.ts +1 -1
  113. package/extensions/plan/steps.ts +3 -3
  114. package/extensions/plan/todos.ts +1 -1
  115. package/extensions/plan/tools/cancel.ts +27 -62
  116. package/extensions/plan/tools/enter.ts +55 -81
  117. package/extensions/plan/tools/exit.ts +221 -238
  118. package/extensions/plan/tools/refusal.ts +18 -0
  119. package/extensions/plan/tools/steps.ts +75 -92
  120. package/extensions/plan/tools/write.ts +77 -120
  121. package/extensions/plan/tools.ts +188 -30
  122. package/extensions/plan/tui.ts +5 -5
  123. package/extensions/plan/types.ts +48 -5
  124. package/extensions/plan/web-client.ts +3 -0
  125. package/extensions/plan/web-lazy.ts +53 -0
  126. package/extensions/plan/web-markdown.ts +9 -67
  127. package/extensions/plan/web-payload.ts +3 -3
  128. package/extensions/plan/web-router.ts +18 -9
  129. package/extensions/plan/web.ts +1 -12
  130. package/extensions/question/tools.ts +6 -2
  131. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  132. package/extensions/rewind/commands/commands.ts +6 -3
  133. package/extensions/rewind/state/runtime.ts +1 -1
  134. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  135. package/extensions/rewind/view/review-tui.ts +4 -4
  136. package/extensions/serve/config.ts +10 -8
  137. package/extensions/serve/index.ts +17 -3
  138. package/extensions/serve/registry.ts +26 -17
  139. package/extensions/session-bus/commands.ts +11 -6
  140. package/extensions/session-bus/join-request.ts +2 -1
  141. package/extensions/session-bus/mail-watch.ts +26 -17
  142. package/extensions/session-bus/protocol.ts +15 -3
  143. package/extensions/session-bus/resolve.ts +5 -7
  144. package/extensions/session-bus/runtime.ts +40 -12
  145. package/extensions/session-bus/store.ts +101 -19
  146. package/extensions/session-bus/types.ts +19 -0
  147. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  148. package/extensions/subagent/agents.ts +1 -1
  149. package/extensions/subagent/commands.ts +4 -2
  150. package/extensions/subagent/format.ts +15 -15
  151. package/extensions/subagent/index.ts +1 -1
  152. package/extensions/subagent/render.ts +3 -3
  153. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  154. package/extensions/team/commands.ts +24 -7
  155. package/extensions/team/dispatch.ts +18 -4
  156. package/extensions/team/format.ts +17 -2
  157. package/extensions/team/index.ts +9 -8
  158. package/extensions/team/schema.ts +6 -5
  159. package/extensions/team/tools.ts +107 -45
  160. package/extensions/team/types.ts +3 -2
  161. package/extensions/todo/commands.ts +4 -2
  162. package/extensions/todo/format.ts +11 -0
  163. package/extensions/todo/render.ts +57 -0
  164. package/extensions/todo/runtime.ts +1 -1
  165. package/extensions/todo/tools.ts +1 -1
  166. package/extensions/todo/view/todo-screen.ts +122 -0
  167. package/extensions/todo/widget.ts +47 -0
  168. package/extensions/watch/commands.ts +4 -2
  169. package/extensions/watch/format.ts +36 -7
  170. package/extensions/watch/render.ts +57 -0
  171. package/extensions/watch/runtime.ts +27 -22
  172. package/extensions/watch/tools.ts +4 -4
  173. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  174. package/extensions/web-access/fetch/tool.ts +12 -4
  175. package/extensions/wechat/commands/commands.ts +18 -14
  176. package/extensions/wechat/index.ts +72 -37
  177. package/extensions/wechat/state/credentials.ts +1 -1
  178. package/extensions/wechat/state/state.ts +1 -1
  179. package/extensions/wechat/text/status.ts +112 -0
  180. package/extensions/wechat/types.ts +2 -2
  181. package/extensions/wechat/view/status-screen.ts +4 -103
  182. package/extensions/worktree/commands.ts +11 -9
  183. package/extensions/worktree/enter.ts +2 -2
  184. package/extensions/worktree/exit.ts +2 -2
  185. package/extensions/worktree/index.ts +5 -5
  186. package/extensions/worktree/screen.ts +3 -3
  187. package/extensions/worktree/status.ts +2 -2
  188. package/extensions/worktree/tools.ts +113 -116
  189. package/lib/README.md +27 -13
  190. package/lib/board/actions.ts +111 -0
  191. package/lib/board/artifact.ts +149 -0
  192. package/lib/board/card.ts +163 -52
  193. package/lib/board/config.ts +58 -0
  194. package/lib/board/contract.ts +113 -0
  195. package/lib/board/join.ts +38 -0
  196. package/lib/board/lanes.ts +182 -0
  197. package/lib/board/lead.ts +130 -0
  198. package/lib/board/limits.ts +78 -0
  199. package/lib/board/links.ts +31 -10
  200. package/lib/board/page-pointer.ts +1 -1
  201. package/lib/board/read.ts +83 -0
  202. package/lib/board/reason.ts +102 -0
  203. package/lib/board/round-text.ts +68 -6
  204. package/lib/board/round.ts +31 -15
  205. package/lib/board/rules.ts +151 -307
  206. package/lib/board/seat.ts +35 -0
  207. package/lib/board/session.ts +136 -54
  208. package/lib/board/skipped.ts +22 -0
  209. package/lib/board/steps.ts +487 -0
  210. package/lib/board/store.ts +87 -30
  211. package/lib/board/tone.ts +92 -0
  212. package/lib/board/view.ts +70 -58
  213. package/{extensions/watch → lib}/child.ts +40 -21
  214. package/lib/env.ts +65 -7
  215. package/lib/format.ts +13 -0
  216. package/lib/http/index.ts +14 -12
  217. package/lib/http/port.ts +15 -10
  218. package/lib/lazy.ts +45 -0
  219. package/lib/plan-entry.ts +39 -10
  220. package/lib/plans/paths.ts +82 -0
  221. package/lib/policy.ts +9 -6
  222. package/lib/report.ts +3 -2
  223. package/lib/requirements.ts +8 -8
  224. package/lib/session-label.ts +111 -5
  225. package/lib/session-roster.ts +43 -1
  226. package/lib/session-tool.ts +5 -4
  227. package/lib/stray-fields.ts +7 -2
  228. package/lib/tool-names.ts +5 -10
  229. package/lib/tui/README.md +23 -8
  230. package/lib/tui/chrome.ts +16 -15
  231. package/lib/tui/cursor.ts +18 -3
  232. package/lib/tui/glyphs.ts +9 -7
  233. package/lib/tui/tokens.ts +33 -2
  234. package/lib/tui/tool.ts +3 -3
  235. package/lib/web/README.md +21 -0
  236. package/lib/web/components.ts +5 -1
  237. package/lib/web/index.ts +1 -0
  238. package/lib/web/markdown.ts +83 -0
  239. package/lib/web/markup.ts +2 -1
  240. package/lib/web/runtime.ts +9 -10
  241. package/package.json +6 -1
  242. package/skills/init/SKILL.md +5 -5
  243. package/skills/next/SKILL.md +3 -5
  244. package/extensions/board/seat.ts +0 -28
  245. package/extensions/todo/tui.ts +0 -212
  246. package/lib/tui/index.ts +0 -34
  247. package/skills/project-docs/SKILL.md +0 -133
  248. package/skills/project-docs/references/audit.md +0 -131
  249. package/skills/project-docs/references/changelog.md +0 -105
  250. package/skills/project-docs/references/discovery.md +0 -127
  251. package/skills/project-docs/references/readme.md +0 -80
  252. package/skills/project-docs/references/release.md +0 -84
  253. package/skills/project-docs/references/roadmap.md +0 -93
  254. 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, NO_SERVER_DEPS, 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, NO_SERVER_DEPS),
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;
@@ -23,6 +23,7 @@ import {
23
23
  CLIENT_BASE_JS,
24
24
  CLIENT_RUNTIME_JS,
25
25
  dialogScript,
26
+ INLINE_MARKDOWN_JS,
26
27
  keysDialogScript,
27
28
  THEME_TOGGLE_JS,
28
29
  } from "../../lib/web/index.ts";
@@ -125,6 +126,8 @@ ${WEB_COMMENTS_JS}
125
126
 
126
127
  ${WEB_COLLAB_JS}
127
128
 
129
+ ${INLINE_MARKDOWN_JS}
130
+
128
131
  ${WEB_MARKDOWN_JS}
129
132
 
130
133
  // Review facts: the plan's step progress, and the comments per section.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The plan review server, loaded when a review first asks for it.
3
+ *
4
+ * The browser review owns `lib/http`, the web kit, and the plan page; none of it
5
+ * is needed to register `/plan` or the plan tools. `index.ts`, `commands.ts`, and
6
+ * `tools.ts` therefore share this proxy over `lib/lazy`: the first review loads
7
+ * `web.ts` once, and `closeAll` on shutdown only closes a review that was already
8
+ * opened, so a session that never reviewed a plan imports none of the server.
9
+ *
10
+ * The synchronous reads (`current`, `shareState`) answer `undefined` until the
11
+ * module is loaded, which is the truth: no review can be open before a load.
12
+ */
13
+
14
+ import { lazy } from "../../lib/lazy.ts";
15
+ import type { PlanStore } from "./plans.ts";
16
+ import type { PlanWebReview, PlanWebReviewDeps } from "./web.ts";
17
+
18
+ /** The review module, as the proxy loads it. */
19
+ export type PlanWebModule = typeof import("./web.ts");
20
+
21
+ /** How the proxy reaches the review module; production uses the dynamic import. */
22
+ export type PlanWebLoader = () => Promise<PlanWebModule>;
23
+
24
+ /**
25
+ * The deps a caller that injects its own review leaves out.
26
+ *
27
+ * A review needs a place on the session's server, so a caller with no wiring says so
28
+ * rather than binding a socket of its own: the start is refused with a sentence.
29
+ */
30
+ export const NO_SERVER_DEPS: PlanWebReviewDeps = {
31
+ mount: async () => ({ kind: "refused", reason: "this review has no server wired" }),
32
+ session: () => "",
33
+ };
34
+
35
+ export function createLazyPlanWebReview(
36
+ store: PlanStore,
37
+ deps: PlanWebReviewDeps,
38
+ load: PlanWebLoader = () => import("./web.ts"),
39
+ ): PlanWebReview {
40
+ const review = lazy(async () => (await load()).createPlanWebReview(store, deps));
41
+ return {
42
+ start: async (cwd, plan, options) => (await review.get()).start(cwd, plan, options),
43
+ current: () => review.peek()?.current(),
44
+ share: async () => (await review.get()).share(),
45
+ stopSharing: async () => (await review.get()).stopSharing(),
46
+ shareState: () => review.peek()?.shareState(),
47
+ // Shutdown must not start a load: a review that never opened has nothing to close.
48
+ closeAll: async () => {
49
+ const opened = review.peek();
50
+ if (opened !== undefined) await opened.closeAll();
51
+ },
52
+ };
53
+ }
@@ -1,76 +1,18 @@
1
1
  /**
2
- * The Markdown half of the review client script.
2
+ * The block half of the plan review's Markdown: tables, fenced code, headings, quotes,
3
+ * and lists.
3
4
  *
4
5
  * It is a fragment of the same IIFE `web-client.ts` ships (`WEB_MARKDOWN_JS` is
5
6
  * interpolated into `REVIEW_CLIENT_JS`), kept in its own module so the client's
6
- * event wiring and its document rendering are read one at a time. Every value it
7
- * writes is a DOM text node: plan and comment text never becomes markup, and the
8
- * only HTML it copies is the server's own `tok-*` spans.
7
+ * event wiring and its document rendering are read one at a time. The text inside a
8
+ * block is rendered by `renderInline`, which crosses from `lib/web/markdown.ts` with
9
+ * the board's card notes — one inline grammar and one link policy for both pages.
10
+ *
11
+ * Every value it writes is a DOM text node: plan and comment text never becomes markup,
12
+ * and the only HTML it copies is the server's own `tok-*` spans.
9
13
  */
10
14
 
11
- export const WEB_MARKDOWN_JS = String.raw` // A link is rendered only for an http(s) or mailto target. Resolving against
12
- // the review origin is what catches a target that only looks relative: the
13
- // URL parser strips a tab or newline, so "/\t/evil.com" is cross-origin once
14
- // it is parsed, and a disguised target must stay text.
15
- function safeHref(url, base) {
16
- var trimmed = String(url).trim();
17
- if (!trimmed || /^\/\//.test(trimmed)) return null;
18
- var resolved;
19
- try { resolved = new URL(trimmed, base); } catch (error) { return null; }
20
- if (resolved.protocol === "mailto:") return trimmed;
21
- if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return null;
22
- if (resolved.username || resolved.password) return null;
23
- if (resolved.origin === new URL(base).origin) return resolved.href;
24
- return /^https?:\/\//i.test(trimmed) ? resolved.href : null;
25
- }
26
-
27
- // Inline token order matters: code, emphasis, and links are matched before a
28
- // bare URL, so a URL inside a link target or a code span is never re-split.
29
- var INLINE = /(\x60[^\x60]+\x60|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\)|(?:https?:\/\/|www\.)[^\s<]+)/g;
30
-
31
- function renderInline(container, text) {
32
- String(text).split(INLINE).forEach(function (part) {
33
- if (!part) return;
34
- var match;
35
- if ((match = /^\x60([^\x60]+)\x60$/.exec(part))) {
36
- var code = el("code"); code.textContent = match[1]; container.appendChild(code); return;
37
- }
38
- if ((match = /^\*\*([^*]+)\*\*$/.exec(part))) {
39
- var strong = el("strong"); strong.textContent = match[1]; container.appendChild(strong); return;
40
- }
41
- if ((match = /^\*([^*]+)\*$/.exec(part))) {
42
- var em = el("em"); em.textContent = match[1]; container.appendChild(em); return;
43
- }
44
- if ((match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part))) {
45
- var href = safeHref(match[2], location.href);
46
- if (!href) { container.appendChild(document.createTextNode(part)); return; }
47
- var link = el("a"); link.textContent = match[1]; link.href = href; link.target = "_blank"; link.rel = "noreferrer noopener";
48
- container.appendChild(link); return;
49
- }
50
- // GFM extended autolink: a bare http(s) or www. URL in running text.
51
- if ((match = /^(?:https?:\/\/|www\.)[^\s<]+$/.exec(part))) {
52
- var url = match[0].replace(/[.,;:!?*_~]+$/, "");
53
- while (url.charAt(url.length - 1) === ")" && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) {
54
- url = url.slice(0, -1).replace(/[.,;:!?*_~]+$/, "");
55
- }
56
- var dot = url.indexOf(".");
57
- var target = /^www\./i.test(url) ? "http://" + url : url;
58
- var safe = dot > 0 && /\.[^./]/.test(url.slice(dot)) ? safeHref(target, location.href) : null;
59
- if (safe) {
60
- var auto = el("a");
61
- auto.textContent = url;
62
- auto.href = safe;
63
- auto.target = "_blank";
64
- auto.rel = "noreferrer noopener";
65
- container.appendChild(auto);
66
- return;
67
- }
68
- }
69
- container.appendChild(document.createTextNode(part));
70
- });
71
- }
72
-
73
- // Copy only text and tok-* spans out of server-highlighted HTML, so a code
15
+ export const WEB_MARKDOWN_JS = String.raw` // Copy only text and tok-* spans out of server-highlighted HTML, so a code
74
16
  // line can never introduce markup even though it arrives as a string.
75
17
  function copyTokens(from, to) {
76
18
  Array.prototype.forEach.call(from.childNodes, function (node) {
@@ -60,7 +60,7 @@ export interface ReviewPageData {
60
60
  outline: ReviewOutlineItem[];
61
61
  /**
62
62
  * The plan's step progress, from the same `extractPlanSteps` the approval and
63
- * `set_plan_steps` use: the `Steps` section when there is one, so the page
63
+ * the `steps` action use: the `Steps` section when there is one, so the page
64
64
  * agrees with what approval seeds rather than counting every checkbox.
65
65
  */
66
66
  steps: { done: number; total: number };
@@ -71,7 +71,7 @@ export interface ReviewPageData {
71
71
  * count, comment length, and quote length, from `feedback.ts`.
72
72
  */
73
73
  limits: { comments: number; comment: number; quote: number };
74
- /** Whether the review may approve the plan (only the `exit_plan_mode` flow). */
74
+ /** Whether the review may approve the plan (only the `plan` tool's `exit` flow). */
75
75
  canApprove: boolean;
76
76
  /** Who is looking: `owner` may approve, `reviewer` may only comment. */
77
77
  role: ReviewRole;
@@ -150,7 +150,7 @@ export function reviewPageData(input: ReviewPayloadInput): ReviewPageData {
150
150
  },
151
151
  ],
152
152
  );
153
- // The same step list approval seeds and set_plan_steps indexes, so the page's
153
+ // The same step list approval seeds and the `steps` action indexes, so the page's
154
154
  // progress cannot disagree with the todo list.
155
155
  const stepList = extractPlanSteps(current);
156
156
  // Highlight code blocks once, at the boundary: the client copies only the tok-*
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * The request router for browser plan review.
3
3
  *
4
- * A request is validated (method, `Host`, origin, token), matched to a route,
5
- * and answered with a plain {@link ServeResponse}, so every route can be tested
6
- * without a socket. The `/events` route is a marker: the node server in `web.ts`
7
- * keeps that response open as a Server-Sent Events stream.
4
+ * A request is validated (method, `Host`, origin, token, and — on a route that
5
+ * reads a body — that the body is JSON), matched to a route, and answered with a
6
+ * plain {@link ServeResponse}, so every route can be tested without a socket. The
7
+ * `/events` route is a marker: the node server in `web.ts` keeps that response open
8
+ * as a Server-Sent Events stream.
8
9
  *
9
10
  * The token is a per-session secret carried in the page URL, and on a shared
10
11
  * review it is the only credential a reviewer has: it names the caller — the
@@ -20,6 +21,7 @@ import {
20
21
  pageHeaders,
21
22
  parseRequestUrl,
22
23
  postOnly,
24
+ requireJsonBody,
23
25
  scriptHeaders,
24
26
  type ServeResponse,
25
27
  } from "../../lib/http/index.ts";
@@ -40,7 +42,7 @@ export interface ReviewContext {
40
42
  caller: (token: string) => ReviewCaller | undefined;
41
43
  /** The host names this server answers to now, so a rebound page is refused. */
42
44
  hosts: () => readonly string[];
43
- /** Whether the browser may approve the plan (only the `exit_plan_mode` flow). */
45
+ /** Whether the browser may approve the plan (only the `plan` tool's `exit` flow). */
44
46
  canApprove: boolean;
45
47
  /** Build the page data for the caller (recomputed per request). */
46
48
  page: (caller: ReviewCaller) => Promise<ReviewPageData>;
@@ -157,7 +159,9 @@ export async function handleReviewRequest(
157
159
  }
158
160
  if (pathname === "/api/share/revoke") {
159
161
  const refusal =
160
- postOnly(method, "revoke a link") ?? roleOnly(caller, ["owner"], "Only the plan owner can revoke a link.");
162
+ postOnly(method, "revoke a link") ??
163
+ requireJsonBody(headers["content-type"]) ??
164
+ roleOnly(caller, ["owner"], "Only the plan owner can revoke a link.");
161
165
  if (refusal) return refusal;
162
166
  const id = linkIdOf(body);
163
167
  if (id === undefined) return jsonError(400, "The request body must name the link's id.");
@@ -169,7 +173,9 @@ export async function handleReviewRequest(
169
173
  // A reviewer's whole surface: persist the comments they wrote and end
170
174
  // nothing. Only the owner's own save decides the review.
171
175
  const refusal =
172
- postOnly(method, "post comments") ?? roleOnly(caller, ["reviewer"], "Only a shared reviewer posts comments.");
176
+ postOnly(method, "post comments") ??
177
+ requireJsonBody(headers["content-type"]) ??
178
+ roleOnly(caller, ["reviewer"], "Only a shared reviewer posts comments.");
173
179
  if (refusal) return refusal;
174
180
  const annotations = parseAnnotations(body);
175
181
  if (isResponse(annotations)) return annotations;
@@ -182,6 +188,7 @@ export async function handleReviewRequest(
182
188
  // `/api/comment`, so this route can never settle a shared review.
183
189
  const refusal =
184
190
  postOnly(method, "save feedback") ??
191
+ requireJsonBody(headers["content-type"]) ??
185
192
  roleOnly(caller, ["owner"], "Only the plan owner saves this review; a reviewer posts comments.");
186
193
  if (refusal) return refusal;
187
194
  const annotations = parseAnnotations(body);
@@ -192,10 +199,12 @@ export async function handleReviewRequest(
192
199
  return jsonResponse(200, { ok: true });
193
200
  }
194
201
  if (pathname === "/api/approve") {
195
- // Only the owner's own page approves, and only the exit_plan_mode flow owns
202
+ // Only the owner's own page approves, and only the `exit` flow owns
196
203
  // the side effects: a reviewer token is refused whatever it posts.
197
204
  const refusal =
198
- postOnly(method, "approve") ?? roleOnly(caller, ["owner"], "A shared review cannot approve the plan.");
205
+ postOnly(method, "approve") ??
206
+ requireJsonBody(headers["content-type"]) ??
207
+ roleOnly(caller, ["owner"], "A shared review cannot approve the plan.");
199
208
  if (refusal) return refusal;
200
209
  if (!ctx.canApprove) return jsonError(403, "This review cannot be approved from the browser.");
201
210
  const annotations = parseAnnotations(body);