@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,5 +1,5 @@
1
1
  /**
2
- * The `exit_plan_mode` tool registration.
2
+ * The `exit` action of the `plan` tool.
3
3
  *
4
4
  * It presents the saved plan for approval, then seeds the approved steps into
5
5
  * the todo list and finalizes the session goal: a `planning` goal becomes the
@@ -9,36 +9,31 @@
9
9
  */
10
10
 
11
11
  import { basename } from "node:path";
12
- import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
13
- import { Type, type Static } from "typebox";
12
+ import type { AgentToolResult, ExtensionAPI, ExtensionToolContext, Theme } from "@earendil-works/pi-coding-agent";
14
13
  import { sanitizeLine } from "../../../lib/format.ts";
15
14
  import { lastGoal } from "../../../lib/goal-state.ts";
16
15
  import { GOAL_TOOL } from "../../../lib/tool-names.ts";
17
- import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
16
+ import { failedResult } from "../../../lib/tool-result.ts";
18
17
  import { GLYPHS } from "../../../lib/tui/glyphs.ts";
19
18
  import { paint } from "../../../lib/tui/tokens.ts";
20
- import { callLine, detailsOrLine, moreHint } from "../../../lib/tui/tool.ts";
21
- import { failedResult } from "../../../lib/tool-result.ts";
19
+ import { callLine, moreHint, type ToolCallText } from "../../../lib/tui/tool.ts";
22
20
  import { feedbackSummary } from "../feedback.ts";
23
- import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
21
+ import { mergeSummary, overCap, preview, TICK_CALL, verificationNote } from "../format.ts";
24
22
  import { sanitizePlanText } from "../plan-text.ts";
25
23
  import { planTitle, type StoredPlan } from "../plans.ts";
