@gavin-hu/my-pi-agent 0.16.0 → 0.17.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 (135) hide show
  1. package/README.md +14 -8
  2. package/README.zh-CN.md +14 -9
  3. package/extensions/bytefly/README.md +174 -101
  4. package/extensions/bytefly/footer/collect/entries.ts +1 -6
  5. package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
  6. package/extensions/bytefly/footer/price/price.ts +12 -8
  7. package/extensions/bytefly/footer/register.ts +13 -1
  8. package/extensions/bytefly/footer/types.ts +11 -0
  9. package/extensions/bytefly/footer/unpriced.ts +36 -0
  10. package/extensions/bytefly/header/component.ts +15 -29
  11. package/extensions/bytefly/header/identity.ts +71 -0
  12. package/extensions/bytefly/header/register.ts +9 -3
  13. package/extensions/bytefly/index.ts +13 -5
  14. package/extensions/bytefly/message/format.ts +20 -13
  15. package/extensions/bytefly/message/register.ts +8 -7
  16. package/extensions/bytefly/panel/commands.ts +46 -25
  17. package/extensions/bytefly/panel/format.ts +49 -14
  18. package/extensions/bytefly/panel/join.ts +35 -9
  19. package/extensions/bytefly/panel/settings.ts +10 -27
  20. package/extensions/bytefly/panel/skills.ts +51 -0
  21. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
  22. package/extensions/bytefly/panel/tui/frame.ts +9 -1
  23. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  24. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  25. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  26. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  27. package/extensions/bytefly/panel/tui.ts +223 -331
  28. package/extensions/bytefly/panel/types.ts +26 -8
  29. package/extensions/bytefly/picker/discovery.ts +32 -27
  30. package/extensions/bytefly/picker/resources.ts +16 -8
  31. package/extensions/bytefly/picker/runtime.ts +62 -34
  32. package/extensions/bytefly/picker/toggle.ts +16 -10
  33. package/extensions/bytefly/picker/types.ts +31 -13
  34. package/extensions/bytefly/update/format.ts +66 -8
  35. package/extensions/bytefly/update/patch.ts +66 -0
  36. package/extensions/bytefly/update/register.ts +4 -11
  37. package/extensions/bytefly/update/release.ts +65 -0
  38. package/extensions/bytefly/update/state.ts +14 -0
  39. package/extensions/bytefly/update/suppress.ts +47 -29
  40. package/extensions/bytefly/update/types.ts +15 -5
  41. package/extensions/check/tui.ts +2 -0
  42. package/extensions/context/README.md +21 -11
  43. package/extensions/context/format.ts +10 -8
  44. package/extensions/context/report.ts +21 -8
  45. package/extensions/context/tui.ts +26 -10
  46. package/extensions/context/types.ts +5 -3
  47. package/extensions/file-browse/README.md +29 -1
  48. package/extensions/file-browse/client.ts +16 -463
  49. package/extensions/file-browse/html.ts +7 -5
  50. package/extensions/file-browse/palette.ts +224 -0
  51. package/extensions/file-browse/router.ts +2 -2
  52. package/extensions/file-browse/styles.ts +8 -3
  53. package/extensions/file-browse/tree.ts +381 -0
  54. package/extensions/file-read/README.md +4 -1
  55. package/extensions/file-read/extract/odt.ts +7 -5
  56. package/extensions/goal/tools.ts +6 -11
  57. package/extensions/job/index.ts +20 -45
  58. package/extensions/job/tools.ts +51 -77
  59. package/extensions/memory/tools.ts +4 -6
  60. package/extensions/memory/tui.ts +5 -1
  61. package/extensions/plan/README.md +88 -33
  62. package/extensions/plan/commands.ts +19 -4
  63. package/extensions/plan/config.ts +27 -0
  64. package/extensions/plan/plan-index.ts +3 -1
  65. package/extensions/plan/plans.ts +31 -13
  66. package/extensions/plan/review.ts +34 -14
  67. package/extensions/plan/runtime.ts +3 -1
  68. package/extensions/plan/steps.ts +43 -14
  69. package/extensions/plan/tools/cancel.ts +79 -0
  70. package/extensions/plan/tools/enter.ts +10 -10
  71. package/extensions/plan/tools/exit.ts +54 -43
  72. package/extensions/plan/tools/steps.ts +7 -19
  73. package/extensions/plan/tools/write.ts +22 -29
  74. package/extensions/plan/tools.ts +5 -2
  75. package/extensions/plan/tui.ts +9 -7
  76. package/extensions/plan/types.ts +8 -0
  77. package/extensions/plan/web-chrome.ts +101 -0
  78. package/extensions/plan/web-client.ts +98 -311
  79. package/extensions/plan/web-markdown.ts +222 -0
  80. package/extensions/plan/web-page.ts +28 -2
  81. package/extensions/plan/web-router.ts +2 -2
  82. package/extensions/rewind/checklist.ts +4 -0
  83. package/extensions/rewind/review-tui.ts +3 -1
  84. package/extensions/rewind/rewind-tui.ts +4 -0
  85. package/extensions/subagent/README.md +25 -12
  86. package/extensions/subagent/format.ts +72 -25
  87. package/extensions/subagent/index.ts +21 -47
  88. package/extensions/subagent/orchestrate.ts +18 -41
  89. package/extensions/subagent/render.ts +29 -42
  90. package/extensions/subagent/stream.ts +24 -18
  91. package/extensions/subagent/types.ts +7 -0
  92. package/extensions/todo/tools.ts +5 -6
  93. package/extensions/todo/tui.ts +5 -1
  94. package/extensions/watch/index.ts +10 -35
  95. package/extensions/watch/tools.ts +31 -59
  96. package/extensions/wechat/README.md +40 -18
  97. package/extensions/wechat/bridge.ts +38 -40
  98. package/extensions/wechat/client.ts +1 -4
  99. package/extensions/wechat/commands.ts +1 -1
  100. package/extensions/wechat/interaction.ts +25 -4
  101. package/extensions/wechat/login-screen.ts +2 -3
  102. package/extensions/wechat/notices.ts +7 -5
  103. package/extensions/wechat/prompt.ts +19 -4
  104. package/extensions/wechat/qrcode.ts +9 -9
  105. package/extensions/wechat/slash.ts +14 -0
  106. package/extensions/wechat/status-screen.ts +2 -2
  107. package/extensions/wechat/tools.ts +3 -11
  108. package/extensions/worktree/README.md +48 -12
  109. package/extensions/worktree/commands.ts +109 -65
  110. package/extensions/worktree/enter.ts +1 -1
  111. package/extensions/worktree/exit.ts +11 -1
  112. package/extensions/worktree/lifecycle.ts +9 -1
  113. package/extensions/worktree/screen.ts +263 -0
  114. package/extensions/worktree/status.ts +84 -38
  115. package/lib/README.md +7 -6
  116. package/lib/config.ts +12 -0
  117. package/lib/format.ts +6 -2
  118. package/lib/interaction.ts +18 -1
  119. package/lib/report.ts +75 -2
  120. package/lib/tool-result.ts +54 -4
  121. package/lib/tui/README.md +16 -8
  122. package/lib/tui/chrome.ts +27 -6
  123. package/lib/tui/dock-screen.ts +6 -2
  124. package/lib/tui/gauge.ts +9 -5
  125. package/lib/usage.ts +26 -0
  126. package/lib/web/README.md +131 -0
  127. package/lib/web/base.ts +19 -2
  128. package/lib/web/components.ts +10 -6
  129. package/lib/web/layout.ts +2 -0
  130. package/lib/web/markup.ts +2 -1
  131. package/lib/web/runtime.ts +103 -25
  132. package/lib/web/tokens.ts +23 -7
  133. package/package.json +3 -1
  134. package/skills/changelog/SKILL.md +57 -45
  135. package/skills/next/SKILL.md +184 -0
