@gavin-hu/my-pi-agent 0.17.0 → 0.19.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 (274) hide show
  1. package/README.md +61 -45
  2. package/README.zh-CN.md +54 -42
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +344 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +465 -416
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +554 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +66 -53
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
  14. package/docs/extensions/plan.md +976 -0
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +285 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +166 -119
  19. package/docs/extensions/team.md +226 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +60 -38
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
  25. package/extensions/board/commands.ts +87 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +72 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +312 -0
  32. package/extensions/board/result.ts +99 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +75 -0
  35. package/extensions/board/seat.ts +29 -0
  36. package/extensions/board/snapshot.ts +41 -0
  37. package/extensions/board/tools.ts +279 -0
  38. package/extensions/board/transport/router.ts +172 -0
  39. package/extensions/board/transport/server.ts +149 -0
  40. package/extensions/board/types.ts +54 -0
  41. package/extensions/board/view/actions.ts +306 -0
  42. package/extensions/board/view/client.ts +41 -0
  43. package/extensions/board/view/keys.ts +121 -0
  44. package/extensions/board/view/live.ts +397 -0
  45. package/extensions/board/view/page.ts +158 -0
  46. package/extensions/board/view/render.ts +496 -0
  47. package/extensions/board/view/styles.ts +211 -0
  48. package/extensions/board/view/team.ts +305 -0
  49. package/extensions/board/writes.ts +99 -0
  50. package/extensions/bytefly/notify/types.ts +1 -1
  51. package/extensions/bytefly/panel/commands.ts +40 -7
  52. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  53. package/extensions/bytefly/panel/settings.ts +11 -21
  54. package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
  55. package/extensions/bytefly/picker/discovery.ts +0 -2
  56. package/extensions/bytefly/picker/types.ts +0 -2
  57. package/extensions/bytefly/update/check.ts +39 -0
  58. package/extensions/bytefly/update/format.ts +15 -0
  59. package/extensions/bytefly/update/register.ts +73 -34
  60. package/extensions/bytefly/update/state.ts +6 -3
  61. package/extensions/context/tui.ts +1 -1
  62. package/extensions/file-browse/{config.ts → config/config.ts} +13 -4
  63. package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
  64. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  65. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  66. package/extensions/file-browse/index.ts +4 -4
  67. package/extensions/file-browse/{live.ts → live/live.ts} +54 -82
  68. package/extensions/file-browse/live/rev.ts +42 -0
  69. package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
  70. package/extensions/file-browse/transport/server.ts +110 -0
  71. package/extensions/file-browse/view/chrome.ts +83 -0
  72. package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
  73. package/extensions/file-browse/view/diff-html.ts +94 -0
  74. package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
  75. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  76. package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -1
  77. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
  78. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
  79. package/extensions/file-browse/{styles.ts → view/styles.ts} +31 -5
  80. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  81. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  82. package/extensions/file-read/schema.ts +2 -1
  83. package/extensions/goal/plan-route.ts +8 -2
  84. package/extensions/job/index.ts +15 -7
  85. package/extensions/memory/store.ts +12 -5
  86. package/extensions/plan/blocks.ts +23 -2
  87. package/extensions/plan/commands.ts +77 -21
  88. package/extensions/plan/feedback.ts +409 -63
  89. package/extensions/plan/index.ts +7 -5
  90. package/extensions/plan/plan-index.ts +44 -10
  91. package/extensions/plan/plan-text.ts +51 -24
  92. package/extensions/plan/plans.ts +162 -57
  93. package/extensions/plan/policy.ts +20 -12
  94. package/extensions/plan/review-prompt.ts +93 -0
  95. package/extensions/plan/review.ts +51 -95
  96. package/extensions/plan/runtime.ts +22 -10
  97. package/extensions/plan/sections.ts +112 -0
  98. package/extensions/plan/steps.ts +63 -95
  99. package/extensions/plan/tools/enter.ts +8 -3
  100. package/extensions/plan/tools/exit.ts +13 -19
  101. package/extensions/plan/tools/write.ts +8 -3
  102. package/extensions/plan/tools.ts +1 -1
  103. package/extensions/plan/tui.ts +29 -15
  104. package/extensions/plan/types.ts +29 -3
  105. package/extensions/plan/web-chrome.ts +2 -1
  106. package/extensions/plan/web-client.ts +122 -178
  107. package/extensions/plan/web-collab.ts +207 -0
  108. package/extensions/plan/web-comments.ts +269 -0
  109. package/extensions/plan/web-page.ts +96 -17
  110. package/extensions/plan/web-router.ts +109 -47
  111. package/extensions/plan/web-share-session.ts +191 -0
  112. package/extensions/plan/web-share.ts +169 -0
  113. package/extensions/plan/web-streams.ts +124 -0
  114. package/extensions/plan/web-tokens.ts +63 -0
  115. package/extensions/plan/web.ts +245 -151
  116. package/extensions/question/dialogs.ts +91 -1
  117. package/extensions/question/tools.ts +2 -1
  118. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  119. package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
  120. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  121. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  122. package/extensions/rewind/index.ts +3 -3
  123. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  124. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  125. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  126. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  127. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  128. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  129. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  130. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  131. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  132. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  133. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  134. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  135. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  136. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  137. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  138. package/extensions/session-bus/commands.ts +113 -0
  139. package/extensions/session-bus/format.ts +101 -0
  140. package/extensions/session-bus/index.ts +75 -0
  141. package/extensions/session-bus/join-request.ts +40 -0
  142. package/extensions/session-bus/mail-watch.ts +134 -0
  143. package/extensions/session-bus/notice.ts +86 -0
  144. package/extensions/session-bus/overlap.ts +36 -0
  145. package/extensions/session-bus/protocol.ts +104 -0
  146. package/extensions/session-bus/resolve.ts +67 -0
  147. package/extensions/session-bus/result.ts +208 -0
  148. package/extensions/session-bus/rows.ts +171 -0
  149. package/extensions/session-bus/runtime.ts +466 -0
  150. package/extensions/session-bus/store.ts +365 -0
  151. package/extensions/session-bus/tools.ts +192 -0
  152. package/extensions/session-bus/tui.ts +394 -0
  153. package/extensions/session-bus/types.ts +98 -0
  154. package/extensions/subagent/agents.ts +9 -8
  155. package/extensions/subagent/invocation.ts +4 -2
  156. package/extensions/team/commands.ts +112 -0
  157. package/extensions/team/dispatch.ts +133 -0
  158. package/extensions/team/format.ts +105 -0
  159. package/extensions/team/index.ts +33 -0
  160. package/extensions/team/schema.ts +29 -0
  161. package/extensions/team/tools.ts +246 -0
  162. package/extensions/team/types.ts +48 -0
  163. package/extensions/watch/index.ts +15 -9
  164. package/extensions/web-access/fetch/page.ts +4 -1
  165. package/extensions/web-access/fetch/structured.ts +447 -0
  166. package/extensions/web-access/fetch/tool.ts +2 -1
  167. package/extensions/web-access/search/tool.ts +2 -1
  168. package/extensions/wechat/bridge.ts +76 -92
  169. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  170. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  171. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  172. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  173. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  174. package/extensions/wechat/dialog/channels.ts +136 -0
  175. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +66 -51
  176. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
  177. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  178. package/extensions/wechat/index.ts +10 -10
  179. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  180. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  181. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +119 -56
  182. package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
  183. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
  184. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  185. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  186. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  187. package/extensions/wechat/{notices.ts → text/notices.ts} +15 -7
  188. package/extensions/wechat/tools.ts +29 -12
  189. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  190. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +104 -40
  191. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  192. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  193. package/extensions/wechat/transport/outbound.ts +142 -0
  194. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  195. package/extensions/wechat/types.ts +10 -1
  196. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  197. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  198. package/extensions/worktree/git.ts +5 -0
  199. package/extensions/worktree/guard.ts +3 -5
  200. package/extensions/worktree/registry.ts +11 -5
  201. package/extensions/worktree/status.ts +1 -3
  202. package/lib/README.md +12 -6
  203. package/lib/board/card.ts +290 -0
  204. package/lib/board/links.ts +117 -0
  205. package/lib/board/page-pointer.ts +256 -0
  206. package/lib/board/round-text.ts +72 -0
  207. package/lib/board/round.ts +89 -0
  208. package/lib/board/rules.ts +304 -0
  209. package/lib/board/session.ts +105 -0
  210. package/lib/board/store.ts +310 -0
  211. package/lib/board/view.ts +150 -0
  212. package/lib/errors.ts +12 -4
  213. package/lib/format.ts +13 -0
  214. package/lib/http/guard.ts +112 -0
  215. package/lib/http/index.ts +16 -5
  216. package/lib/http/router.ts +40 -13
  217. package/lib/http/serve.ts +234 -0
  218. package/lib/http/server.ts +29 -13
  219. package/lib/http/sink.ts +110 -0
  220. package/lib/http/token.ts +38 -0
  221. package/lib/interaction.ts +10 -0
  222. package/lib/notice.ts +29 -2
  223. package/lib/plan-entry.ts +7 -2
  224. package/lib/policy.ts +12 -2
  225. package/lib/qr.ts +64 -0
  226. package/lib/repo-key.ts +74 -0
  227. package/lib/report.ts +52 -33
  228. package/lib/requirements.ts +7 -9
  229. package/lib/session-label.ts +64 -0
  230. package/lib/session-roster.ts +130 -0
  231. package/lib/session-tool.ts +184 -0
  232. package/lib/tool-names.ts +25 -2
  233. package/lib/tui/README.md +16 -2
  234. package/lib/tui/dock-screen.ts +85 -17
  235. package/lib/tui/glyphs.ts +2 -0
  236. package/lib/tui/line-pane.ts +15 -4
  237. package/lib/tui/tool.ts +23 -4
  238. package/lib/web/README.md +5 -2
  239. package/lib/web/components.ts +6 -1
  240. package/lib/web/runtime.ts +7 -1
  241. package/lib/web/tokens.ts +25 -2
  242. package/package.json +27 -14
  243. package/skills/init/SKILL.md +80 -45
  244. package/skills/init/references/agents-md-quality.md +26 -2
  245. package/skills/init/references/interview-questions.md +36 -14
  246. package/skills/next/SKILL.md +77 -51
  247. package/skills/project-docs/SKILL.md +133 -0
  248. package/skills/project-docs/references/audit.md +131 -0
  249. package/skills/project-docs/references/changelog.md +105 -0
  250. package/skills/project-docs/references/discovery.md +127 -0
  251. package/skills/project-docs/references/readme.md +80 -0
  252. package/skills/project-docs/references/release.md +84 -0
  253. package/skills/project-docs/references/roadmap.md +93 -0
  254. package/skills/project-docs/scripts/state.ts +316 -0
  255. package/skills/refactor/SKILL.md +100 -125
  256. package/skills/refactor/references/handoff.md +35 -0
  257. package/skills/refactor/references/signals.md +54 -0
  258. package/skills/refactor/scripts/inventory.ts +587 -0
  259. package/extensions/file-browse/README.md +0 -467
  260. package/extensions/file-browse/server.ts +0 -134
  261. package/extensions/plan/README.md +0 -709
  262. package/extensions/plan/config.ts +0 -27
  263. package/extensions/wechat/outbound.ts +0 -100
  264. package/extensions/wechat/qrcode.ts +0 -46
  265. package/skills/changelog/SKILL.md +0 -148
  266. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  267. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  268. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  269. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  270. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  271. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  272. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  273. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  274. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -5,7 +5,8 @@
