@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
@@ -10,6 +10,7 @@
10
10
  import type { JsonValue } from "@earendil-works/pi-ai";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { GOAL_TOOL } from "../../lib/tool-names.ts";
13
+ import { failedResult, textResult } from "../../lib/tool-result.ts";
13
14
  import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
14
15
  import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
15
16
  import { paint } from "../../lib/tui/tokens.ts";
@@ -52,17 +53,11 @@ function toStructuredContent(details: GoalDetails): Record<string, JsonValue> {
52
53
  */
53
54
  function goalResult(details: GoalDetails, options: { error?: string; openTodos?: number } = {}) {
54
55
  const { error, openTodos = 0 } = options;
55
- return {
56
- content: [
57
- {
58
- type: "text" as const,
59
- text: error === undefined ? formatGoalText(details.goal, openTodos) : `Error: ${error}`,
60
- },
61
- ],
62
- details,
63
- structuredContent: toStructuredContent(details),
64
- ...(error === undefined ? {} : { isError: true as const }),
65
- };
56
+ const text = error === undefined ? formatGoalText(details.goal, openTodos) : `Error: ${error}`;
57
+ const structuredContent = toStructuredContent(details);
58
+ return error === undefined
59
+ ? textResult(text, details, structuredContent)
60
+ : failedResult(text, details, structuredContent);
66
61
  }
67
62
 
68
63
  export function registerTools(pi: ExtensionAPI, runtime: GoalRuntime): void {
@@ -14,7 +14,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
14
  import { isExtensionEnabled } from "../../lib/env.ts";
15
15
  import { JOB_FINISHED_CHANNEL, type JobFinishedEvent } from "../../lib/events.ts";
16
16
  import { noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
17
- import { createPendingReporter } from "../../lib/report.ts";
17
+ import { createPendingReporter, registerPendingChannel } from "../../lib/report.ts";
18
18
  import { registerCommands } from "./commands.ts";
19
19
  import { formatCompletion, jobOutcome, shortLabel } from "./format.ts";
20
20
  import { createJobsRuntime, type JobsRuntime } from "./runtime.ts";
@@ -80,51 +80,26 @@ export default function jobs(pi: ExtensionAPI, deps: JobsDeps = {}): void {
80
80
  registerTools(pi, runtime);
81
81
  registerCommands(pi, runtime);
82
82
 
83
- pi.on("session_start", (_event, ctx) => {
84
- reporter.sessionStart();
85
- runtime.load(ctx);
83
+ registerPendingChannel(pi, {
84
+ reporter,
85
+ runtime,
86
86
  // `shutdown` clears these; a reload or session replacement reuses the
87
87
  // runtime, so re-arm them on every session start.
88
- runtime.onFinish = () => reporter.report();
89
- // Notify-listening extensions follow completions on the shared bus; the
90
- // runtime only knows the callback, never the channel.
91
- runtime.onComplete = (job) => {
92
- try {
93
- pi.events.emit(JOB_FINISHED_CHANNEL, toFinishedEvent(job));
94
- } catch {
95
- // The event bus is optional.
96
- }
97
- };
98
- });
99
-
100
- // Tree navigation does not change the process table; only repaint.
101
- pi.on("session_tree", (_event, ctx) => {
102
- runtime.setStatus(ctx);
103
- });
104
-
105
- pi.on("agent_start", () => {
106
- reporter.agentStart();
107
- });
108
- pi.on("agent_settled", () => {
109
- reporter.agentSettled();
110
- });
111
-
112
- // Report completions to the model at the start of the next turn.
113
- pi.on("before_agent_start", (_event, ctx) => {
114
- const reported = reporter.beforeAgentStart();
115
- if (!reported) return undefined;
116
- runtime.setStatus(ctx);
117
- return reported;
118
- });
119
-
120
- // Keep stale completion notices out of later turns. Only this extension's
121
- // notices are pruned; another source's are left for its own handler.
122
- pi.on("context", (event) => reporter.filterContext(event.messages));
123
-
124
- // `shutdown` detaches the UI controller, which clears both chips.
125
- pi.on("session_shutdown", async () => {
126
- runtime.onFinish = undefined;
127
- runtime.onComplete = undefined;
128
- await runtime.shutdown();
88
+ onSessionStart: () => {
89
+ runtime.onFinish = () => reporter.report();
90
+ // Notify-listening extensions follow completions on the shared bus; the
91
+ // runtime only knows the callback, never the channel.
92
+ runtime.onComplete = (job) => {
93
+ try {
94
+ pi.events.emit(JOB_FINISHED_CHANNEL, toFinishedEvent(job));
95
+ } catch {
96
+ // The event bus is optional.
97
+ }
98
+ };
99
+ },
100
+ onSessionShutdown: () => {
101
+ runtime.onFinish = undefined;
102
+ runtime.onComplete = undefined;
103
+ },
129
104
  });
130
105
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
- import { errorResult } from "../../lib/tool-result.ts";
12
+ import { errorResult, textResult } from "../../lib/tool-result.ts";
13
13
  import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
14
14
  import { callLine, detailsOrLine, elapsedSuffix, rejectionLine, syncElapsedTimer } from "../../lib/tui/tool.ts";
15
15
  import { SEPARATORS } from "../../lib/tui/glyphs.ts";
@@ -77,46 +77,40 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
77
77
  },
78
78
  ctx,
79
79
  );
80
- return {
81
- content: [
82
- {
83
- type: "text" as const,
84
- text: `Started ${job.id} (pid ${job.pid ?? "?"}): ${job.command}\nPoll with \`job status ${job.id}\`, or \`job wait ${job.id}\`.`,
85
- },
86
- ],
87
- details: { action: "start", job: toRecord(job) } satisfies JobDetails,
88
- };
80
+ return textResult(
81
+ `Started ${job.id} (pid ${job.pid ?? "?"}): ${job.command}\nPoll with \`job status ${job.id}\`, or \`job wait ${job.id}\`.`,
82
+ { action: "start", job: toRecord(job) } satisfies JobDetails,
83
+ );
89
84
  }
90
85
 
91
86
  case "list": {
92
87
  const jobs = runtime.list();
93
- return {
94
- content: [{ type: "text" as const, text: formatJobList(jobs, Date.now(), runtime.sessionId) }],
95
- details: { action: "list", jobs, sessionId: runtime.sessionId } satisfies JobDetails,
96
- };
88
+ return textResult(formatJobList(jobs, Date.now(), runtime.sessionId), {
89
+ action: "list",
90
+ jobs,
91
+ sessionId: runtime.sessionId,
92
+ } satisfies JobDetails);
97
93
  }
98
94
 
99
95
  case "status": {
100
96
  const job = runtime.get(call.id);
101
97
  if (!job) return errorResult("status", `no job "${call.id}".`);
102
- return {
103
- content: [{ type: "text" as const, text: formatJobStatus(job, Date.now(), runtime.sessionId) }],
104
- details: { action: "status", job: toRecord(job), sessionId: runtime.sessionId } satisfies JobDetails,
105
- };
98
+ return textResult(formatJobStatus(job, Date.now(), runtime.sessionId), {
99
+ action: "status",
100
+ job: toRecord(job),
101
+ sessionId: runtime.sessionId,
102
+ } satisfies JobDetails);
106
103
  }
107
104
 
108
105
  case "logs": {
109
106
  const result = runtime.logs(call.id, call.lines);
110
107
  if (!result) return errorResult("logs", `no job "${call.id}".`);
111
- return {
112
- content: [{ type: "text" as const, text: result.text }],
113
- details: {
114
- action: "logs",
115
- job: toRecord(result.job),
116
- logs: result.lines.join("\n"),
117
- truncated: result.truncated,
118
- } satisfies JobDetails,
119
- };
108
+ return textResult(result.text, {
109
+ action: "logs",
110
+ job: toRecord(result.job),
111
+ logs: result.lines.join("\n"),
112
+ truncated: result.truncated,
113
+ } satisfies JobDetails);
120
114
  }
121
115
 
122
116
  case "send": {
@@ -132,15 +126,12 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
132
126
  result.closed ? " and closed its stdin" : ""
133
127
  }.`
134
128
  : `Closed ${job.id}'s stdin.`;
135
- return {
136
- content: [{ type: "text" as const, text }],
137
- details: {
138
- action: "send",
139
- job: toRecord(job),
140
- sent: result.sent,
141
- stdinClosed: result.closed,
142
- } satisfies JobDetails,
143
- };
129
+ return textResult(text, {
130
+ action: "send",
131
+ job: toRecord(job),
132
+ sent: result.sent,
133
+ stdinClosed: result.closed,
134
+ } satisfies JobDetails);
144
135
  }
145
136
 
146
137
  case "kill": {
@@ -149,30 +140,27 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
149
140
  const job = runtime.kill(call.id, call.signal);
150
141
  if (!job) return errorResult("kill", `no job "${call.id}".`);
151
142
  const wasRunning = before.status === "running";
152
- return {
153
- content: [
154
- {
155
- type: "text" as const,
156
- text: wasRunning
157
- ? `Sent ${call.signal ?? "SIGTERM"} to ${job.id} (${job.label}).`
158
- : `${job.id} already ${job.status}; nothing to kill.`,
159
- },
160
- ],
161
- details: {
143
+ return textResult(
144
+ wasRunning
145
+ ? `Sent ${call.signal ?? "SIGTERM"} to ${job.id} (${job.label}).`
146
+ : `${job.id} already ${job.status}; nothing to kill.`,
147
+ {
162
148
  action: "kill",
163
149
  job: toRecord(job),
164
150
  signalled: wasRunning,
165
151
  signal: call.signal ?? "SIGTERM",
166
152
  } satisfies JobDetails,
167
- };
153
+ );
168
154
  }
169
155
 
170
156
  case "wait": {
171
157
  const result = await runtime.wait(call.id, call.timeoutMs, signal, () =>
172
- onUpdate?.({
173
- content: [{ type: "text" as const, text: `Waiting on ${call.id}…` }],
174
- details: { action: "wait", job: runtime.get(call.id) } satisfies JobDetails,
175
- }),
158
+ onUpdate?.(
159
+ textResult(`Waiting on ${call.id}…`, {
160
+ action: "wait",
161
+ job: runtime.get(call.id),
162
+ } satisfies JobDetails),
163
+ ),
176
164
  );
177
165
  if (!result) return errorResult("wait", `no job "${call.id}".`);
178
166
  const { job, timedOut, cancelled } = result;
@@ -181,37 +169,23 @@ export function registerTools(pi: ExtensionAPI, runtime: JobsRuntime): void {
181
169
  : timedOut
182
170
  ? " (still running after the timeout)"
183
171
  : "";
184
- return {
185
- content: [
186
- {
187
- type: "text" as const,
188
- text: `${formatCompletion(job)}${note}${job.lastLine ? `\nlast: ${job.lastLine}` : ""}`,
189
- },
190
- ],
191
- details: {
192
- action: "wait",
193
- job: toRecord(job),
194
- timedOut,
195
- cancelled,
196
- } satisfies JobDetails,
197
- };
172
+ return textResult(`${formatCompletion(job)}${note}${job.lastLine ? `\nlast: ${job.lastLine}` : ""}`, {
173
+ action: "wait",
174
+ job: toRecord(job),
175
+ timedOut,
176
+ cancelled,
177
+ } satisfies JobDetails);
198
178
  }
199
179
 
200
180
  case "clear": {
201
181
  const result = runtime.clear(call.id, call.all ?? false);
202
182
  if (result.refused) return errorResult("clear", result.refused);
203
- return {
204
- content: [
205
- {
206
- type: "text" as const,
207
- text:
208
- result.cleared === 0
209
- ? "Nothing to clear."
210
- : `Cleared ${result.cleared} job${result.cleared === 1 ? "" : "s"}.`,
211
- },
212
- ],
213
- details: { action: "clear", cleared: result.cleared, jobs: runtime.list() } satisfies JobDetails,
214
- };
183
+ return textResult(
184
+ result.cleared === 0
185
+ ? "Nothing to clear."
186
+ : `Cleared ${result.cleared} job${result.cleared === 1 ? "" : "s"}.`,
187
+ { action: "clear", cleared: result.cleared, jobs: runtime.list() } satisfies JobDetails,
188
+ );
215
189
  }
216
190
  }
217
191
  } catch (error) {
@@ -11,6 +11,7 @@
11
11
  import type { JsonValue } from "@earendil-works/pi-ai";
12
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
14
+ import { failedResult, textResult } from "../../lib/tool-result.ts";
14
15
  import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
15
16
  import { messageOf } from "./errors.ts";
16
17
  import { formatCallText, formatResultText, scopeLabel } from "./format.ts";
@@ -49,12 +50,9 @@ function toStructuredContent(details: MemoryDetails): Record<string, JsonValue>
49
50
  */
50
51
  function memoryResult(details: MemoryDetails, error?: string) {
51
52
  const full: MemoryDetails = error === undefined ? details : { ...details, error };
52
- return {
53
- content: [{ type: "text" as const, text: formatResultText(full) }],
54
- details: full,
55
- structuredContent: toStructuredContent(full),
56
- ...(error === undefined ? {} : { isError: true as const }),
57
- };
53
+ const text = formatResultText(full);
54
+ const structuredContent = toStructuredContent(full);
55
+ return error === undefined ? textResult(text, full, structuredContent) : failedResult(text, full, structuredContent);
58
56
  }
59
57
 
60
58
  export function registerTools(pi: ExtensionAPI, runtime: MemoryRuntime): void {
@@ -43,7 +43,7 @@ const MAX_NOTE_CHARS = 500;
43
43
  /** Rows the screen shows when the terminal height is unknown. */
44
44
  const SCREEN_DEFAULT_ROWS = 14;
45
45
 
46
- /** Fixed rows around the list: header, summary, blank, blank, hint, blank. */
46
+ /** Fixed rows around the list: header, summary, blank, blank, hint, closing rule. */
47
47
  const CHROME = 6;
48
48
 
49
49
  /** Which scopes the list shows. */
@@ -482,6 +482,8 @@ export class MemoryPickerComponent implements Component {
482
482
  const body: string[] = [];
483
483
  for (const line of this.editor.render(Math.max(1, width - 2))) body.push(`${pad}${line}`);
484
484
  return dockListFrame({
485
+ theme: this.theme,
486
+ width,
485
487
  header: titleRule(this.theme, width, `Memory · ${verb} ${scope}`),
486
488
  summary: clip(` ${paint(this.theme, "meta", summary)}`, width),
487
489
  body,
@@ -528,6 +530,8 @@ export class MemoryPickerComponent implements Component {
528
530
  ]);
529
531
 
530
532
  return dockListFrame({
533
+ theme: this.theme,
534
+ width: w,
531
535
  header: titleRule(this.theme, w, "Memory"),
532
536
  summary: this.summary(w),
533
537
  body,
@@ -45,18 +45,35 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
45
45
  empty repositories, and falls back to evidence with recorded assumptions when
46
46
  the interview is unavailable or cancelled; it never invents commands.
47
47
  - **Plan files.** The model saves the plan with `write_plan`; the file lands in
48
- `<repo-root>/.pi/plans/<YYYY-MM-DD-HHmm>-<slug>.md` (or `<agent-dir>/plans`
49
- outside a repository). The plans directory is self-ignoring, so plan files
50
- never show up as untracked files or in `rewind` snapshots.
48
+ `<repo-root>/.pi/plans/<YYYY-MM-DD-HHmm>-<slug>.md` — the entered worktree's
49
+ root when one is active — or `<agent-dir>/plans` outside a repository. The
50
+ plans directory is self-ignoring, so plan files never show up as untracked
51
+ files or in `rewind` snapshots.
51
52
  - **A discoverable index.** `/plan list` walks the plans directory and shows
52
53
  every plan with its title, modified time, and step progress, newest first — in
53
54
  the TUI as a docked list where `Enter` opens the plan's review, elsewhere as a
54
55
  notice with each plan's path. The directory stays the index: no registry file,
55
56
  one bounded read per plan.
56
57
  - **A shape that reads.** `write_plan` documents a canonical outline — a
57
- `# <Title>`, then `## Goal`, `## Context`, `## Approach`, `## Steps`,
58
- `## Risks`, and `## Verification` — and the model is guided to follow it. This
59
- is guidance only: any markdown is accepted, stored, and rendered as written.
58
+ `# <Title>`, then `## Goal`, `## Context`, `## Approach`, `## Visual`,
59
+ `## Steps`, `## Risks`, and `## Verification` — and the model is guided to
60
+ follow it. Every example in the template is the shape wanted rather than a
61
+ description of it, and each section has one job: the goal is one sentence, the
62
+ context is the facts that force the approach, the approach names the
63
+ alternative it rejected, and verification is a fence of the commands to run.
64
+ `Visual` carries the end state in an untagged fence, with one line naming what to
65
+ look at: the same surface before and after when the change alters one that
66
+ already exists, one frame when the surface is new, because a reviewer judges the
67
+ result before the steps that build it; a change with no visible surface drops it.
68
+ The description calls the shape a guide rather than a form: `Context`,
69
+ `Approach`, `Visual`, and `Risks` are dropped when they have nothing to say,
70
+ while `Steps` and `Verification` are kept, because the steps are read from that
71
+ section (without it, the shallowest list anywhere becomes the steps) and
72
+ Verification becomes the last todo item. Within `Steps` only the shallowest
73
+ list is tracked, so nested bullets stay notes instead of becoming todos. This
74
+ is guidance only: any markdown is accepted, stored, and rendered as written,
75
+ and a refinement is asked to keep its section headings so the review can still
76
+ mark only what changed.
60
77
  - **Trackable steps.** Steps are written as `- [ ]` checkboxes in the `## Steps`
61
78
  section; approval seeds them into `todo`, with the `## Verification` section as
62
79
  the last item so the definition of done travels with the work. A plan with more
@@ -76,7 +93,9 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
76
93
  widget and the plan do not drift apart. Pass the step's text as `expected` and
77
94
  an update whose step number moved is refused instead of ticking the wrong line.
78
95
  - **Reviewable plan.** `exit_plan_mode` reads the plan file back and presents
79
- it for approval. The user approves, keeps planning, or asks for a refinement.
96
+ it for approval. The user approves, keeps planning, asks for a refinement, or
97
+ exits plan mode without approving; the last keeps the plan file for a later
98
+ review and records no todo items or goal.
80
99
  On a remote turn (WeChat) the plan file is sent first, then a digest of its
81
100
  title, step count, and first steps, then the choices, so the review is still
82
101
  answerable when the attachment is not opened. The choices include **Send the
@@ -100,9 +119,10 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
100
119
  replacement: existing items keep their place and status, a step whose text is
101
120
  already there is skipped, and the tool's item cap defers the rest instead of
102
121
  dropping anything. The plan's `## Verification` section follows as the **last**
103
- todo item, summarized on one line (`Verify: …`) — it is not a plan step, so it is
104
- ticked in the todo list rather than with `set_plan_steps`, and it never counts
105
- against the 50-step cap. A plan without a `Steps` heading keeps the older rule —
122
+ todo item, summarized on one line (`Verify: …`) — a fenced block contributes its
123
+ commands, not its fence lines. It is not a plan step, so it is ticked in the todo
124
+ list rather than with `set_plan_steps`, and it never counts against the 50-step
125
+ cap. A plan without a `Steps` heading keeps the older rule —
106
126
  the shallowest list in the document — so prose bullets in a plan that follows the
107
127
  outline never become todos, and its verification bullets are already covered.
108
128
  Each seeded item is matched back to its step by text, so ticking a step with
@@ -113,8 +133,9 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
113
133
  (`planning`) is finalized from the plan's own `## Goal` section — falling back
114
134
  to the title — through the `goal` tool, with no extra dialog, because approving
115
135
  the plan *is* the decision. A session with no goal at all gets one confirm to
116
- record the same objective, and an `active` or `achieved` goal is never
117
- rewritten: a final objective is the user's. No path spends a model turn.
136
+ record the same objective, unless `plan.goalPrompt` is off, which records it
137
+ without asking. An `active` or `achieved` goal is never rewritten: a final
138
+ objective is the user's. No path spends a model turn.
118
139
  - **Branch-aware.** The enabled flag and the review baseline are custom session
119
140
  entries, so `/resume` and `/tree` restore the right mode and keep the
120
141
  changed-section markers.
@@ -125,7 +146,8 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
125
146
  |---|---|---|
126
147
  | `enter_plan_mode` | normal mode | Ask to enter plan mode; empty schema, confirmed with `ctx.ui.confirm`. |
127
148
  | `write_plan` | plan mode | Save the plan: `title` (file-name slug), `content` (full markdown in the documented outline), optional `plan_path` to overwrite a refinement. Returns the absolute path. A plan with more than 50 top-level steps in its `Steps` section is refused, with the cap and the fix named, and nothing is written. |
128
- | `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. Refuses a file whose step list is over the cap, before the review screen. |
149
+ | `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. The review also offers **Exit plan mode (discard)**, which leaves plan mode without approving. Refuses a file whose step list is over the cap, before the review screen. |
150
+ | `cancel_plan_mode` | plan mode | Leave plan mode without a plan file or a review. Restores write access; records no todo items or goal; keeps any saved plan. Empty schema. |
129
151
  | `set_plan_steps` | normal mode | Keep an approved plan current: set the checkboxes of the given top-level steps (`updates`, each a 1-based `step` in the order `exit_plan_mode` listed, plus `status`, and `expected` — the step's text, so a reordered plan is refused) to `pending` or `completed` in one write. Only the checkboxes change, and a reopened step drops its `[DONE:n]` marker. The todo item approval seeded from a step is ticked with it: the file is written first and is authoritative, a renamed item is left alone, and a failed todo write is reported in the result. |
130
152
 
131
153
  ### Commands and flags
@@ -137,6 +159,7 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
137
159
  | `/plan <prompt>` | the user, with the task | immediate |
138
160
  | `/plan list` | the user, browsing the plans directory | immediate |
139
161
  | `/plan review [path]` | the user, reviewing a plan in the browser | immediate |
162
+ | `/plan cancel` | the user, leaving plan mode without approving | immediate |
140
163
  | `/skill:init` | the user, creating or updating `AGENTS.md` | enters plan mode; write waits for plan approval |
141
164
  | `Ctrl+Alt+P` | the user | immediate |
142
165
  | `--plan` | the user, at launch | immediate |
@@ -204,7 +227,7 @@ package, so load the whole package (`pi -e .`) or install it; loading only
204
227
  ## Behaviour by mode
205
228
 
206
229
  The review screen is TUI-only. `exit_plan_mode` opens `PlanViewComponent`: a
207
- scrollable, width-safe view of the plan with approve/refine/keep in the footer.
230
+ scrollable, width-safe view of the plan with approve/refine/keep/exit in the footer.
208
231
  The body is rendered with Pi's own `Markdown` component and `getMarkdownTheme()`,
209
232
  so headings, lists, inline code, code blocks, links, and quotes carry the active
210
233
  theme's colors; task items get the shared `◼` (done) or `◻` (pending) marker,
@@ -213,7 +236,11 @@ actions (`a approve & run` in `success`, `r refine` in `accent`).
213
236
  `a` ends plan mode, restores write access, and seeds `todo`; `r` keeps the plan
214
237
  mounted and opens an inline editor beneath it (`Enter` submits, an empty buffer
215
238
  does nothing, `Esc` returns to the plan); `Esc` keeps planning and tells the
216
- model the plan was not approved. When the plan changed since the previous review
239
+ model the plan was not approved; `x` leaves plan mode without approving and
240
+ keeps the plan file. The dialog path offers the same five outcomes, including
241
+ **Exit plan mode (discard)**. Dismissing the dialog (or a timeout) is also an
242
+ exit, not a silent `keep`; only the explicit **Keep planning** option stays in
243
+ plan mode. When the plan changed since the previous review
217
244
  on this branch, a summary line under the title names how many sections changed
218
245
  and the scroll row appends `· changed` to the current section. Scroll with `↑`/`↓` or `j`/`k`, `space`/`b` or
219
246
  `PgUp`/`PgDn`, `d`/`u`, `g`/`G`, and the mouse wheel. Press `n`/`p` to jump to
@@ -225,12 +252,12 @@ from the design system's `gaugeBar` and stays
225
252
  visible while the refine editor is open, since the body above is still clipped.
226
253
  Hints are
227
254
  dropped whole as the terminal narrows, and each footer leads with its safe exit
228
- (`Esc keep planning` in the review, `Esc back to plan` in the inline editor) so
255
+ (`Esc keep plan` in the review, `Esc back to plan` in the inline editor) so
229
256
  the exit survives even when only one hint fits.
230
257
 
231
- Dialog-capable non-TUI modes (RPC) show the same three choices as a select menu
258
+ Dialog-capable non-TUI modes (RPC) show the same five choices as a select menu
232
259
  plus the refine editor; cancelling the editor returns to the select instead of
233
- being reported as "not approved". A WeChat turn takes the same dialog path and
260
+ being reported as "not approved". Dismissing the plan select is an exit. A WeChat turn takes the same dialog path and
234
261
  is answered over WeChat, even though the session is in TUI mode: the choice
235
262
  comes from `askHuman(ctx, { custom, dialogs, file, body })` in
236
263
  `lib/interaction.ts`, not from `ctx.mode`. A remote turn also receives the plan
@@ -276,6 +303,16 @@ exit_plan_mode submitted a plan
276
303
  … 12 more lines (ctrl+o to expand)
277
304
  ```
278
305
 
306
+ ## Configuration
307
+
308
+ The `plan` section of the shared `bytefly.json` (global then project), or the
309
+ **Offer the session goal** row in `/bytefly settings`. It is read when a plan is
310
+ approved, so a change applies to the next approval without a reload.
311
+
312
+ | Key | Default | Meaning |
313
+ |---|---|---|
314
+ | `goalPrompt` | `true` | Ask before recording an approved plan's objective as the session goal; `false` records it without asking. |
315
+
279
316
  ## Security
280
317
 
281
318
  Plan mode is a **guard rail, not a sandbox**: extensions run with Pi's OS
@@ -409,18 +446,28 @@ file and returns them to the model.
409
446
  - **Inline threads.** A comment renders directly under the block it anchors to,
410
447
  labelled and indented as a thread, so it reads in place and the plan keeps one
411
448
  measure at every width. There is no separate column to reflow.
449
+ - **Nothing typed is lost.** **Save feedback**, **Approve & run**, and **Back to
450
+ Pi** refuse while a composer holds text and put the cursor back in it, and a
451
+ deleted comment is offered back in the header for a few seconds. The page keeps
452
+ no draft of its own — these are the three ways an open comment could disappear.
453
+ - **Prints as a document.** Printing uses the light palette whatever the screen
454
+ shows, drops the header, panel, rail, and progress bar, and lets the comments
455
+ stay with the blocks they belong to.
412
456
  - **Keyboard first.** `j`/`k` move to the next or previous block, `c` comments
413
457
  on the current block, `a` approves (the approval flow only), `s` saves, `Esc`
414
458
  closes the composer or the sheet and then the review, and `?` opens a shortcut
415
- sheet; the header's `?` button opens the same sheet.
459
+ sheet; the header's `?` button opens the same sheet. Either dialog traps
460
+ `Tab` and hands focus back to whatever opened it.
416
461
  - **Decision in place.** A review opened for approval (`w` on the review screen)
417
462
  shows **Approve & run** beside Save feedback: it persists the current comments,
418
463
  ends plan mode, restores write access, seeds `todo`, and hands control back to
419
- the model. **Save feedback** keeps plan mode and returns the comments as a
420
- refinement; **Back to Pi** closes the review with no decision. Each action
421
- settles the page on a terminal state — `Approved`, `Saved`, or `Closed` — so
422
- the tab never falls to Offline. `/plan review` is comment-only: it renders no
423
- approve action and the server refuses one.
464
+ the model. Because that cannot be undone from the page, the button and `a`
465
+ open a confirmation that says what happens and starts on **Cancel**; only its
466
+ own **Approve & run** posts. **Save feedback** keeps plan mode and returns the
467
+ comments as a refinement; **Back to Pi** closes the review with no decision.
468
+ Each action settles the page on a terminal state — `Approved`, `Saved`, or
469
+ `Closed` — so the tab never falls to Offline. `/plan review` is comment-only:
470
+ it renders no approve action and the server refuses one.
424
471
  - **Markdown that reads.** Headings, lists, task items, blockquotes, fenced
425
472
  code, GFM tables, `---` rules, and bare-URL autolinks render; a list item's
426
473
  continuation lines hang under the item's text, inline code is a soft fill
@@ -441,9 +488,10 @@ file and returns them to the model.
441
488
  page rendered and answers `409`; the page then offers a **Reload** action,
442
489
  because only a reload can re-anchor the comments to the new text.
443
490
  - **Back to the model.** A save from the review screen returns `refined: true`
444
- with the comments, so the model stays in plan mode and revises the same file.
445
- `/plan review` sends them as a user message (a plan-mode revision or a plain
446
- update, depending on the mode).
491
+ with the comments, so the model stays in plan mode and revises the same file. A
492
+ save that carries no comments asks for nothing, so the review screen comes back
493
+ instead. `/plan review` sends them as a user message (a plan-mode revision or a
494
+ plain update, depending on the mode).
447
495
  - **Loopback and tokenized.** The server binds `127.0.0.1`, carries a random
448
496
  per-session token in the URL, rejects a missing or foreign `Host` header,
449
497
  serves only `GET`, `HEAD`, and `POST`, and caps the request body. A save or cancel
@@ -503,7 +551,7 @@ file and returns them to the model.
503
551
  |---|---|
504
552
  | Tools | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps` registered with `executionMode: "sequential"`. `enter_plan_mode` is active normally with `readOnlyHint: true`; `write_plan` (`readOnlyHint: false`) and `exit_plan_mode` (`readOnlyHint: true`) are `defaultActive: false`. `set_plan_steps` is active normally (`readOnlyHint: false`, `idempotentHint: true`); the read-only policy default-denies it in plan mode. Each returns typed `details` (`EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `SetPlanStepsDetails`), and `ExitPlanModeDetails` carries the review's `annotations` when a browser approval or save supplied comments; no `outputSchema`/`structuredContent`. |
505
553
  | Commands and flags | `/plan` (toggle, or enter and send a task), `/plan list` (plans index), `/plan review [path]` (browser review), `/skill:init` (skill-backed `AGENTS.md` bootstrap), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
506
- | State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })`, and each review's baseline persisted with `pi.appendEntry("plan-baseline", { path, sections })` as one digest per heading section; both rebuilt from `ctx.sessionManager.getBranch()`, so `/resume` and `/tree` restore the mode and the changed markers of the branch they land on. Plan writes a goal only at approval, through `ctx.executeTool("goal", { objective })`: a `planning` goal is promoted to the plan's `## Goal`, a session with none is offered one, and an `active`/`achieved` goal is left alone. A review that shows the same content appends nothing. The last plan path lives in runtime memory for the footer chip, and a bare `/plan review` falls back to the newest plan file in the directory. |
554
+ | State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })`, and each review's baseline persisted with `pi.appendEntry("plan-baseline", { path, sections })` as one digest per heading section; both rebuilt from `ctx.sessionManager.getBranch()`, so `/resume` and `/tree` restore the mode and the changed markers of the branch they land on. Plan writes a goal only at approval, through `ctx.executeTool("goal", { objective })`: a `planning` goal is promoted to the plan's `## Goal`, a session with none is offered one unless `plan.goalPrompt` is off, and an `active`/`achieved` goal is left alone. A review that shows the same content appends nothing. The last plan path lives in runtime memory for the footer chip, and a bare `/plan review` falls back to the newest plan file in the directory. |
507
555
  | Entry requests | `pi.events` channel `PLAN_ENTER_REQUEST` from `lib/plan-entry.ts`: the `goal` workflow asks, with the shared consent sentence, and plan enables the mode on the caller's context without a second dialog. Subscribed on `session_start`, released on `session_shutdown`. |
508
556
  | Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context, with a pursued session goal read from the branch through `lib/goal-state.ts`; `context` keeps only the newest plan context while enabled and drops stale ones otherwise. |
509
557
  | Lifecycle | `session_start`/`session_tree` restore state; `session_shutdown` clears the footer status and closes any browser review server. |
@@ -577,8 +625,12 @@ file and returns them to the model.
577
625
  share one normalization so a tab cannot read as a change.
578
626
  - **The outline guides, it does not enforce.** `write_plan` documents the
579
627
  canonical shape in its description and stores whatever markdown it is given;
580
- no section is required and nothing is rewritten. That keeps the model free to
581
- adapt the document while making the common case consistent and scannable.
628
+ no section is required and nothing is rewritten. The description names the two
629
+ headings that do carry behavior — `Steps` is what approval reads as steps, and
630
+ `Verification` the trailing check — so the model is told the cost of dropping
631
+ one instead of having the plan refused. The review's baseline is keyed by
632
+ heading text, so a refinement is asked to keep its headings; a rename reads as
633
+ an edit, which is the one change the markers cannot tell from a rewrite.
582
634
  - **Sections are rendered one at a time.** Pi's `Markdown` exposes no
583
635
  source-line map, so `tui.ts` splits the body at its headings and renders each
584
636
  section separately. That gives exact offsets for the current-section label and
@@ -623,9 +675,10 @@ file and returns them to the model.
623
675
  |---|---|
624
676
  | `index.ts` | Wiring: tools, command, flag, shortcut, events, context injection. |
625
677
  | `types.ts` | `PlanModeEntry`, `EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `SetPlanStepsDetails`. |
678
+ | `config.ts` | The `plan` section loader: `goalPrompt`, default on. |
626
679
  | `policy.ts` | Plan mode's read-only policy, the shared prompt summary, and the bash-refusal guidance. |
627
680
  | `git-bash.ts` | `checkReadOnlyGit`: the pure allowlist that restricts `bash` to read-only git commands. |
628
- | `plans.ts` | Plan-file store: slug/stamp naming, directory resolution, containment, atomic `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
681
+ | `plans.ts` | Plan-file store: slug/stamp naming, directory resolution (repository, active worktree, or agent directory), containment, atomic `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
629
682
  | `todos.ts` | Approval-time todo merge (pure): the last list replayed from the branch, and the merge of the plan's steps into it with the cap respected. |
630
683
  | `steps.ts` | Plan-document structure (pure): the outline, step extraction and the `Steps`-section scope, heading sections, the plan's own goal (`goalSection`), and the checkbox edit. |
631
684
  | `format.ts` | Pure result/approval text: the plan preview, the step-cap diagnosis, step labels, the merge summary, and the verification note. |
@@ -641,8 +694,10 @@ file and returns them to the model.
641
694
  | `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
642
695
  | `web.ts` | `createPlanWebReview`: the review session (SSE close detection, the body cap, settle-after-flush, abort) over the shared loopback bind, writer, and close. |
643
696
  | `web-router.ts` | Pure request handler: token/`Host`/method checks and the review routes, over the shared page and JSON header sets. |
644
- | `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet), its data island, and inline CSS. |
645
- | `web-client.ts` | The `/client.js` source (block/thread rendering, the comment rail, outline and facts, inline comments, selection quotes, tables, highlighted code, keyboard nav, save/approve/back). |
697
+ | `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet, approve confirmation), its data island, and inline CSS. |
698
+ | `web-client.ts` | The `/client.js` source: state and wiring (comment rail, comments, selection quotes, save/approve/back, keyboard nav) over the two fragments below. |
699
+ | `web-markdown.ts` | The client's document half: `safeHref` and the inline/table/code/list rendering (interpolated into `web-client.ts`). |
700
+ | `web-chrome.ts` | The client's chrome half: the state chip, the status line, the header, reading progress, and the stream's lifetime (interpolated into `web-client.ts`). |
646
701
  | `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
647
702
  | [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
648
703
  | [`../../lib/plan-entry.ts`](../../lib/plan-entry.ts) | Plan-mode entry shared with `goal`: the read-only summary, the consent sentence, and the `pi.events` request `plan` answers by enabling the mode. |