@@ -18,6 +18,8 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
18
18
  import { GLYPHS } from "../../../lib/tui/glyphs.ts";
19
19
  import { paint } from "../../../lib/tui/tokens.ts";
20
20
  import { callLine, detailsOrLine, moreHint } from "../../../lib/tui/tool.ts";
21
+ import { failedResult } from "../../../lib/tool-result.ts";
22
+ import { loadConfig } from "../config.ts";
21
23
  import { feedbackSummary } from "../feedback.ts";
22
24
  import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
23
25
  import { sanitizePlanText } from "../plan-text.ts";
@@ -51,7 +53,8 @@ interface GoalOutcome {
51
53
  * Finalise the session goal from the approved plan.
52
54
  *
53
55
  * A `planning` goal needs no dialog — approving the plan *is* the decision — and a
54
- * session with no goal at all gets one confirm. A final goal (`active` or
56
+ * session with no goal at all gets one confirm, unless `plan.goalPrompt` is off,
57
+ * which records the objective without asking. A final goal (`active` or
55
58
  * `achieved`) is the user's own: approving ordinary work never rewrites it.
56
59
  * Recording runs through the goal tool, so validation and the widget stay goal's,
57
60
  * and no model turn is spent.
@@ -62,8 +65,10 @@ async function finalizeGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promis
62
65
  if (current !== null && current.status !== "planning") return { objective };
63
66
  if (current === null) {
64
67
  if (!ctx.hasUI) return { objective };
65
- const approved = await ctx.ui.confirm("Record this plan as the session goal?", objective);
66
- if (!approved) return { objective };
68
+ if (loadConfig(ctx.cwd).goalPrompt) {
69
+ const approved = await ctx.ui.confirm("Record this plan as the session goal?", objective);
70
+ if (!approved) return { objective };
71
+ }
67
72
  }
68
73
  const outcome = await ctx.executeTool(GOAL_TOOL, { objective, status: "active" });
69
74
  const action = current === null ? "recorded" : "promoted";
@@ -99,65 +104,47 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
99
104
  const planPath = (params as ExitPlanModeArgs).plan_path.trim();
100
105
 
101
106
  if (!runtime.isEnabled()) {
102
- return {
103
- content: [{ type: "text", text: "Not in plan mode; there is no plan to approve." }],
104
- details: { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
105
- isError: true,
106
- };
107
+ return failedResult("Not in plan mode; there is no plan to approve.", {
108
+ approved: false,
109
+ plan: "",
110
+ planPath,
111
+ } satisfies ExitPlanModeDetails);
107
112
  }
108
113
 
109
114
  const file = await runtime.plans.read(ctx.cwd, planPath);
110
115
  if (!file) {
111
- return {
112
- content: [
113
- {
114
- type: "text",
115
- text: `No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
116
- },
117
- ],
118
- details: { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
119
- isError: true,
120
- };
116
+ return failedResult(
117
+ `No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
118
+ { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
119
+ );
121
120
  }
122
121
 
123
122
  const total = planStepSummary(file.content).total;
124
123
  if (total > MAX_STEPS) {
125
124
  // A hand-edited or older plan file can still be over the cap; refuse it here
126
125
  // too, so approval never seeds a list it cannot hold in full.
127
- return {
128
- content: [
129
- {
130
- type: "text",
131
- text: `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
132
- },
133
- ],
134
- details: {
126
+ return failedResult(
127
+ `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
128
+ {
135
129
  approved: false,
136
130
  plan: file.content,
137
131
  planPath: file.path,
138
132
  relativePath: file.relativePath,
139
133
  } satisfies ExitPlanModeDetails,
140
- isError: true,
141
- };
134
+ );
142
135
  }
143
136
 
144
137
  if (!ctx.hasUI) {
145
- return {
146
- content: [
147
- {
148
- type: "text",
149
- text: `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.`,
150
- },
151
- ],
152
- details: {
138
+ return failedResult(
139
+ `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.`,
140
+ {
153
141
  approved: false,
154
142
  plan: file.content,
155
143
  planPath: file.path,
156
144
  relativePath: file.relativePath,
157
145
  unavailable: true,
158
146
  } satisfies ExitPlanModeDetails,
159
- isError: true,
160
- };
147
+ );
161
148
  }
162
149
 
163
150
  // One decision, however the session is attached; `review.ts` owns the loop,
@@ -208,6 +195,28 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
208
195
  } satisfies ExitPlanModeDetails,
209
196
  };
210
197
  }
198
+ if (decision.action === "cancel") {
199
+ runtime.disable(ctx);
200
+ const shown = decision.plan.relativePath;
201
+ ctx.ui.notify("Plan mode off — full access restored. The plan was not approved.", "info");
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text:
207
+ "Plan mode is off and write access is restored. The plan was not approved and no steps were recorded. " +
208
+ `The plan file ${shown} remains if the user wants to review it later.`,
209
+ },
210
+ ],
211
+ details: {
212
+ approved: false,
213
+ cancelled: true,
214
+ plan: decision.plan.content,
215
+ planPath: decision.plan.path,
216
+ relativePath: decision.plan.relativePath,
217
+ } satisfies ExitPlanModeDetails,
218
+ };
219
+ }
211
220
 
212
221
  runtime.disable(ctx);
213
222
  const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
@@ -269,11 +278,13 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
269
278
  const details = outcome.details;
270
279
  const heading = details.unavailable
271
280
  ? paint(theme, "warn", "No UI to approve the plan")
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");
281
+ : details.cancelled
282
+ ? paint(theme, "warn", "Plan mode off — plan not approved")
283
+ : details.approved
284
+ ? paint(theme, "ok", `${GLYPHS.check} Plan approved`)
285
+ : details.refined
286
+ ? paint(theme, "accent", "Refining the plan")
287
+ : paint(theme, "warn", "Plan not approved");
277
288
  const extra =
278
289
  details.refined && details.refinement
279
290
  ? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
@@ -13,6 +13,7 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
13
13
  import { GLYPHS } from "../../../lib/tui/glyphs.ts";
14
14
  import { paint } from "../../../lib/tui/tokens.ts";
15
15
  import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
16
+ import { failedResult, textResult } from "../../../lib/tool-result.ts";
16
17
  import { describeUpdates, stepLabel } from "../format.ts";
17
18
  import { SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
18
19
  import { extractPlanSteps } from "../steps.ts";
@@ -66,39 +67,26 @@ export function registerSetPlanStepsTool(pi: ExtensionAPI, runtime: PlanRuntime)
66
67
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
67
68
  const args = params as SetPlanStepsArgs;
68
69
  if (runtime.isEnabled()) {
69
- return {
70
- content: [{ type: "text", text: "Not while planning; refine the plan with write_plan instead." }],
71
- details: undefined,
72
- isError: true,
73
- };
70
+ return failedResult("Not while planning; refine the plan with write_plan instead.", undefined);
74
71
  }
75
72
  try {
76
73
  const plan = await runtime.plans.setSteps(ctx.cwd, args.plan_path, args.updates);
77
74
  const steps = extractPlanSteps(plan.content);
78
75
  const done = steps.filter((step) => step.status === "completed").length;
79
76
  const todoNote = await mirrorTodos(ctx, args.updates);
80
- return {
81
- content: [
82
- {
83
- type: "text",
84
- text: `${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}${todoNote}.`,
85
- },
86
- ],
87
- details: {
77
+ return textResult(
78
+ `${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}${todoNote}.`,
79
+ {
88
80
  path: plan.path,
89
81
  relativePath: plan.relativePath,
90
82
  updates: args.updates,
91
83
  done,
92
84
  total: steps.length,
93
85
  } satisfies SetPlanStepsDetails,
94
- };
86
+ );
95
87
  } catch (error) {
96
88
  const message = error instanceof Error ? error.message : String(error);
97
- return {
98
- content: [{ type: "text", text: `Could not update the plan: ${message}` }],
99
- details: undefined,
100
- isError: true,
101
- };
89
+ return failedResult(`Could not update the plan: ${message}`, undefined);
102
90
  }
103
91
  },
104
92
 
@@ -12,6 +12,7 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
12
12
  import { GLYPHS } from "../../../lib/tui/glyphs.ts";
13
13
  import { paint } from "../../../lib/tui/tokens.ts";
14
14
  import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
15
+ import { failedResult, textResult } from "../../../lib/tool-result.ts";
15
16
  import { overCap, stepElisionNote } from "../format.ts";
16
17
  import { WRITE_PLAN_TOOL, type PlanRuntime } from "../runtime.ts";
17
18
  import { MAX_STEPS, PLAN_OUTLINE, planStepSummary } from "../steps.ts";
@@ -42,11 +43,22 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
42
43
  "Save the plan you designed as a markdown file under .pi/plans (project root, or the agent directory outside a " +
43
44
  "repository). Write the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
44
45
  `${PLAN_OUTLINE}\n\n` +
46
+ "Each section has one job: lead it with its outcome, keep a prose section to a few bullets or two or three " +
47
+ "sentences, give each step one verifiable outcome with the detail in a nested note, and put the sketch, files, " +
48
+ "and commands in inline code or a fence rather than pasted prose. " +
49
+ "When the change alters a surface that already exists, Visual shows it before and after in one fence each; " +
50
+ "tag a fence only when every line in it is code. " +
51
+ "The outline is a guide, not a form: drop Context, Approach, Visual, or Risks when there is nothing to say. " +
52
+ "Keep Steps and Verification — the steps are read from that section (without it, the shallowest list " +
53
+ "anywhere becomes the steps), and Verification becomes the last todo item. " +
54
+ "In Steps, each top-level bullet is one outcome in the order you will do them, and deeper bullets are notes. " +
45
55
  "Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining. " +
46
56
  `A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it cannot track.`,
47
57
  promptSnippet: "Save the designed plan as a markdown file under .pi/plans.",
48
58
  promptGuidelines: [
49
59
  "While plan mode is active, write the full plan with write_plan, then call exit_plan_mode with the returned path.",
60
+ "When refining with plan_path, keep every section heading as it was: the review marks changed sections by heading, " +
61
+ "so a rename reads as an edit.",
50
62
  ],
51
63
  parameters: WritePlanParams,
52
64
  defaultActive: false,
@@ -57,27 +69,17 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
57
69
 
58
70
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
59
71
  if (!runtime.isEnabled()) {
60
- return {
61
- content: [{ type: "text", text: "Not in plan mode; call enter_plan_mode first." }],
62
- details: undefined,
63
- isError: true,
64
- };
72
+ return failedResult("Not in plan mode; call enter_plan_mode first.", undefined);
65
73
  }
66
74
  const args = params as WritePlanArgs;
67
75
  const summary = planStepSummary(args.content);
68
76
  if (summary.total > MAX_STEPS) {
69
77
  // Refuse before writing: a note after the fact arrives once the file exists and
70
78
  // the review has listed every step, so the overflow is already lost.
71
- return {
72
- content: [
73
- {
74
- type: "text",
75
- text: `${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
76
- },
77
- ],
78
- details: undefined,
79
- isError: true,
80
- };
79
+ return failedResult(
80
+ `${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
81
+ undefined,
82
+ );
81
83
  }
82
84
  try {
83
85
  const file = await runtime.plans.write(ctx.cwd, {
@@ -86,22 +88,13 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
86
88
  planPath: args.plan_path,
87
89
  });
88
90
  runtime.setLastPlan(ctx, file.path);
89
- return {
90
- content: [
91
- {
92
- type: "text",
93
- text: `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
94
- },
95
- ],
96
- details: { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
97
- };
91
+ return textResult(
92
+ `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
93
+ { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
94
+ );
98
95
  } catch (error) {
99
96
  const message = error instanceof Error ? error.message : String(error);
100
- return {
101
- content: [{ type: "text", text: `Could not write the plan: ${message}` }],
102
- details: undefined,
103
- isError: true,
104
- };
97
+ return failedResult(`Could not write the plan: ${message}`, undefined);
105
98
  }
106
99
  },
107
100
 
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * `enter_plan_mode` lets the model ask to plan before touching code; it needs
5
5
  * user confirmation. `write_plan` saves the plan artifact under `.pi/plans` and
6
- * returns its path. `exit_plan_mode` reads that file back, presents it for
7
- * approval (a scrollable review screen in the TUI, a select dialog elsewhere),
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),
8
9
  * and on approval seeds the `todo` tool with the plan's steps (through
9
10
  * `ctx.executeTool`, so the todo widget and validation run normally).
10
11
  * `set_plan_steps` then keeps the plan current: after approval the model ticks
@@ -22,6 +23,7 @@
22
23
 
23
24
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
24
25
  import type { PlanRuntime } from "./runtime.ts";
26
+ import { registerCancelTool } from "./tools/cancel.ts";
25
27
  import { registerEnterTool } from "./tools/enter.ts";
26
28
  import { registerExitTool } from "./tools/exit.ts";
27
29
  import { registerSetPlanStepsTool } from "./tools/steps.ts";
@@ -34,6 +36,7 @@ export function registerTools(
34
36
  web: PlanWebReview = createPlanWebReview(runtime.plans),
35
37
  ): void {
36
38
  registerEnterTool(pi, runtime);
39
+ registerCancelTool(pi, runtime);
37
40
  registerWriteTool(pi, runtime);
38
41
  registerExitTool(pi, runtime, web);
39
42
  registerSetPlanStepsTool(pi, runtime);
@@ -37,7 +37,7 @@ import {
37
37
  import { isCloseKey } from "../../lib/tui/cursor.ts";
38
38
  import { gaugeBar } from "../../lib/tui/gauge.ts";
39
39
  import { GLYPHS } from "../../lib/tui/glyphs.ts";
40
- import { titleRule } from "../../lib/tui/rules.ts";
40
+ import { plainRule, titleRule } from "../../lib/tui/rules.ts";
41
41
  import { emptyState, checkbox } from "../../lib/tui/rows.ts";
42
42
  import { paint } from "../../lib/tui/tokens.ts";
43
43
  import { planTitle, type StoredPlan } from "./plans.ts";
@@ -45,7 +45,7 @@ import { changedSections, sanitizePlanText, type SectionDigest } from "./plan-te
45
45
  import { extractPlanSections } from "./steps.ts";
46
46
 
47
47
  /** What the user chose from the read/review screen. */
48
- export type PlanViewAction = "approve" | "refine" | "keep" | "browser";
48
+ export type PlanViewAction = "approve" | "refine" | "keep" | "browser" | "cancel";
49
49
 
50
50
  /** A plan section's rendered start line, for navigation and the current-section label. */
51
51
  interface SectionOffset {
@@ -87,7 +87,7 @@ export interface PlanViewOptions {
87
87
 
88
88
  /** Rows the screen shows when the terminal height is unknown. */
89
89
  const DEFAULT_ROWS = 20;
90
- /** Header, blanks, footer, and trailing rows around a non-empty plan. */
90
+ /** Header, blanks, footer, and closing rule around a non-empty plan. */
91
91
  const CHROME_ROWS = 5;
92
92
  /** Narrowest width where Markdown's 2-column padding still fits the body. */
93
93
  const MIN_PADDED_WIDTH = 6;
@@ -97,13 +97,14 @@ const BODY_PADDING = 2;
97
97
  const GAUGE_SEGMENTS = 10;
98
98
  /** Footer hints shared by the review and the inline editor's primary actions. */
99
99
  const PRIMARY_HINTS: ScreenHint[] = [
100
- "Esc keep planning",
101
- { text: "a approve & run", color: "success" },
100
+ "Esc keep plan",
101
+ { text: "a approve", color: "success" },
102
102
  { text: "r refine", color: "accent" },
103
103
  { text: "w browser", color: "accent" },
104
+ { text: "x exit plan", color: "warning" },
104
105
  ];
105
106
  /** Scroll keys appended to the review footer while the plan overflows. */
106
- const SCROLL_HINTS: ScreenHint[] = ["↑/↓ or k/j scroll", "space/b page", "g/G ends"];
107
+ const SCROLL_HINTS: ScreenHint[] = ["j/k scroll", "space page", "g/G ends"];
107
108
  /** Section-jump hint, shown when the plan has more than one heading section. */
108
109
  const SECTION_HINTS: ScreenHint[] = ["n/p section"];
109
110
 
@@ -332,6 +333,7 @@ export class PlanViewComponent implements Component {
332
333
  else if (data === "a") this.options.onClose("approve");
333
334
  else if (data === "r") this.startRefine();
334
335
  else if (data === "w") this.options.onClose("browser");
336
+ else if (data === "x") this.options.onClose("cancel");
335
337
  }
336
338
 
337
339
  /** Wheel scrolling in fullscreen; regular mode leaves the wheel to the terminal. */
@@ -403,7 +405,7 @@ export class PlanViewComponent implements Component {
403
405
  this.maxOffset > 0 ? [...PRIMARY_HINTS, ...SCROLL_HINTS, ...sectionHints] : PRIMARY_HINTS;
404
406
  lines.push(screenHint(theme, w, hints));
405
407
  }
406
- lines.push("");
408
+ lines.push(plainRule(theme, w));
407
409
  return lines;
408
410
  }
409
411
  }
@@ -74,6 +74,14 @@ export interface ExitPlanModeDetails {
74
74
  refinement?: string;
75
75
  /** Comments the review carried when it approved or requested a refinement. */
76
76
  annotations?: PlanAnnotation[];
77
+ /** True when the user left plan mode without approving the plan. */
78
+ cancelled?: boolean;
77
79
  /** True when no interactive UI was available to ask for approval. */
78
80
  unavailable?: boolean;
79
81
  }
82
+
83
+ /** Structured result carried in `cancel_plan_mode`'s `details`. */
84
+ export interface CancelPlanModeDetails {
85
+ /** True when plan mode was on and has now been turned off. */
86
+ cancelled: boolean;
87
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The review page's chrome: what the page says about itself.
3
+ *
4
+ * It is a fragment of the same IIFE `web-client.ts` ships (`WEB_CHROME_JS` is
5
+ * interpolated into `REVIEW_CLIENT_JS`), kept in its own module so the state
6
+ * chip, the status line, and the header are read together and away from the
7
+ * document rendering. It paints only text it was handed.
8
+ */
9
+
10
+ export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
11
+ if (!statusEl) return;
12
+ statusEl.textContent = text || "";
13
+ statusEl.title = text || "";
14
+ statusEl.className = kind ? "bf-status bf-status--" + kind : "bf-status";
15
+ }
16
+
17
+ // The always-visible header chip: connection, comment count, and whether
18
+ // there are edits the file does not have yet.
19
+ function updateState() {
20
+ if (!stateEl) return;
21
+ var label;
22
+ var tone = "";
23
+ if (approved) {
24
+ label = "Approved";
25
+ tone = "success";
26
+ } else if (saved) {
27
+ label = "Saved";
28
+ tone = "success";
29
+ } else if (closed) {
30
+ // Settled by the user, not by a dropped stream: never flash Offline.
31
+ label = "Closed";
32
+ } else if (connected === true) {
33
+ label = "Connected";
34
+ tone = "success";
35
+ } else if (everConnected) {
36
+ // Only after the stream has opened once: an error before that is a
37
+ // transient initial connect, so keep showing Connecting rather than Offline.
38
+ label = "Offline";
39
+ tone = "danger";
40
+ } else {
41
+ label = "Connecting\u2026";
42
+ }
43
+ var count = annotations.length;
44
+ label += " \u00b7 " + count + (count === 1 ? " comment" : " comments");
45
+ if (dirty && !saved && !approved && !closed) {
46
+ label += " \u00b7 unsaved";
47
+ if (!tone) tone = "warn";
48
+ }
49
+ stateEl.textContent = label;
50
+ stateEl.className = "bf-chip bf-chip--accent review-state" + (tone ? " bf-chip--" + tone : "");
51
+ }
52
+
53
+ function renderHeader() {
54
+ var titleEl = document.getElementById("plan-title");
55
+ if (titleEl && data.title) {
56
+ titleEl.textContent = data.title;
57
+ titleEl.title = data.title;
58
+ }
59
+ if (data.title) document.title = "Plan review \u00b7 " + data.title;
60
+ }
61
+
62
+ function updateProgress() {
63
+ if (!progressEl) return;
64
+ var doc = document.scrollingElement || document.documentElement;
65
+ var top = doc ? doc.scrollTop : 0;
66
+ var height = doc ? doc.scrollHeight : 0;
67
+ var view = doc ? doc.clientHeight : 0;
68
+ if (planEl && planEl.scrollHeight > planEl.clientHeight + 4) {
69
+ top = planEl.scrollTop; height = planEl.scrollHeight; view = planEl.clientHeight;
70
+ }
71
+ var max = height - view;
72
+ var percent = max > 0 ? Math.min(100, Math.max(0, (top / max) * 100)) : 0;
73
+ progressEl.style.width = percent + "%";
74
+ }
75
+
76
+ function lockActions() {
77
+ if (approveButton) approveButton.disabled = true;
78
+ if (saveButton) saveButton.disabled = true;
79
+ if (cancelButton) cancelButton.disabled = true;
80
+ }
81
+
82
+ // A conflict means the file moved under the page; only a reload can re-anchor.
83
+ function offerReload() {
84
+ if (document.getElementById("reload")) return;
85
+ var controls = document.querySelector(".review-controls");
86
+ if (!controls) return;
87
+ var reload = el("button");
88
+ reload.type = "button";
89
+ reload.id = "reload";
90
+ reload.className = "bf-btn bf-btn--primary";
91
+ reload.textContent = "Reload";
92
+ reload.addEventListener("click", function () { window.location.reload(); });
93
+ controls.appendChild(reload);
94
+ }
95
+
96
+ // The stream exists only to notice a closed tab. Once the review is settled it
97
+ // has no job, and leaving it open retries a dead port until the tab closes.
98
+ function settleStream() {
99
+ if (stream && stream.close) { stream.close(); stream = null; }
100
+ }
101
+ `;