@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
@@ -10,13 +10,16 @@
10
10
  * the digest is computed over.
11
11
  *
12
12
  * Only top-level list items become steps: nested sub-bullets are notes, and the
13
- * shallowest list level is treated as the plan. When the plan has a `Steps`
14
- * section (the canonical outline's heading), only that section's list items are
15
- * steps, so prose bullets elsewhere never seed the todo list or consume the
16
- * step cap. List items inside fenced code blocks are ignored. A `- [ ]` / `- [x]`
13
+ * shallowest list level is treated as the plan. An indented, marker-less line
14
+ * continues the item above it, so a wrapped step keeps its full text. When the
15
+ * plan has a `Steps` section (the canonical outline's heading), only that
16
+ * section's list items are steps, so prose bullets elsewhere never seed the todo
17
+ * list or consume the step cap. List items inside fenced code blocks are ignored. A `- [ ]` / `- [x]`
17
18
  * checkbox or a `[DONE:n]` marker sets the status.
18
19
  */
19
20
 
21
+ import { extractSections, FENCE, HEADING, normalizeHeadingText } from "./sections.ts";
22
+
20
23
  /** Most steps seeded from one plan. */
21
24
  export const MAX_STEPS = 50;
22
25
  /** Longest seeded step text; longer lines are elided. */
@@ -82,26 +85,17 @@ export interface PlanStep {
82
85
  status: "pending" | "completed";
83
86
  }
84
87
 
85
- /** A heading-delimited chunk of the plan, for the review screen's navigation. */
86
- export interface PlanSection {
87
- /** Plain heading text, or `""` for the content before the first heading. */
88
- title: string;
89
- /** The section's markdown, including its heading line. */
90
- text: string;
91
- /** 0-based source line of the section's heading (0 for the preamble). */
92
- line: number;
93
- /** Heading level, or 0 for the preamble before the first heading. */
94
- level: number;
95
- }
96
-
97
88
  /** A top-level list item located in the source. */
98
89
  interface PlanListItem {
99
- /** Zero-based source line index. */
90
+ /** Zero-based source line index of the item's first line. */
100
91
  line: number;
101
92
  indent: number;
102
93
  /** The list marker with its trailing whitespace, e.g. `1. ` or `- `. */
103
94
  lead: string;
104
- /** Item text after the marker, right-trimmed (an optional checkbox included). */
95
+ /**
96
+ * Item text after the marker on its first line; indented continuation lines
97
+ * are folded into {@link step}, so a checkbox edit rewrites only this line.
98
+ */
105
99
  raw: string;
106
100
  step: PlanStep;
107
101
  }
@@ -109,39 +103,10 @@ interface PlanListItem {
109
103
  /** A top-level numbered or bulleted list item, capturing its parts. */
110
104
  const LIST_ITEM = /^([ \t]*)((?:\d+[.)]|[-*+])\s+)(.*\S)\s*$/;
111
105
  const CHECKBOX = /^\[([ xX])\]\s*(.*)$/;
112
- /**
113
- * A markdown heading outside a fence, capturing its level and text: up to three
114
- * leading spaces, a space after the hashes, and a closing hash run only when a
115
- * space separates it. Shared with `blocks.ts`, so the section split and the
116
- * browser outline read a heading line the same way, and `## C#` keeps its title.
117
- */
118
- export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)(?:\s+#+)?\s*$/;
119
- /**
120
- * A markdown code-fence opener or closer (backticks or tildes). Shared with
121
- * `blocks.ts`: a tab-indented fence is prose for both parsers, never code for
122
- * one and prose for the other.
123
- */
124
- export const FENCE = /^ {0,3}(`{3,}|~{3,})/;
125
106
  const DONE_MARKER = /\[DONE:\d+\]/gi;
126
107
 
127
- /**
128
- * A heading or step label as the review surfaces compare it: inline emphasis and
129
- * code removed, whitespace collapsed. Exported so `blocks.ts` labels an outline
130
- * entry the same way `extractPlanSections` labels its section, which is what
131
- * lets the browser mark a changed section.
132
- */
133
- export function normalizeHeadingText(text: string): string {
134
- return text
135
- .replace(/\*\*([^*]+)\*\*/g, "$1")
136
- .replace(/\*([^*]+)\*/g, "$1")
137
- .replace(/(?<![A-Za-z0-9_])_([^_]+)_(?![A-Za-z0-9_])/g, "$1")
138
- .replace(/`([^`]+)`/g, "$1")
139
- .replace(/\s+/g, " ")
140
- .trim();
141
- }
142
-
143
- /** Strip inline markdown emphasis and code, then collapse to one clean line. */
144
- function plainText(text: string): string {
108
+ /** A label with its inline markers and any done marker removed. */
109
+ function cleanLabel(text: string): string {
145
110
  return normalizeHeadingText(text.replace(DONE_MARKER, ""));
146
111
  }
147
112
 
@@ -157,7 +122,12 @@ function parseStep(raw: string): PlanStep {
157
122
  }
158
123
  if (/\[DONE:\d+\]/i.test(text)) done = true;
159
124
 
160
- return { content: plainText(text), status: done ? "completed" : "pending" };
125
+ return { content: cleanLabel(text), status: done ? "completed" : "pending" };
126
+ }
127
+
128
+ /** Leading whitespace width of a line, for continuation indent checks. */
129
+ function indentOf(line: string): number {
130
+ return line.length - line.trimStart().length;
161
131
  }
162
132
 
163
133
  /** Every top-level list item, in order, before de-duplication and capping. */
@@ -165,22 +135,57 @@ function scanListItems(plan: string): PlanListItem[] {
165
135
  const items: PlanListItem[] = [];
166
136
  // Fences hide example lists (`- name: foo` in a YAML block) from extraction.
167
137
  let fence: string | undefined;
138
+ // The item still collecting indented continuation lines, and those lines.
139
+ let current: { item: PlanListItem; continuation: string[] } | undefined;
140
+
141
+ /** Fold the collected continuation into the item, then close it. */
142
+ const finish = (): void => {
143
+ if (current === undefined) return;
144
+ if (current.continuation.length > 0) {
145
+ current.item.step = parseStep(`${current.item.raw} ${current.continuation.join(" ")}`);
146
+ }
147
+ current = undefined;
148
+ };
168
149
 
169
150
  plan.split("\n").forEach((line, index) => {
170
151
  const fenceMatch = line.match(FENCE);
171
152
  if (fenceMatch) {
153
+ finish();
172
154
  if (fence === undefined) fence = fenceMatch[1][0];
173
155
  else if (fence === fenceMatch[1][0]) fence = undefined;
174
156
  return;
175
157
  }
176
158
  if (fence !== undefined) return;
177
159
 
160
+ if (line.trim() === "") {
161
+ finish();
162
+ return;
163
+ }
164
+
178
165
  const match = line.match(LIST_ITEM);
179
- if (!match) return;
180
- const step = parseStep(match[3]);
181
- if (!step.content) return;
182
- items.push({ line: index, indent: match[1].length, lead: match[2], raw: match[3], step });
166
+ if (match) {
167
+ finish();
168
+ const step = parseStep(match[3]);
169
+ if (!step.content) return;
170
+ const item: PlanListItem = { line: index, indent: match[1].length, lead: match[2], raw: match[3], step };
171
+ items.push(item);
172
+ current = { item, continuation: [] };
173
+ return;
174
+ }
175
+
176
+ // A deeper, marker-less line continues the step's paragraph; a heading,
177
+ // a shallow line, or a blank line ends it. `raw` stays the first line so
178
+ // `setStepStatuses` edits the checkbox without touching these lines.
179
+ if (current !== undefined && indentOf(line) > current.item.indent && !HEADING.test(line)) {
180
+ const text = line.trim();
181
+ if (text) current.continuation.push(text);
182
+ return;
183
+ }
184
+
185
+ finish();
183
186
  });
187
+
188
+ finish();
184
189
  return items;
185
190
  }
186
191
 
@@ -208,7 +213,7 @@ function stepsSection(plan: string): { start: number; end: number } | undefined
208
213
  if (!heading) continue;
209
214
  const depth = heading[1].length;
210
215
  if (start < 0) {
211
- if (plainText(heading[2]).toLowerCase() !== "steps") continue;
216
+ if (normalizeHeadingText(heading[2]).toLowerCase() !== "steps") continue;
212
217
  start = index;
213
218
  level = depth;
214
219
  continue;
@@ -271,7 +276,7 @@ const LIST_PREFIX = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/;
271
276
  */
272
277
  export function verificationItem(plan: string): PlanStep | undefined {
273
278
  if (stepsSection(plan) === undefined) return undefined;
274
- const section = extractPlanSections(plan).find(
279
+ const section = extractSections(plan).find(
275
280
  (candidate) => candidate.level > 0 && candidate.title.toLowerCase() === "verification",
276
281
  );
277
282
  if (!section) return undefined;
@@ -281,7 +286,7 @@ export function verificationItem(plan: string): PlanStep | undefined {
281
286
  // A fenced block is how the outline asks for commands; its fence lines are
282
287
  // punctuation, not a check, so only the commands inside it are carried.
283
288
  .filter((line) => !FENCE.test(line))
284
- .map((line) => plainText(line.replace(LIST_PREFIX, "")))
289
+ .map((line) => cleanLabel(line.replace(LIST_PREFIX, "")))
285
290
  .filter(Boolean);
286
291
  if (items.length === 0) return undefined;
287
292
  return { content: elide(`Verify: ${items.join("; ")}`), status: "pending" };
@@ -361,43 +366,6 @@ export function setStepStatuses(plan: string, updates: readonly StepEdit[]): str
361
366
  return lines.join("\n");
362
367
  }
363
368
 
364
- /**
365
- * Sections of a plan, split at heading lines outside code fences. The content
366
- * before the first heading is a section titled `""`. Pi's `Markdown` renderer
367
- * exposes no source-line map, so the review screen renders these chunks
368
- * separately to know where each section begins.
369
- */
370
- export function extractPlanSections(plan: string): PlanSection[] {
371
- const sections: PlanSection[] = [];
372
- let current: PlanSection | undefined;
373
- let fence: string | undefined;
374
-
375
- const flush = (): void => {
376
- if (current && (current.title !== "" || current.text.trim() !== "")) sections.push(current);
377
- current = undefined;
378
- };
379
-
380
- plan.split("\n").forEach((line, index) => {
381
- const fenceMatch = line.match(FENCE);
382
- if (fenceMatch) {
383
- if (fence === undefined) fence = fenceMatch[1][0];
384
- else if (fence === fenceMatch[1][0]) fence = undefined;
385
- } else if (fence === undefined) {
386
- const heading = line.match(HEADING);
387
- if (heading) {
388
- flush();
389
- current = { title: plainText(heading[2]), text: line, line: index, level: heading[1].length };
390
- return;
391
- }
392
- }
393
- if (current === undefined) current = { title: "", text: line, line: 0, level: 0 };
394
- else current.text += `\n${line}`;
395
- });
396
-
397
- flush();
398
- return sections;
399
- }
400
-
401
369
  /**
402
370
  * The plan's own objective: the first paragraph of its `## Goal` section, with
403
371
  * inline markdown and a leading list marker removed.
@@ -407,7 +375,7 @@ export function extractPlanSections(plan: string): PlanSection[] {
407
375
  * `## Goal` section, so the caller can fall back to the title.
408
376
  */
409
377
  export function goalSection(plan: string): string | undefined {
410
- const section = extractPlanSections(plan).find(
378
+ const section = extractSections(plan).find(
411
379
  (candidate) => candidate.level === 2 && normalizeHeadingText(candidate.title).toLowerCase() === "goal",
412
380
  );
413
381
  if (section === undefined) return undefined;
@@ -12,8 +12,8 @@ import { GLYPHS } from "../../../lib/tui/glyphs.ts";
12
12
  import { paint } from "../../../lib/tui/tokens.ts";
13
13
  import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
14
14
  import { failedResult } from "../../../lib/tool-result.ts";
15
- import { isRemoteTurn } from "../../../lib/interaction.ts";
16
- import { PLAN_CONSENT, READ_ONLY_SUMMARY } from "../../../lib/plan-entry.ts";
15
+ import { isRemoteTurn, remoteLanguage } from "../../../lib/interaction.ts";
16
+ import { PLAN_CONSENT, PLAN_CONSENT_SHORT, PLAN_CONSENT_SHORT_ZH, READ_ONLY_SUMMARY } from "../../../lib/plan-entry.ts";
17
17
  import { ENTER_TOOL, type PlanRuntime } from "../runtime.ts";
18
18
  import type { EnterPlanModeDetails } from "../types.ts";
19
19
 
@@ -53,7 +53,12 @@ export function registerEnterTool(pi: ExtensionAPI, runtime: PlanRuntime): void
53
53
  );
54
54
  }
55
55
 
56
- const approved = await ctx.ui.confirm("Enter plan mode?", PLAN_CONSENT);
56
+ const consent = isRemoteTurn(ctx)
57
+ ? remoteLanguage(ctx) === "zh"
58
+ ? PLAN_CONSENT_SHORT_ZH
59
+ : PLAN_CONSENT_SHORT
60
+ : PLAN_CONSENT;
61
+ const approved = await ctx.ui.confirm("Enter plan mode?", consent);
57
62
  if (!approved) {
58
63
  return {
59
64
  content: [{ type: "text", text: "Stayed in normal mode. Proceed, or ask the user how to continue." }],
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * It presents the saved plan for approval, then seeds the approved steps into
5
5
  * the todo list and finalizes the session goal: a `planning` goal becomes the
6
- * plan's own `## Goal`, and a session with no goal is offered the same. The
6
+ * plan's own `## Goal`, and a session with no goal is given the same. The
7
7
  * review loop and the browser lifecycle belong to `review.ts`; the nested writes
8
8
  * belong to `todo-seed.ts` and the goal tool.
9
9
  */
@@ -19,7 +19,6 @@ 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
21
  import { failedResult } from "../../../lib/tool-result.ts";
22
- import { loadConfig } from "../config.ts";
23
22
  import { feedbackSummary } from "../feedback.ts";
24
23
  import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
25
24
  import { sanitizePlanText } from "../plan-text.ts";
@@ -35,7 +34,7 @@ import { seedTodos } from "./todo-seed.ts";
35
34
  const ExitPlanModeParams = Type.Object({
36
35
  plan_path: Type.String({
37
36
  description:
38
- "Path to the plan file written by write_plan (absolute, or relative to the working directory). The user reviews this file.",
37
+ "Path to the plan file written by write_plan: the absolute path it returned, or the `~/…` form it displayed. A bare name resolves inside the plans directory. The user reviews this file.",
39
38
  }),
40
39
  });
41
40
 
@@ -52,24 +51,18 @@ interface GoalOutcome {
52
51
  /**
53
52
  * Finalise the session goal from the approved plan.
54
53
  *
55
- * A `planning` goal needs no dialog — approving the plan *is* the decision — and a
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
58
- * `achieved`) is the user's own: approving ordinary work never rewrites it.
59
- * Recording runs through the goal tool, so validation and the widget stay goal's,
60
- * and no model turn is spent.
54
+ * Approving the plan *is* the decision, so neither a `planning` goal nor a
55
+ * session with none is asked again. A final goal (`active` or `achieved`) is the
56
+ * user's own: approving ordinary work never rewrites it. Recording runs through
57
+ * the goal tool when it is registered — validation and the widget stay goal's,
58
+ * and no model turn is spent — and stays silent when that sibling extension is
59
+ * not installed, because there is nothing to report and nothing to act on.
61
60
  */
62
- async function finalizeGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
61
+ async function finalizeGoal(pi: ExtensionAPI, ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
63
62
  const objective = planObjective(plan);
64
63
  const current = lastGoal(ctx.sessionManager.getBranch());
65
64
  if (current !== null && current.status !== "planning") return { objective };
66
- if (current === null) {
67
- if (!ctx.hasUI) 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
- }
72
- }
65
+ if (!pi.getAllTools().some((tool) => tool.name === GOAL_TOOL)) return { objective };
73
66
  const outcome = await ctx.executeTool(GOAL_TOOL, { objective, status: "active" });
74
67
  const action = current === null ? "recorded" : "promoted";
75
68
  return outcome.isError ? { objective, action, error: firstTextLine(outcome) } : { objective, action };
@@ -101,7 +94,8 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
101
94
  annotations: { readOnlyHint: true, openWorldHint: false },
102
95
 
103
96
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
104
- const planPath = (params as ExitPlanModeArgs).plan_path.trim();
97
+ const args = params as ExitPlanModeArgs;
98
+ const planPath = args.plan_path.trim();
105
99
 
106
100
  if (!runtime.isEnabled()) {
107
101
  return failedResult("Not in plan mode; there is no plan to approve.", {
@@ -220,7 +214,7 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
220
214
 
221
215
  runtime.disable(ctx);
222
216
  const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
223
- const goal = await finalizeGoal(ctx, decision.plan);
217
+ const goal = await finalizeGoal(pi, ctx, decision.plan);
224
218
  const listing =
225
219
  steps.length > 0
226
220
  ? `\n\n${steps
@@ -40,7 +40,7 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
40
40
  name: WRITE_PLAN_TOOL,
41
41
  label: "Write plan",
42
42
  description:
43
- "Save the plan you designed as a markdown file under .pi/plans (project root, or the agent directory outside a " +
43
+ "Save the plan you designed as a markdown file in the plans directory (the agent directory, keyed by " +
44
44
  "repository). Write the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
45
45
  `${PLAN_OUTLINE}\n\n` +
46
46
  "Each section has one job: lead it with its outcome, keep a prose section to a few bullets or two or three " +
@@ -51,10 +51,11 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
51
51
  "The outline is a guide, not a form: drop Context, Approach, Visual, or Risks when there is nothing to say. " +
52
52
  "Keep Steps and Verification — the steps are read from that section (without it, the shallowest list " +
53
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. " +
54
+ "In Steps, each top-level bullet is one outcome in the order you will do them; " +
55
+ "an indented line continues that outcome, and a deeper bullet is a note. " +
55
56
  "Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining. " +
56
57
  `A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it cannot track.`,
57
- promptSnippet: "Save the designed plan as a markdown file under .pi/plans.",
58
+ promptSnippet: "Save the designed plan as a markdown file in the plans directory.",
58
59
  promptGuidelines: [
59
60
  "While plan mode is active, write the full plan with write_plan, then call exit_plan_mode with the returned path.",
60
61
  "When refining with plan_path, keep every section heading as it was: the review marks changed sections by heading, " +
@@ -88,6 +89,10 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
88
89
  planPath: args.plan_path,
89
90
  });
90
91
  runtime.setLastPlan(ctx, file.path);
92
+ // A store an older version kept in the checkout was moved in by this
93
+ // write; say so once, where the user is looking.
94
+ const moved = runtime.plans.takeNotice();
95
+ if (moved) ctx.ui.notify(moved, "info");
91
96
  return textResult(
92
97
  `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
93
98
  { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
@@ -2,7 +2,7 @@
2
2
  * Model-facing registration for the plan-mode tools.
3
3
  *
4
4
  * `enter_plan_mode` lets the model ask to plan before touching code; it needs
5
- * user confirmation. `write_plan` saves the plan artifact under `.pi/plans` and
5
+ * user confirmation. `write_plan` saves the plan artifact in the plans directory and
6
6
  * returns its path. `cancel_plan_mode` leaves planning without approving, so a
7
7
  * phone user is never trapped in read-only mode. `exit_plan_mode` reads that file
8
8
  * back, presents it for approval (a scrollable review screen in the TUI, a select dialog elsewhere),
@@ -6,9 +6,11 @@
6
6
  * the active Markdown theme, so headings, lists, code, links, and quotes match
7
7
  * the rest of the application; plan-authored control characters are stripped
8
8
  * first so the file cannot drive the terminal. Task checkboxes are recolored
9
- * and the footer colors the primary actions. It returns the chosen action
10
- * through `onClose` so the caller can act
11
- * only after the screen is gone and input focus is free again.
9
+ * and the footer colors the primary actions. Approving leaves plan mode and
10
+ * starts the work, so it asks `y/N` first through the shared `ConfirmState`;
11
+ * the plan stays mounted, and `Esc` backs out of the question before it backs
12
+ * out of the review. It returns the chosen action through `onClose` so the
13
+ * caller can act only after the screen is gone and input focus is free again.
12
14
  */
13
15
 
14
16
  import { basename } from "node:path";
@@ -35,14 +37,14 @@ import {
35
37
  type ViewportRowsSource,
36
38
  } from "../../lib/tui/chrome.ts";
37
39
  import { isCloseKey } from "../../lib/tui/cursor.ts";
40
+ import { ConfirmState } from "../../lib/tui/confirm.ts";
38
41
  import { gaugeBar } from "../../lib/tui/gauge.ts";
39
42
  import { GLYPHS } from "../../lib/tui/glyphs.ts";
40
43
  import { plainRule, titleRule } from "../../lib/tui/rules.ts";
41
44
  import { emptyState, checkbox } from "../../lib/tui/rows.ts";
42
45
  import { paint } from "../../lib/tui/tokens.ts";
43
46
  import { planTitle, type StoredPlan } from "./plans.ts";
44
- import { changedSections, sanitizePlanText, type SectionDigest } from "./plan-text.ts";
45
- import { extractPlanSections } from "./steps.ts";
47
+ import { changedSections, planDisplayText, type SectionDigest } from "./plan-text.ts";
46
48
 
47
49
  /** What the user chose from the read/review screen. */
48
50
  export type PlanViewAction = "approve" | "refine" | "keep" | "browser" | "cancel";
@@ -175,6 +177,8 @@ export class PlanViewComponent implements Component {
175
177
  private cache: BodyCache | null = null;
176
178
  /** True while the inline refine editor owns the keyboard. */
177
179
  private editing = false;
180
+ /** The y/N approval question, shared with the other destructive screens. */
181
+ private readonly confirm = new ConfirmState();
178
182
  /** The inline refine editor, built lazily once a TUI handle is available. */
179
183
  private editor: Editor | null = null;
180
184
 
@@ -195,12 +199,11 @@ export class PlanViewComponent implements Component {
195
199
  const padding = width >= MIN_PADDED_WIDTH ? BODY_PADDING : 0;
196
200
  const base = this.options.markdownTheme ?? getMarkdownTheme();
197
201
  const theme = planMarkdownTheme(this.theme, base);
198
- const current = sanitizePlanText(this.options.plan.content);
199
- const changed = changedSections(this.options.baseline, current);
200
- const chunks = extractPlanSections(current);
202
+ const current = planDisplayText(this.options.plan.content);
203
+ const marks = changedSections(this.options.baseline, current);
201
204
  const lines: string[] = [];
202
205
  const sections: SectionOffset[] = [];
203
- chunks.forEach((chunk, index) => {
206
+ marks.forEach(({ section: chunk, changed }, index) => {
204
207
  // Markdown pads every line to the full width for backgrounds; this screen
205
208
  // draws no background, so the padding is dropped.
206
209
  const rendered = new Markdown(chunk.text, padding, 0, theme)
@@ -209,14 +212,14 @@ export class PlanViewComponent implements Component {
209
212
  sections.push({
210
213
  title: chunk.title,
211
214
  offset: lines.length,
212
- ...(changed[index] ? { changed: true } : {}),
215
+ ...(changed ? { changed: true } : {}),
213
216
  });
214
217
  lines.push(...rendered);
215
218
  // Sections render independently, so restore the blank line a heading-only
216
219
  // chunk omits before the next section.
217
- if (index < chunks.length - 1 && lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
220
+ if (index < marks.length - 1 && lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
218
221
  });
219
- this.cache = { width, lines, sections, changedCount: changed.filter(Boolean).length };
222
+ this.cache = { width, lines, sections, changedCount: marks.filter((mark) => mark.changed).length };
220
223
  return this.cache;
221
224
  }
222
225
 
@@ -314,6 +317,12 @@ export class PlanViewComponent implements Component {
314
317
  this.options.requestRender();
315
318
  return;
316
319
  }
320
+ // A pending question owns the keyboard, so a scroll key cannot slip past it
321
+ // and `Esc` backs out of the question rather than the review.
322
+ if (this.confirm.pending) {
323
+ if (this.confirm.resolve(data) !== undefined) this.options.requestRender();
324
+ return;
325
+ }
317
326
  if (isCloseKey(data)) {
318
327
  this.options.onClose("keep");
319
328
  return;
@@ -330,8 +339,10 @@ export class PlanViewComponent implements Component {
330
339
  else if (matchesKey(data, Key.end) || data === "G") this.scrollTo(this.maxOffset);
331
340
  else if (data === "n") this.jumpSection(1);
332
341
  else if (data === "p") this.jumpSection(-1);
333
- else if (data === "a") this.options.onClose("approve");
334
- else if (data === "r") this.startRefine();
342
+ else if (data === "a") {
343
+ this.confirm.ask("Approve and run the plan? y/N", () => this.options.onClose("approve"));
344
+ this.options.requestRender();
345
+ } else if (data === "r") this.startRefine();
335
346
  else if (data === "w") this.options.onClose("browser");
336
347
  else if (data === "x") this.options.onClose("cancel");
337
348
  }
@@ -400,10 +411,13 @@ export class PlanViewComponent implements Component {
400
411
  } else {
401
412
  if (this.maxOffset > 0) lines.push(scrollRow(theme, w, this.offset, end, body.length, sectionLabel));
402
413
  else lines.push("");
414
+ const pending = this.confirm.pending;
403
415
  const sectionHints = sections.length > 1 ? SECTION_HINTS : [];
404
416
  const hints: ScreenHint[] =
405
417
  this.maxOffset > 0 ? [...PRIMARY_HINTS, ...SCROLL_HINTS, ...sectionHints] : PRIMARY_HINTS;
406
- lines.push(screenHint(theme, w, hints));
418
+ lines.push(
419
+ pending ? truncateToWidth(` ${paint(theme, "warn", pending.prompt)}`, w) : screenHint(theme, w, hints),
420
+ );
407
421
  }
408
422
  lines.push(plainRule(theme, w));
409
423
  return lines;
@@ -6,6 +6,32 @@ import type { PlanAnnotation } from "./feedback.ts";
6
6
  import type { SectionDigest } from "./plan-text.ts";
7
7
  import type { StepUpdate } from "./steps.ts";
8
8
 
9
+ /**
10
+ * Who is looking at a running review: the session that opened it (`owner`, the
11
+ * only role that can approve), or someone the link was shared with (`reviewer`,
12
+ * who may comment).
13
+ */
14
+ export type ReviewRole = "owner" | "reviewer";
15
+
16
+ /**
17
+ * What a review token names: its role, and — for a reviewer — the name it posts
18
+ * under, bound the first time that link writes.
19
+ */
20
+ export interface ReviewCaller {
21
+ role: ReviewRole;
22
+ /** The reviewer's name, once this link has written; absent for the owner and
23
+ * for a reviewer that has not commented yet. */
24
+ name?: string;
25
+ }
26
+
27
+ /**
28
+ * One frame on the review's event stream. `comment`, `plan`, and `share` say a
29
+ * page should re-read the island; `closed` ends the review for everyone, and
30
+ * `revoked` tells a reviewer the owner stopped sharing. The frame carries no
31
+ * state of its own, so the island stays the one contract.
32
+ */
33
+ export type ReviewFrame = "comment" | "plan" | "share" | "closed" | "revoked";
34
+
9
35
  /** Persisted (custom entry) plan-mode state. */
10
36
  export interface PlanModeEntry {
11
37
  enabled: boolean;
@@ -38,7 +64,7 @@ export interface EnterPlanModeDetails {
38
64
  export interface WritePlanDetails {
39
65
  /** Absolute path of the written plan file. */
40
66
  path: string;
41
- /** Path relative to the working directory, when inside it. */
67
+ /** How the path is shown: `~`-shortened, since plans live outside the project. */
42
68
  relativePath: string;
43
69
  /** Size of the written plan in bytes. */
44
70
  bytes: number;
@@ -48,7 +74,7 @@ export interface WritePlanDetails {
48
74
  export interface SetPlanStepsDetails {
49
75
  /** Absolute path of the plan file that was updated. */
50
76
  path: string;
51
- /** Path relative to the working directory, for transcript display. */
77
+ /** How the path is shown: `~`-shortened. */
52
78
  relativePath: string;
53
79
  /** The checkbox changes that were applied, in call order. */
54
80
  updates: StepUpdate[];
@@ -66,7 +92,7 @@ export interface ExitPlanModeDetails {
66
92
  plan: string;
67
93
  /** Absolute path of the reviewed plan file. */
68
94
  planPath?: string;
69
- /** Path relative to the working directory, for transcript display. */
95
+ /** How the path is shown: `~`-shortened. */
70
96
  relativePath?: string;
71
97
  /** True when the user asked to refine the plan instead of approving. */
72
98
  refined?: boolean;
@@ -43,7 +43,7 @@ export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
43
43
  var count = annotations.length;
44
44
  label += " \u00b7 " + count + (count === 1 ? " comment" : " comments");
45
45
  if (dirty && !saved && !approved && !closed) {
46
- label += " \u00b7 unsaved";
46
+ label += " \u00b7 " + (role === "reviewer" ? "unposted" : "unsaved");
47
47
  if (!tone) tone = "warn";
48
48
  }
49
49
  stateEl.textContent = label;
@@ -76,6 +76,7 @@ export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
76
76
  function lockActions() {
77
77
  if (approveButton) approveButton.disabled = true;
78
78
  if (saveButton) saveButton.disabled = true;
79
+ if (postButton) postButton.disabled = true;
79
80
  if (cancelButton) cancelButton.disabled = true;
80
81
  }
81
82