26
- import { reviewPlan } from "../review.ts";
27
- import { EXIT_TOOL, SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
24
+ import { reviewPlan } from "../review-lazy.ts";
25
+ import { PLAN_TOOL, type PlanRuntime } from "../runtime.ts";
28
26
  import { goalSection, MAX_STEPS, planStepSummary } from "../steps.ts";
29
- import type { ExitPlanModeDetails } from "../types.ts";
27
+ import type { ExitPlanModeDetails, PlanDetails } from "../types.ts";
30
28
  import type { PlanWebReview } from "../web.ts";
31
29
  import { firstTextLine } from "./nested.ts";
30
+ import { planRefusal } from "./refusal.ts";
32
31
  import { seedTodos } from "./todo-seed.ts";
33
32
 
34
- const ExitPlanModeParams = Type.Object({
35
- plan_path: Type.String({
36
- description:
37
- "Path to the plan file written by write_plan: the absolute path it returned, or the `~/…` form it displayed. A bare name resolves inside the plans directory. The user reviews this file.",
38
- }),
39
- });
40
-
41
- type ExitPlanModeArgs = Static<typeof ExitPlanModeParams>;
33
+ /** The fields `action: "exit"` reads. */
34
+ export interface ExitPlanModeArgs {
35
+ plan_path?: string;
36
+ }
42
37
 
43
38
  /** How the goal ended up: no action when it was already final. */
44
39
  interface GoalOutcome {
@@ -77,233 +72,221 @@ function planObjective(plan: StoredPlan): string {
77
72
  return title || planTitle(basename(plan.path));
78
73
  }
79
74
 
80
- export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: PlanWebReview): void {
81
- pi.registerTool({
82
- name: EXIT_TOOL,
83
- label: "Exit plan mode",
84
- description:
85
- "Present the saved plan and ask the user to approve it. Reads the file written by write_plan, so call it with " +
86
- "plan_path. On approval plan mode ends, write access returns, and the plan's steps go into the todo list; the " +
87
- "user may instead keep planning or request a refinement. The plan file stays the source of truth.",
88
- promptSnippet: "Present the saved plan and ask the user to approve leaving plan mode.",
89
- parameters: ExitPlanModeParams,
90
- defaultActive: false,
91
- executionMode: "sequential",
92
- // readOnlyHint stays true: it writes no files, and the review dialog inside
93
- // execute is the human gate, so an outer prompt would only double-ask.
94
- annotations: { readOnlyHint: true, openWorldHint: false },
95
-
96
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
97
- const args = params as ExitPlanModeArgs;
98
- const planPath = args.plan_path.trim();
75
+ export async function executeExit(
76
+ pi: ExtensionAPI,
77
+ runtime: PlanRuntime,
78
+ web: PlanWebReview,
79
+ args: ExitPlanModeArgs,
80
+ ctx: ExtensionToolContext,
81
+ signal: AbortSignal | undefined,
82
+ ): Promise<AgentToolResult<PlanDetails>> {
83
+ const planPath = args.plan_path?.trim() ?? "";
99
84
 
100
- if (!runtime.isEnabled()) {
101
- return failedResult("Not in plan mode; there is no plan to approve.", {
102
- approved: false,
103
- plan: "",
104
- planPath,
105
- } satisfies ExitPlanModeDetails);
106
- }
85
+ if (!runtime.isEnabled()) {
86
+ return planRefusal("exit", "Not in plan mode; there is no plan to approve.");
87
+ }
88
+ if (planPath === "") {
89
+ return planRefusal("exit", "`exit` needs plan_path: the file `plan` action `write` returned.");
90
+ }
107
91
 
108
- const file = await runtime.plans.read(ctx.cwd, planPath);
109
- if (!file) {
110
- return failedResult(
111
- `No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
112
- { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
113
- );
114
- }
92
+ const file = await runtime.plans.read(ctx.cwd, planPath);
93
+ if (!file) {
94
+ return planRefusal(
95
+ "exit",
96
+ `No plan file at "${planPath}". Save the plan with \`plan\` action \`write\` first, then pass the returned path.`,
97
+ );
98
+ }
115
99
 
116
- const total = planStepSummary(file.content).total;
117
- if (total > MAX_STEPS) {
118
- // A hand-edited or older plan file can still be over the cap; refuse it here
119
- // too, so approval never seeds a list it cannot hold in full.
120
- return failedResult(
121
- `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
122
- {
123
- approved: false,
124
- plan: file.content,
125
- planPath: file.path,
126
- relativePath: file.relativePath,
127
- } satisfies ExitPlanModeDetails,
128
- );
129
- }
100
+ const total = planStepSummary(file.content).total;
101
+ if (total > MAX_STEPS) {
102
+ // A hand-edited or older plan file can still be over the cap; refuse it here
103
+ // too, so approval never seeds a list it cannot hold in full. The refusal
104
+ // carries the file it read, so the block still shows what was not presented.
105
+ return failedResult(
106
+ `Error: ${overCap(total)} It was not presented for approval; call \`plan\` action \`write\` with plan_path to group the steps, then \`exit\` again.`,
107
+ {
108
+ action: "exit",
109
+ approved: false,
110
+ plan: file.content,
111
+ planPath: file.path,
112
+ relativePath: file.relativePath,
113
+ } satisfies ExitPlanModeDetails,
114
+ );
115
+ }
130
116
 
131
- if (!ctx.hasUI) {
132
- return failedResult(
133
- `No interactive UI is available to approve the plan. It is saved at ${file.relativePath}; present it and ask the user to approve it in their reply.`,
134
- {
135
- approved: false,
136
- plan: file.content,
137
- planPath: file.path,
138
- relativePath: file.relativePath,
139
- unavailable: true,
140
- } satisfies ExitPlanModeDetails,
141
- );
142
- }
117
+ if (!ctx.hasUI) {
118
+ return failedResult(
119
+ `Error: No interactive UI is available to approve the plan. It is saved at ${file.relativePath}; present it and ask the user to approve it in their reply.`,
120
+ {
121
+ action: "exit",
122
+ approved: false,
123
+ plan: file.content,
124
+ planPath: file.path,
125
+ relativePath: file.relativePath,
126
+ unavailable: true,
127
+ } satisfies ExitPlanModeDetails,
128
+ );
129
+ }
143
130
 
144
- // One decision, however the session is attached; `review.ts` owns the loop,
145
- // the baseline, and closing the browser review when the turn is aborted.
146
- const decision = await reviewPlan({
147
- ctx,
148
- web,
149
- plans: runtime.plans,
150
- baselines: runtime,
151
- plan: file,
152
- signal,
153
- });
131
+ // One decision, however the session is attached; `review.ts` owns the loop,
132
+ // the baseline, and closing the browser review when the turn is aborted.
133
+ const decision = await reviewPlan({
134
+ ctx,
135
+ web,
136
+ plans: runtime.plans,
137
+ baselines: runtime,
138
+ plan: file,
139
+ signal,
140
+ });
154
141
 
155
- if (decision.action === "refine") {
156
- // A browser save already wrote its comments into the file; either way the
157
- // model revises the same plan and presents it again.
158
- const feedback = decision.fromBrowser
159
- ? `The user reviewed the plan in the browser and left feedback:\n${decision.refinement}`
160
- : `The user asked to refine the plan: ${decision.refinement}`;
161
- return {
162
- content: [
163
- {
164
- type: "text",
165
- text: `${feedback}\nStay in plan mode, revise the plan (write_plan, reusing plan_path), then call exit_plan_mode again.`,
166
- },
167
- ],
168
- details: {
169
- approved: false,
170
- plan: decision.plan.content,
171
- planPath: decision.plan.path,
172
- relativePath: decision.plan.relativePath,
173
- refined: true,
174
- refinement: decision.refinement,
175
- annotations: decision.comments.length > 0 ? decision.comments : undefined,
176
- } satisfies ExitPlanModeDetails,
177
- };
178
- }
179
- if (decision.action === "keep") {
180
- return {
181
- content: [
182
- { type: "text", text: "Plan not approved. Stay in plan mode, ask what to change, and revise the plan." },
183
- ],
184
- details: {
185
- approved: false,
186
- plan: decision.plan.content,
187
- planPath: decision.plan.path,
188
- relativePath: decision.plan.relativePath,
189
- } satisfies ExitPlanModeDetails,
190
- };
191
- }
192
- if (decision.action === "cancel") {
193
- runtime.disable(ctx);
194
- const shown = decision.plan.relativePath;
195
- ctx.ui.notify("Plan mode off — full access restored. The plan was not approved.", "info");
196
- return {
197
- content: [
198
- {
199
- type: "text",
200
- text:
201
- "Plan mode is off and write access is restored. The plan was not approved and no steps were recorded. " +
202
- `The plan file ${shown} remains if the user wants to review it later.`,
203
- },
204
- ],
205
- details: {
206
- approved: false,
207
- cancelled: true,
208
- plan: decision.plan.content,
209
- planPath: decision.plan.path,
210
- relativePath: decision.plan.relativePath,
211
- } satisfies ExitPlanModeDetails,
212
- };
213
- }
142
+ if (decision.action === "refine") {
143
+ // A browser save already wrote its comments into the file; either way the
144
+ // model revises the same plan and presents it again.
145
+ const feedback = decision.fromBrowser
146
+ ? `The user reviewed the plan in the browser and left feedback:\n${decision.refinement}`
147
+ : `The user asked to refine the plan: ${decision.refinement}`;
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: `${feedback}\nStay in plan mode, revise the plan (\`plan\` action \`write\`, reusing plan_path), then call \`plan\` action \`exit\` again.`,
153
+ },
154
+ ],
155
+ details: {
156
+ action: "exit",
157
+ approved: false,
158
+ plan: decision.plan.content,
159
+ planPath: decision.plan.path,
160
+ relativePath: decision.plan.relativePath,
161
+ refined: true,
162
+ refinement: decision.refinement,
163
+ annotations: decision.comments.length > 0 ? decision.comments : undefined,
164
+ } satisfies ExitPlanModeDetails,
165
+ };
166
+ }
167
+ if (decision.action === "keep") {
168
+ return {
169
+ content: [
170
+ { type: "text", text: "Plan not approved. Stay in plan mode, ask what to change, and revise the plan." },
171
+ ],
172
+ details: {
173
+ action: "exit",
174
+ approved: false,
175
+ plan: decision.plan.content,
176
+ planPath: decision.plan.path,
177
+ relativePath: decision.plan.relativePath,
178
+ } satisfies ExitPlanModeDetails,
179
+ };
180
+ }
181
+ if (decision.action === "cancel") {
182
+ runtime.disable(ctx);
183
+ const shown = decision.plan.relativePath;
184
+ ctx.ui.notify("Plan mode off — full access restored. The plan was not approved.", "info");
185
+ return {
186
+ content: [
187
+ {
188
+ type: "text",
189
+ text:
190
+ "Plan mode is off and write access is restored. The plan was not approved and no steps were recorded. " +
191
+ `The plan file ${shown} remains if the user wants to review it later.`,
192
+ },
193
+ ],
194
+ details: {
195
+ action: "exit",
196
+ approved: false,
197
+ cancelled: true,
198
+ plan: decision.plan.content,
199
+ planPath: decision.plan.path,
200
+ relativePath: decision.plan.relativePath,
201
+ } satisfies ExitPlanModeDetails,
202
+ };
203
+ }
214
204
 
215
- runtime.disable(ctx);
216
- // A plan refined and approved again seeds over the list its first approval made:
217
- // the steps that approval appended are the ones this one may replace.
218
- const previous = runtime.seeded(decision.plan.path);
219
- const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content, previous);
220
- if (error === undefined && steps.length > 0) {
221
- runtime.markSeeded(
222
- decision.plan.path,
223
- steps.map((step) => step.content),
224
- );
225
- }
226
- const goal = await finalizeGoal(pi, ctx, decision.plan);
227
- const listing =
228
- steps.length > 0
229
- ? `\n\n${steps
230
- .map((step, index) => `${index + 1}. [${step.status === "completed" ? "x" : " "}] ${step.content}`)
231
- .join("\n")}`
232
- : "";
233
- const tail =
234
- steps.length === 0
235
- ? " The plan has no steps to record."
236
- : error
237
- ? ` Could not record the steps in the todo list: ${error}. Keep these steps in mind and tick them off with ${SET_PLAN_STEPS_TOOL} as you go.`
238
- : merged
239
- ? mergeSummary(merged, steps)
240
- : ` Could not read the todo list. Record these steps with the todo tool before you start, and tick them off with ${SET_PLAN_STEPS_TOOL}.`;
241
- const verification = verificationNote(merged);
242
- const goalNote =
243
- goal.action === undefined
244
- ? ""
245
- : goal.error !== undefined
246
- ? ` Could not ${goal.action === "recorded" ? "record" : "update"} the session goal: ${goal.error}. Use the goal tool to set it.`
247
- : goal.action === "recorded"
248
- ? ` Recorded the session goal: ${goal.objective}.`
249
- : ` Set the session goal from the plan: ${goal.objective}.`;
250
- const browserNote = decision.comments.length
251
- ? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
252
- : "";
253
- return {
254
- content: [
255
- {
256
- type: "text",
257
- text: `Plan approved. Plan mode is off and write access is restored. The plan file ${decision.plan.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${verification}${goalNote}${listing}`,
258
- },
259
- ],
260
- details: {
261
- approved: true,
262
- plan: decision.plan.content,
263
- planPath: decision.plan.path,
264
- relativePath: decision.plan.relativePath,
265
- annotations: decision.comments.length > 0 ? decision.comments : undefined,
266
- } satisfies ExitPlanModeDetails,
267
- };
268
- },
205
+ runtime.disable(ctx);
206
+ // A plan refined and approved again seeds over the list its first approval made:
207
+ // the steps that approval appended are the ones this one may replace.
208
+ const previous = runtime.seeded(decision.plan.path);
209
+ const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content, previous);
210
+ if (error === undefined && steps.length > 0) {
211
+ runtime.markSeeded(
212
+ decision.plan.path,
213
+ steps.map((step) => step.content),
214
+ );
215
+ }
216
+ const goal = await finalizeGoal(pi, ctx, decision.plan);
217
+ const listing =
218
+ steps.length > 0
219
+ ? `\n\n${steps
220
+ .map((step, index) => `${index + 1}. [${step.status === "completed" ? "x" : " "}] ${step.content}`)
221
+ .join("\n")}`
222
+ : "";
223
+ const tick = TICK_CALL;
224
+ const tail =
225
+ steps.length === 0
226
+ ? " The plan has no steps to record."
227
+ : error
228
+ ? ` Could not record the steps in the todo list: ${error}. Keep these steps in mind and tick them off with ${tick} as you go.`
229
+ : merged
230
+ ? mergeSummary(merged, steps)
231
+ : ` Could not read the todo list. Record these steps with the todo tool before you start, and tick them off with ${tick}.`;
232
+ const verification = verificationNote(merged);
233
+ const goalNote =
234
+ goal.action === undefined
235
+ ? ""
236
+ : goal.error !== undefined
237
+ ? ` Could not ${goal.action === "recorded" ? "record" : "update"} the session goal: ${goal.error}. Use the goal tool to set it.`
238
+ : goal.action === "recorded"
239
+ ? ` Recorded the session goal: ${goal.objective}.`
240
+ : ` Set the session goal from the plan: ${goal.objective}.`;
241
+ const browserNote = decision.comments.length
242
+ ? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
243
+ : "";
244
+ return {
245
+ content: [
246
+ {
247
+ type: "text",
248
+ text: `Plan approved. Plan mode is off and write access is restored. The plan file ${decision.plan.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${verification}${goalNote}${listing}`,
249
+ },
250
+ ],
251
+ details: {
252
+ action: "exit",
253
+ approved: true,
254
+ plan: decision.plan.content,
255
+ planPath: decision.plan.path,
256
+ relativePath: decision.plan.relativePath,
257
+ annotations: decision.comments.length > 0 ? decision.comments : undefined,
258
+ } satisfies ExitPlanModeDetails,
259
+ };
260
+ }
269
261
 