5
5
  * processes with their output streamed to log files; the status chip shows
6
6
  * running and unreported-failure counts, and finished jobs are reported to the
7
7
  * model — and shown as a bytefly notice — at the next turn, or by triggering one
8
- * turn for a `wake` job.
8
+ * turn for a `wake` job as soon as the agent is idle, including right after a
9
+ * running turn settles.
9
10
  *
10
11
  * Load with: pi --extension ./extensions/job
11
12
  */
@@ -13,7 +14,7 @@
13
14
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
15
  import { isExtensionEnabled } from "../../lib/env.ts";
15
16
  import { JOB_FINISHED_CHANNEL, type JobFinishedEvent } from "../../lib/events.ts";
16
- import { noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
17
+ import { burstText, noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
17
18
  import { createPendingReporter, registerPendingChannel } from "../../lib/report.ts";
18
19
  import { registerCommands } from "./commands.ts";
19
20
  import { formatCompletion, jobOutcome, shortLabel } from "./format.ts";
@@ -25,8 +26,13 @@ import type { JobRecord } from "./types.ts";
25
26
  const JOB_CONTEXT_MARKER = "[BACKGROUND JOBS]";
26
27
 
27
28
  function buildContext(jobs: JobRecord[]): string {
28
- const lines = jobs.map((job) => formatCompletion(job));
29
- return `${JOB_CONTEXT_MARKER}\n${jobs.length} background job${jobs.length === 1 ? "" : "s"} finished:\n${lines.join("\n")}\nRun \`job logs <id>\` for output.`;
29
+ const count = jobs.length;
30
+ return burstText({
31
+ marker: JOB_CONTEXT_MARKER,
32
+ headline: `${count} background job${count === 1 ? "" : "s"} finished`,
33
+ lines: jobs.map((job) => formatCompletion(job)),
34
+ hint: "Run `job logs <id>` for output.",
35
+ });
30
36
  }
31
37
 
32
38
  /** The shared notice for pending completions: model text plus renderer details. */
@@ -66,15 +72,17 @@ export default function jobs(pi: ExtensionAPI, deps: JobsDeps = {}): void {
66
72
  if (!isExtensionEnabled("job")) return;
67
73
  const runtime = deps.runtime ?? createJobsRuntime();
68
74
 
69
- // Opt-in wake: a `wake` job finished while the agent is idle. The reporter
70
- // reports *all* pending completions, not just the waking one, so a job that
71
- // finished earlier is not silently marked seen and dropped.
75
+ // Opt-in wake: a `wake` job finished. The reporter reports *all* pending
76
+ // completions, not just the waking one, so a job that finished earlier is not
77
+ // silently marked seen and dropped, and it retries a completion that arrived
78
+ // mid-turn once the run settles.
72
79
  const reporter = createPendingReporter<JobRecord>({
73
80
  wake: (notice) => pi.sendMessage(notice, { triggerTurn: true }),
74
81
  pending: () => runtime.pendingCompletions(),
75
82
  markReported: (records) => runtime.markReported(records.map((job) => job.id)),
76
83
  buildNotice: jobNotice,
77
84
  source: "job",
85
+ wakeRequested: () => runtime.pendingCompletions().some((job) => job.wake),
78
86
  });
79
87
 
80
88
  registerTools(pi, runtime);
@@ -74,17 +74,24 @@ export function readMemoryFile(path: string): string {
74
74
  return readFileSync(path, "utf-8");
75
75
  }
76
76
 
77
- /** Coarse identity of a store file, for cheap change detection. */
77
+ /**
78
+ * Identity of a store file, for cheap change detection.
79
+ *
80
+ * The stamp is nanoseconds, not milliseconds: an edit that keeps the file's size
81
+ * is otherwise invisible for a whole millisecond, which is long enough for an
82
+ * editor that writes in place. `size` stays a number so the doctor's byte counts
83
+ * read the same.
84
+ */
78
85
  export interface MemoryFileStamp {
79
- mtimeMs: number;
86
+ mtimeNs: bigint;
80
87
  size: number;
81
88
  }
82
89
 
83
90
  /** Freshness stamp for a store file; `undefined` when it is missing or unstattable. */
84
91
  export function memoryFileStamp(path: string): MemoryFileStamp | undefined {
85
92
  try {
86
- const info = statSync(path);
87
- return { mtimeMs: info.mtimeMs, size: info.size };
93
+ const info = statSync(path, { bigint: true });
94
+ return { mtimeNs: info.mtimeNs, size: Number(info.size) };
88
95
  } catch {
89
96
  return undefined;
90
97
  }
@@ -93,7 +100,7 @@ export function memoryFileStamp(path: string): MemoryFileStamp | undefined {
93
100
  /** Whether two stamps describe the same file state. */
94
101
  export function sameStamp(a: MemoryFileStamp | undefined, b: MemoryFileStamp | undefined): boolean {
95
102
  if (a === undefined || b === undefined) return a === b;
96
- return a.mtimeMs === b.mtimeMs && a.size === b.size;
103
+ return a.mtimeNs === b.mtimeNs && a.size === b.size;
97
104
  }
98
105
 
99
106
  /** Note text of every bullet line, sanitized to one terminal-safe line. */
@@ -14,7 +14,7 @@
14
14
  * step.
15
15
  */
16
16
 
17
- import { FENCE, HEADING } from "./steps.ts";
17
+ import { FENCE, HEADING } from "./sections.ts";
18
18
 
19
19
  /** How a block is rendered and grouped. */
20
20
  export type PlanBlockKind = "heading" | "paragraph" | "list" | "code" | "quote" | "table" | "hr" | "blank";
@@ -176,7 +176,7 @@ export function planBlocks(plan: string): PlanBlock[] {
176
176
  }
177
177
 
178
178
  /** The block containing source line `line`, or the nearest block before it. */
179
- export function blockAt(blocks: PlanBlock[], line: number): PlanBlock | undefined {
179
+ export function blockAt(blocks: readonly PlanBlock[], line: number): PlanBlock | undefined {
180
180
  let candidate: PlanBlock | undefined;
181
181
  for (const block of blocks) {
182
182
  if (block.start <= line && line <= block.end) return block;
@@ -186,6 +186,27 @@ export function blockAt(blocks: PlanBlock[], line: number): PlanBlock | undefine
186
186
  return candidate;
187
187
  }
188
188
 
189
+ /**
190
+ * The nearest block with content at or before `line`, else the first one after
191
+ * it: what a comment in blank space belongs to.
192
+ *
193
+ * A blank line is a block, so {@link blockAt} may name one, but a comment cannot
194
+ * hang on nothing: the review page renders no line for a blank block, so an
195
+ * anchor there is one it can never attach a thread to.
196
+ */
197
+ export function contentBlockAt(blocks: readonly PlanBlock[], line: number): PlanBlock | undefined {
198
+ let before: PlanBlock | undefined;
199
+ for (const block of blocks) {
200
+ const content = block.kind !== "blank";
201
+ if (block.start <= line) {
202
+ if (content) before = block;
203
+ continue;
204
+ }
205
+ if (content) return before ?? block;
206
+ }
207
+ return before;
208
+ }
209
+
189
210
  /** The source inside a fenced code block, without its opening and closing fence lines. */
190
211
  export function codeBody(block: PlanBlock): string[] {
191
212
  if (block.kind !== "code") return block.lines.slice();
@@ -2,15 +2,18 @@
2
2
  * Slash command for the plan-mode extension.
3
3
  *
4
4
  * `/plan` toggles plan mode, or enters it and sends an optional task;
5
- * `Ctrl+Alt+P` toggles too. Two subcommands are reserved: `/plan review [path]`
5
+ * `Ctrl+Alt+P` toggles too. Four subcommands are reserved: `/plan review [path]`
6
6
  * opens a plan in the browser (and sends saved comments to the model as a
7
- * refinement), and `/plan list` shows the plans directory, newest first, with
8
- * Enter opening a review.
7
+ * refinement), `/plan share [stop]` prints or revokes the link a teammate opens,
8
+ * `/plan list` shows the plans directory, newest first with Enter opening a
9
+ * review, and `/plan cancel` leaves the mode without approving.
9
10
  */
10
11
 
11
12
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
13
  import { isRemoteTurn } from "../../lib/interaction.ts";
13
14
  import { READ_ONLY_SUMMARY } from "../../lib/plan-entry.ts";
15
+ import { qrLines } from "../../lib/qr.ts";
16
+ import { withRailsSuppressed } from "../../lib/rails.ts";
14
17
  import { feedbackSummary } from "./feedback.ts";
15
18
  import { PlanIndexComponent, formatPlanIndex } from "./plan-index.ts";
16
19
  import { runBrowserReview } from "./review.ts";
@@ -21,6 +24,7 @@ import { createPlanWebReview, type PlanWebReview } from "./web.ts";
21
24
  const ENABLED_NOTICE = `Plan mode enabled — ${READ_ONLY_SUMMARY}.`;
22
25
  const DISABLED_NOTICE = "Plan mode disabled — full access restored.";
23
26
  const REVIEW_PREFIX = "review";
27
+ const SHARE_PREFIX = "share";
24
28
  const LIST_PREFIX = "list";
25
29
  const CANCEL_PREFIX = "cancel";
26
30
 
@@ -67,6 +71,44 @@ function isReviewSubcommand(prompt: string): boolean {
67
71
  return target.endsWith(".md") || target.includes("/") || target.includes("\\");
68
72
  }
69
73
 
74
+ /**
75
+ * Whether a command line is the reserved `share` subcommand. Exact `share` and
76
+ * `share stop` are; `share the tokens` stays a task.
77
+ */
78
+ function isShareSubcommand(prompt: string): boolean {
79
+ const text = prompt.toLowerCase();
80
+ return text === SHARE_PREFIX || text === `${SHARE_PREFIX} stop`;
81
+ }
82
+
83
+ /**
84
+ * `/plan share`: print the link (and a QR) a teammate opens, or revoke it. The
85
+ * browser page has the same button; this is the path a phone-owned or remote
86
+ * session needs, where there is no page to click.
87
+ */
88
+ async function shareReview(web: PlanWebReview, prompt: string, ctx: ExtensionContext): Promise<void> {
89
+ const stopping = prompt.toLowerCase().endsWith("stop");
90
+ const result = stopping ? await web.stopSharing() : await web.share();
91
+ if (!result) {
92
+ ctx.ui.notify("Plan share: no browser review is open; run /plan review first.", "warning");
93
+ return;
94
+ }
95
+ if (stopping) {
96
+ ctx.ui.notify("Plan share: reviewers can no longer open this review.", "info");
97
+ return;
98
+ }
99
+ const link = result.state.links.at(-1);
100
+ if (!result.state.sharing || !link) {
101
+ ctx.ui.notify(`Plan share: ${result.state.reason ?? "no link is available"}.`, "warning");
102
+ return;
103
+ }
104
+ const lines = ["Review link: " + link.url, ...(await qrLines(link.url))];
105
+ // One link is one person, so more than one is the team the page can list.
106
+ if (result.state.links.length > 1) {
107
+ lines.push(`${result.state.links.length} links shared — the page lists every link and who each one posts as.`);
108
+ }
109
+ ctx.ui.notify(lines.join("\n"), "info");
110
+ }
111
+
70
112
  /**
71
113
  * Open `planPath` in the browser and, if the user saves comments, hand them to
72
114
  * the model. A cancel is silent beyond a notice; nothing is sent.
@@ -137,6 +179,10 @@ async function openPlanIndex(
137
179
  ctx: ExtensionContext,
138
180
  ): Promise<void> {
139
181
  const dir = await runtime.plans.dirFor(ctx.cwd);
182
+ // A store an older version kept in the checkout was just moved in; the list is
183
+ // where a user notices their plans are somewhere else.
184
+ const moved = runtime.plans.takeNotice();
185
+ if (moved) ctx.ui.notify(moved, "info");
140
186
  const summarise = async (): Promise<Awaited<ReturnType<typeof runtime.plans.index>>> => runtime.plans.index(ctx.cwd);
141
187
  const summaries = await summarise();
142
188
 
@@ -147,22 +193,26 @@ async function openPlanIndex(
147
193
 
148
194
  // The review runs after the screen closes, so the input focus is free again.
149
195
  let selected: (PlanSummary | undefined) | undefined;
150
- await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
151
- const component = new PlanIndexComponent({
152
- summaries,
153
- dir,
154
- theme,
155
- onSelect: (summary) => {
156
- selected = summary;
157
- done();
158
- },
159
- onClose: () => done(),
160
- reload: summarise,
161
- requestRender: () => tui.requestRender(),
162
- viewportRows: () => tui.terminal?.rows,
163
- });
164
- return component;
165
- });
196
+ // A dock list shares the editor slot with the goal and todo rails, so it takes
197
+ // the dock to itself for as long as it is open, like the other list screens.
198
+ await withRailsSuppressed(pi, () =>
199
+ ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
200
+ const component = new PlanIndexComponent({
201
+ summaries,
202
+ dir,
203
+ theme,
204
+ onSelect: (summary) => {
205
+ selected = summary;
206
+ done();
207
+ },
208
+ onClose: () => done(),
209
+ reload: summarise,
210
+ requestRender: () => tui.requestRender(),
211
+ viewportRows: () => tui.terminal?.rows,
212
+ });
213
+ return component;
214
+ }),
215
+ );
166
216
  if (selected) await reviewInBrowser(pi, runtime, web, selected.path, ctx);
167
217
  }
168
218
 
@@ -173,11 +223,13 @@ export function registerCommands(
173
223
  ): void {
174
224
  pi.registerCommand("plan", {
175
225
  description:
176
- "Toggle plan mode (read-only exploration), enter it with an optional task, list saved plans, review one in the browser, or cancel planning without approving",
226
+ "Toggle plan mode (read-only exploration), enter it with an optional task, list saved plans, review one in the browser, share that review with a link, or cancel planning without approving",
177
227
  getArgumentCompletions: (prefix) => {
178
228
  const text = prefix.trimStart();
179
229
  if (text.includes(" ")) return null;
180
- const subcommands = [LIST_PREFIX, REVIEW_PREFIX, CANCEL_PREFIX].filter((name) => name.startsWith(text));
230
+ const subcommands = [LIST_PREFIX, REVIEW_PREFIX, SHARE_PREFIX, CANCEL_PREFIX].filter((name) =>
231
+ name.startsWith(text),
232
+ );
181
233
  return subcommands.length > 0 ? subcommands.map((value) => ({ value, label: value })) : null;
182
234
  },
183
235
  handler: async (args, ctx) => {
@@ -198,6 +250,10 @@ export function registerCommands(
198
250
  await openPlanIndex(pi, runtime, web, ctx);
199
251
  return;
200
252
  }
253
+ if (isShareSubcommand(prompt)) {
254
+ await shareReview(web, prompt, ctx);
255
+ return;
256
+ }
201
257
  if (isReviewSubcommand(prompt)) {
202
258
  await reviewInBrowser(pi, runtime, web, reviewTarget(prompt), ctx);
203
259
  return;