270
- renderCall(_args, theme, context) {
271
- return callLine(theme, context?.lastComponent, EXIT_TOOL, "submitted a plan");
272
- },
262
+ export function callExit(args: ExitPlanModeArgs, theme: Theme, last: unknown): ToolCallText {
263
+ const path = args.plan_path?.trim() ?? "";
264
+ return callLine(theme, last, PLAN_TOOL, path === "" ? "exit" : `exit ${basename(path)}`);
265
+ }
273
266
 
274
- renderResult(result, { expanded }, theme, context) {
275
- const text = reuseText(context?.lastComponent);
276
- const outcome = detailsOrLine(theme, result, expanded, toolDetails<ExitPlanModeDetails>(result.details));
277
- if ("line" in outcome) {
278
- text.setText(outcome.line);
279
- return text;
280
- }
281
- const details = outcome.details;
282
- const heading = details.unavailable
283
- ? paint(theme, "warn", "No UI to approve the plan")
284
- : details.cancelled
285
- ? paint(theme, "warn", "Plan mode off — plan not approved")
286
- : details.approved
287
- ? paint(theme, "ok", `${GLYPHS.check} Plan approved`)
288
- : details.refined
289
- ? paint(theme, "accent", "Refining the plan")
290
- : paint(theme, "warn", "Plan not approved");
291
- const extra =
292
- details.refined && details.refinement
293
- ? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
294
- : "";
295
- const shown = details.relativePath ?? details.planPath;
296
- const path = shown ? `\n${paint(theme, "meta", shown)}` : "";
297
- const planText = sanitizePlanText(details.plan);
298
- let out = `${heading}${extra}${path}`;
299
- if (planText) {
300
- const { body, more } = expanded ? { body: planText, more: 0 } : preview(planText);
301
- // A blank line separates the header block from the plan body.
302
- out += `\n\n${theme.fg("toolOutput", body)}`;
303
- if (more > 0) out += `\n${moreHint(theme, more)}`;
304
- }
305
- text.setText(out);
306
- return text;
307
- },
308
- });
267
+ export function textExit(details: ExitPlanModeDetails, expanded: boolean, theme: Theme): string {
268
+ const heading = details.unavailable
269
+ ? paint(theme, "warn", "No UI to approve the plan")
270
+ : details.cancelled
271
+ ? paint(theme, "warn", "Plan mode off — plan not approved")
272
+ : details.approved
273
+ ? paint(theme, "ok", `${GLYPHS.check} Plan approved`)
274
+ : details.refined
275
+ ? paint(theme, "accent", "Refining the plan")
276
+ : paint(theme, "warn", "Plan not approved");
277
+ const extra =
278
+ details.refined && details.refinement
279
+ ? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
280
+ : "";
281
+ const shown = details.relativePath ?? details.planPath;
282
+ const path = shown ? `\n${paint(theme, "meta", shown)}` : "";
283
+ const planText = sanitizePlanText(details.plan);
284
+ let out = `${heading}${extra}${path}`;
285
+ if (planText) {
286
+ const { body, more } = expanded ? { body: planText, more: 0 } : preview(planText);
287
+ // A blank line separates the header block from the plan body.
288
+ out += `\n\n${paint(theme, "toolOutput", body)}`;
289
+ if (more > 0) out += `\n${moreHint(theme, more)}`;
290
+ }
291
+ return out;
309
292
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * One refusal for the folded `plan` tool.
3
+ *
4
+ * A rejected action says what it needed and carries the same words in `details`,
5
+ * so the transcript draws the reason rather than a bare rejected-call envelope.
6
+ * Every refusal happens before the first write, so a rejected call leaves the
7
+ * plan mode and the plan file exactly as they were.
8
+ */
9
+
10
+ import { failedResult, type ToolFailure } from "../../../lib/tool-result.ts";
11
+ import type { PlanAction, RefusedPlanDetails } from "../types.ts";
12
+
13
+ export function planRefusal<Action extends PlanAction>(
14
+ action: Action,
15
+ message: string,
16
+ ): ToolFailure<RefusedPlanDetails> {
17
+ return failedResult(`Error: ${message}`, { refused: action, error: message });
18
+ }