@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
@@ -0,0 +1,269 @@
1
+ /**
2
+ * The review page's comment threads: selection quotes, the composer, the cards.
3
+ *
4
+ * A fragment of the same IIFE `web-client.ts` ships (`WEB_COMMENTS_JS` is
5
+ * interpolated into `REVIEW_CLIENT_JS`), kept apart so what a comment *is* — its
6
+ * anchor, its author, and who may edit it — is read in one place. It mutates the
7
+ * page's `annotations` array and asks the page to repaint; it owns no state.
8
+ */
9
+
10
+ export const WEB_COMMENTS_JS = String.raw` function selectionIn(wrap) {
11
+ var selection = window.getSelection && window.getSelection();
12
+ if (!selection || selection.isCollapsed) return "";
13
+ var range = selection.rangeCount ? selection.getRangeAt(0) : null;
14
+ if (!range || !wrap.contains(range.commonAncestorContainer)) return "";
15
+ return selection.toString().replace(/\s+/g, " ").trim().slice(0, limits.quote);
16
+ }
17
+
18
+ function commentsBoxAfter(wrap) {
19
+ var next = wrap.nextElementSibling;
20
+ if (next && next.classList && next.classList.contains("bf-comments")) return next;
21
+ var box = el("div", "bf-comments");
22
+ wrap.insertAdjacentElement("afterend", box);
23
+ return box;
24
+ }
25
+
26
+ // Enter submits a comment; Shift+Enter is left for the browser.
27
+ function submitOnEnter(area, submit) {
28
+ area.addEventListener("keydown", function (event) {
29
+ // An IME uses Enter to commit a composition; submitting then would send a
30
+ // half-typed comment.
31
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
32
+ event.preventDefault();
33
+ submit();
34
+ });
35
+ }
36
+
37
+ // One seam for "this writer changed a comment": the unsaved marker now, and the
38
+ // draft this tab keeps across a reload beside it.
39
+ function markDirty() {
40
+ dirty = true;
41
+ saveDraft();
42
+ }
43
+
44
+ // The draft is this tab's own: the comments this writer added and has not saved,
45
+ // and the text in a composer that has not become a comment yet, keyed by the plan
46
+ // revision the server rendered. It never travels to the server, and nothing
47
+ // another writer added is kept here.
48
+ var DRAFT_KEY = "bf-review-draft";
49
+ var DRAFT_LIMIT = 131072;
50
+
51
+ /**
52
+ * The open composer's block line, text, and the selection it quotes, or null when
53
+ * nothing is being typed. The quote is half of what the comment will say, so a
54
+ * draft that keeps the text and drops the quote would restore a different note.
55
+ */
56
+ function typingOf() {
57
+ var area = draftTextarea();
58
+ var wrap = area && area.closest ? area.closest(".bf-block") : null;
59
+ var anchor = wrap ? Number(wrap.getAttribute("data-line")) : -1;
60
+ if (!area || !(anchor >= 0) || area.value === "") return null;
61
+ var composer = area.closest ? area.closest(".bf-composer") : null;
62
+ return { anchor: anchor, text: area.value, quote: (composer && composer.getAttribute("data-quote")) || "" };
63
+ }
64
+
65
+ function saveDraft() {
66
+ if (saved || approved || closed) return;
67
+ var mine = annotations.filter(isMine);
68
+ var typing = typingOf();
69
+ if (mine.length === 0 && !typing) { clearDraft(); return; }
70
+ try {
71
+ var record = { rev: String(data.rev || ""), mine: mine };
72
+ if (typing) record.typing = typing;
73
+ var text = JSON.stringify(record);
74
+ if (text.length > DRAFT_LIMIT) return;
75
+ window.sessionStorage.setItem(DRAFT_KEY, text);
76
+ } catch (error) {}
77
+ }
78
+
79
+ function clearDraft() {
80
+ try { window.sessionStorage.removeItem(DRAFT_KEY); } catch (error) {}
81
+ }
82
+
83
+ /**
84
+ * Put this tab's unsaved work back: after a reload of the same plan, and after
85
+ * another writer's comment arrived and the island was re-read. A draft for a
86
+ * different plan revision is stale, so it is dropped rather than re-anchored.
87
+ * Returns the composer text to reopen, which needs the blocks to have rendered.
88
+ */
89
+ function restoreDraft() {
90
+ var raw = null;
91
+ try { raw = window.sessionStorage.getItem(DRAFT_KEY); } catch (error) { raw = null; }
92
+ if (!raw) return null;
93
+ var draft = null;
94
+ try { draft = JSON.parse(raw); } catch (error) { draft = null; }
95
+ var mine = draft && Array.isArray(draft.mine) ? draft.mine : [];
96
+ var stored = draft && draft.typing && typeof draft.typing.text === "string" ? draft.typing : null;
97
+ // Rebuilt field by field: storage is hand-editable, and a chip rendered from a
98
+ // non-string quote would read as [object Object].
99
+ var typing =
100
+ stored && typeof stored.anchor === "number"
101
+ ? { anchor: stored.anchor, text: stored.text, quote: typeof stored.quote === "string" ? stored.quote : "" }
102
+ : null;
103
+ if (!draft || draft.rev !== String(data.rev || "") || (mine.length === 0 && !typing)) {
104
+ clearDraft();
105
+ return null;
106
+ }
107
+ if (mine.length > 0) {
108
+ annotations = annotations.filter(function (annotation) { return !isMine(annotation); }).concat(mine);
109
+ dirty = true;
110
+ }
111
+ return typing;
112
+ }
113
+
114
+ function openComposer(wrap, block, quote, initial) {
115
+ closeComposer();
116
+ wrap.classList.add("bf-block--composing");
117
+ var composer = el("div", "bf-composer");
118
+ if (quote) {
119
+ var chip = el("div", "bf-quote-chip");
120
+ chip.textContent = "\u201c" + quote + "\u201d";
121
+ composer.appendChild(chip);
122
+ // The chip is the quote's only record; the draft reads it back from here.
123
+ composer.setAttribute("data-quote", quote);
124
+ }
125
+ var area = el("textarea", "bf-textarea");
126
+ area.placeholder = "Leave feedback on this part of the plan (Enter to add)";
127
+ area.setAttribute("aria-label", "Comment");
128
+ area.maxLength = limits.comment;
129
+ if (initial) area.value = initial;
130
+ // Every keystroke is this tab's draft, so a reload while typing costs nothing.
131
+ area.addEventListener("input", function () { saveDraft(); });
132
+ composer.appendChild(area);
133
+ var row = el("div", "bf-composer__row");
134
+ var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
135
+ cancel.addEventListener("click", closeComposer);
136
+ var add = el("button"); add.type = "button"; add.className = "bf-btn bf-btn--primary"; add.textContent = "Add comment";
137
+ var submit = function () {
138
+ if (closed) { setStatus("The review is closed; this comment was not added.", "error"); return; }
139
+ if (role === "reviewer" && !reviewerName) { askName(); return; }
140
+ var comment = area.value.replace(/\s+/g, " ").trim();
141
+ if (!comment) { area.focus(); return; }
142
+ if (annotations.length >= limits.comments) { setStatus("Comment limit reached (" + limits.comments + ").", "error"); return; }
143
+ var entry = quote ? { anchor: block.start, quote: quote, comment: comment } : { anchor: block.start, comment: comment };
144
+ if (role === "reviewer") entry.author = reviewerName;
145
+ annotations.push(entry);
146
+ // Close first: the text is now a comment, so the draft keeps one or the other.
147
+ closeComposer();
148
+ markDirty();
149
+ setStatus("Comment added.");
150
+ renderPlan();
151
+ };
152
+ add.addEventListener("click", submit);
153
+ submitOnEnter(area, submit);
154
+ row.appendChild(cancel); row.appendChild(add); composer.appendChild(row);
155
+ commentsBoxAfter(wrap).appendChild(composer);
156
+ area.focus();
157
+ // A restored draft resumes where it left off, with the caret at its end.
158
+ if (initial) area.setSelectionRange(area.value.length, area.value.length);
159
+ }
160
+
161
+ function startEdit(card, index) {
162
+ var annotation = annotations[index];
163
+ card.textContent = "";
164
+ var area = el("textarea", "bf-textarea");
165
+ area.value = annotation.comment;
166
+ area.setAttribute("aria-label", "Edit comment");
167
+ area.maxLength = limits.comment;
168
+ card.appendChild(area);
169
+ var row = el("div", "bf-comment__row");
170
+ var cancel = el("button"); cancel.type = "button"; cancel.className = "bf-btn"; cancel.textContent = "Cancel";
171
+ cancel.addEventListener("click", renderPlan);
172
+ var apply = el("button"); apply.type = "button"; apply.className = "bf-btn bf-btn--primary"; apply.textContent = "Save";
173
+ apply.addEventListener("click", function () {
174
+ var comment = area.value.replace(/\s+/g, " ").trim();
175
+ if (!comment) { area.focus(); return; }
176
+ annotation.comment = comment;
177
+ markDirty();
178
+ setStatus("Comment updated.");
179
+ renderPlan();
180
+ });
181
+ submitOnEnter(area, function () { apply.click(); });
182
+ row.appendChild(cancel); row.appendChild(apply); card.appendChild(row);
183
+ area.focus();
184
+ }
185
+
186
+ // Deleting a comment is one click, so it is undoable rather than confirmed: the
187
+ // removed note is held for a few seconds and offered back in the header.
188
+ var undo = null;
189
+
190
+ function clearUndo() {
191
+ if (undo && undo.timer) window.clearTimeout(undo.timer);
192
+ if (undo && undo.button && undo.button.parentNode) undo.button.parentNode.removeChild(undo.button);
193
+ undo = null;
194
+ }
195
+
196
+ function offerUndo(index, annotation) {
197
+ var controls = document.querySelector(".review-controls");
198
+ clearUndo();
199
+ if (!controls) return;
200
+ var button = el("button", "bf-btn");
201
+ button.type = "button";
202
+ button.id = "undo-delete";
203
+ button.textContent = "Undo delete";
204
+ button.addEventListener("click", function () {
205
+ annotations.splice(Math.min(index, annotations.length), 0, annotation);
206
+ markDirty();
207
+ clearUndo();
208
+ renderPlan();
209
+ setStatus("Comment restored.", "saved");
210
+ });
211
+ controls.appendChild(button);
212
+ undo = { button: button, timer: window.setTimeout(clearUndo, 6000) };
213
+ }
214
+
215
+ function removeComment(index) {
216
+ var annotation = annotations[index];
217
+ if (!annotation) return;
218
+ annotations.splice(index, 1);
219
+ markDirty();
220
+ renderPlan();
221
+ offerUndo(index, annotation);
222
+ setStatus("Comment deleted.");
223
+ }
224
+
225
+ // A comment belongs to whoever wrote it: an untagged one is the owner's, a
226
+ // tagged one is that reviewer's. Everyone else's is read-only on this page.
227
+ function isMine(annotation) {
228
+ if (role === "owner") return !annotation.author;
229
+ return !!reviewerName && annotation.author === reviewerName;
230
+ }
231
+
232
+ function renderCommentCards(box, list) {
233
+ list.forEach(function (annotation) {
234
+ var index = annotations.indexOf(annotation);
235
+ var card = el("div", "bf-comment");
236
+ var head = el("div", "bf-comment__head");
237
+ var label = el("span", annotation.author ? "bf-comment__label bf-comment__label--author" : "bf-comment__label");
238
+ label.textContent = annotation.author ? "@" + annotation.author : "Review";
239
+ head.appendChild(label);
240
+ head.appendChild(el("span", "bf-comment__spacer"));
241
+ if (isMine(annotation)) {
242
+ var actions = el("div", "bf-comment__actions");
243
+ var edit = el("button"); edit.type = "button"; edit.className = "bf-btn"; edit.textContent = "Edit";
244
+ edit.addEventListener("click", function () { startEdit(card, index); });
245
+ var remove = el("button"); remove.type = "button"; remove.className = "bf-btn"; remove.textContent = "Delete";
246
+ remove.addEventListener("click", function () { removeComment(index); });
247
+ actions.appendChild(edit); actions.appendChild(remove); head.appendChild(actions);
248
+ }
249
+ card.appendChild(head);
250
+ if (annotation.quote) {
251
+ var quote = el("div", "bf-comment__quote");
252
+ quote.textContent = "\u201c" + annotation.quote + "\u201d";
253
+ card.appendChild(quote);
254
+ }
255
+ var body = el("div", "bf-comment__body");
256
+ body.textContent = annotation.comment;
257
+ card.appendChild(body);
258
+ box.appendChild(card);
259
+ });
260
+ }
261
+
262
+ function renderComments(container, anchor) {
263
+ var mine = annotations.filter(function (annotation) { return annotation.anchor === anchor; });
264
+ if (mine.length === 0) return;
265
+ var box = el("div", "bf-comments");
266
+ renderCommentCards(box, mine);
267
+ container.appendChild(box);
268
+ }
269
+ `;
@@ -27,6 +27,8 @@ import {
27
27
  } from "../../lib/web/index.ts";
28
28
  import type { PlanBlock } from "./blocks.ts";
29
29
  import type { PlanAnnotation } from "./feedback.ts";
30
+ import type { ReviewRole } from "./types.ts";
31
+ import type { ShareState } from "./web-share.ts";
30
32
 
31
33
  /** A heading the outline can jump to. */
32
34
  export interface ReviewOutlineItem {
@@ -67,8 +69,24 @@ export interface ReviewPageData {
67
69
  limits: { comments: number; comment: number; quote: number };
68
70
  /** Whether the review may approve the plan (only the `exit_plan_mode` flow). */
69
71
  canApprove: boolean;
72
+ /** Who is looking: `owner` may approve, `reviewer` may only comment. */
73
+ role: ReviewRole;
74
+ /**
75
+ * The name a reviewer's link posts under, once it has been bound. Absent for the
76
+ * owner and for a reviewer that has not written yet, which is what tells the page
77
+ * whether its name control is still a choice.
78
+ */
79
+ author?: string;
80
+ /** A running share, so a reload re-renders the link the owner already minted. */
81
+ share?: ShareState;
70
82
  /** Per-session secret the client echoes on every request. */
71
83
  token: string;
84
+ /**
85
+ * A short digest of the plan text the page rendered. It keys the unsaved draft a
86
+ * tab keeps for itself, so a reload restores the comments this writer has not
87
+ * saved yet, and a plan that changed underneath drops them instead.
88
+ */
89
+ rev: string;
72
90
  }
73
91
 
74
92
  /** Page styles: the shared base + theme + shell + components, then the review. */
@@ -76,6 +94,15 @@ export const REVIEW_CSS = `${WEB_BASE_CSS}
76
94
  ${WEB_THEME_CSS}
77
95
  ${WEB_LAYOUT_CSS}
78
96
  ${WEB_COMPONENTS_CSS}
97
+ .review-share__list{list-style:none;margin:0 0 var(--space-3);padding:0;display:flex;flex-direction:column;gap:var(--space-2)}
98
+ .review-share__row{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);border:1px solid var(--border);border-radius:var(--radius-md)}
99
+ .review-share__url{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:var(--fs-sm) var(--font-mono);color:var(--text)}
100
+ .review-share__who{flex:0 0 auto;font:var(--fs-xs) var(--font-mono);color:var(--muted)}
101
+ .review-share__who--bound{color:var(--accent)}
102
+ .review-share__actions{display:flex;flex:0 0 auto;gap:4px}
103
+ .review-share__actions .bf-btn{padding:1px 8px;font-size:var(--fs-xs)}
104
+ .review-share__qr{display:flex;justify-content:center;margin:0 0 var(--space-3)}
105
+ .review-share__qr svg{width:168px;height:168px;background:#fff;border-radius:var(--radius-md);padding:var(--space-1)}
79
106
  .bf-review{--review-measure:1012px;--review-rail:26px;--review-gutter:34px;flex:1 1 auto;padding:var(--space-5) var(--space-6) 120px}
80
107
  /* The document is one content column at GitHub's Markdown body width, with the
81
108
  comment rail inside it, so a plan reads the way the same file does on GitHub. */
@@ -83,9 +110,9 @@ ${WEB_COMPONENTS_CSS}
83
110
  hairline reading-progress bar pinned to the header's bottom edge. */
84
111
  .review-bar{align-items:center;gap:var(--space-2) var(--space-4)}
85
112
  .review-bar .bf-brand{flex:0 0 auto}
86
- .review-heading{display:flex;align-items:center;flex:1 1 160px;min-width:0;overflow:hidden;border-left:1px solid var(--border);padding-left:var(--space-4)}
87
- .review-title{display:block;flex:1 1 auto;min-width:0;font-size:var(--fs-lg);font-weight:var(--fw-semibold);line-height:var(--lh-tight);color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
88
- .review-state{flex:0 0 auto;white-space:nowrap}
113
+ .review-heading{display:flex;align-items:center;flex:1 1 240px;min-width:0;overflow:hidden;border-left:1px solid var(--border);padding-left:var(--space-4)}
114
+ .review-title{display:block;flex:1 1 auto;min-width:8ch;font-size:var(--fs-lg);font-weight:var(--fw-semibold);line-height:var(--lh-tight);color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
115
+ .review-state{flex:0 0 auto;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
89
116
  .review-status{flex:0 0 auto;max-width:42ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
90
117
  .review-status:empty{display:none}
91
118
  .review-controls{display:inline-flex;align-items:center;gap:var(--space-2);flex:0 0 auto}
@@ -150,6 +177,8 @@ ${WEB_COMPONENTS_CSS}
150
177
  .bf-comment{padding:var(--space-3) var(--space-4)}
151
178
  .bf-comment__head{display:flex;align-items:center;gap:var(--space-2);margin-bottom:6px}
152
179
  .bf-comment__label{font:var(--fw-semibold) var(--fs-xs) var(--font-mono);color:var(--warn);text-transform:uppercase;letter-spacing:.06em}
180
+ /* A reviewer's name is a name, not a label: keep its own capitalisation. */
181
+ .bf-comment__label--author{text-transform:none;letter-spacing:0}
153
182
  .bf-comment__spacer{flex:1 1 auto}
154
183
  .bf-comment__actions{display:flex;gap:4px}
155
184
  .bf-comment__actions .bf-btn{padding:1px 8px;font-size:var(--fs-xs)}
@@ -171,12 +200,21 @@ ${WEB_COMPONENTS_CSS}
171
200
  .bf-review{padding:var(--space-4) 18px 80px}
172
201
  .review-nav__title{display:none}
173
202
  }
174
- @media (max-width:820px){
203
+ /* The controls are the widest thing in the bar — seven of them once a review can
204
+ be shared — so the title moves to its own row while they still need a row of
205
+ their own, rather than being squeezed to nothing beside it. */
206
+ @media (max-width:1120px){
175
207
  .review-bar{align-items:flex-start}
176
208
  .review-heading{flex:1 1 100%;order:3;border-left:0;padding-left:0;margin-top:var(--space-1)}
177
209
  .review-controls{order:2;margin-left:auto}
178
210
  .review-status{order:4;flex:1 1 100%;max-width:none;white-space:normal}
179
211
  }
212
+ /* Below a phone's width the controls wrap instead of running off the page. */
213
+ @media (max-width:640px){
214
+ .review-controls{flex-wrap:wrap;justify-content:flex-end;max-width:100%}
215
+ .review-heading{gap:var(--space-2)}
216
+ .review-title{min-width:0}
217
+ }
180
218
  @media (prefers-reduced-motion:no-preference){.bf-block{transition:background var(--motion-fast) var(--motion-ease)}}
181
219
  @media (hover:none){.bf-block__add{opacity:1}}
182
220
  /* On paper the document is the whole page: no comment rail to hover, no composer,
@@ -195,21 +233,26 @@ ${WEB_COMPONENTS_CSS}
195
233
  * for first and defers to the shortcut sheet (`?`) for the rest, so the line does
196
234
  * not have to grow every time a binding is added.
197
235
  */
198
- function reviewHints(canApprove: boolean): string {
236
+ function reviewHints(role: ReviewRole, canApprove: boolean): string {
199
237
  const approve = canApprove ? `, <kbd class="bf-kbd">a</kbd> to approve` : "";
200
- return `<p class="review-hints">Press <kbd class="bf-kbd">j</kbd>/<kbd class="bf-kbd">k</kbd> to move, <kbd class="bf-kbd">c</kbd> to comment${approve}, <kbd class="bf-kbd">s</kbd> to save, <kbd class="bf-kbd">?</kbd> for all shortcuts.</p>`;
238
+ const save = role === "owner" ? "save" : "post";
239
+ return `<p class="review-hints">Press <kbd class="bf-kbd">j</kbd>/<kbd class="bf-kbd">k</kbd> to move, <kbd class="bf-kbd">c</kbd> to comment${approve}, <kbd class="bf-kbd">s</kbd> to ${save}, <kbd class="bf-kbd">?</kbd> for all shortcuts.</p>`;
201
240
  }
202
241
 
203
242
  /** The shortcut sheet's rows: the review's own keys, led by approve when offered. */
204
- function reviewKeys(canApprove: boolean): KeyHint[] {
243
+ function reviewKeys(role: ReviewRole, canApprove: boolean): KeyHint[] {
205
244
  const rows: KeyHint[] = [];
206
245
  if (canApprove) rows.push({ keys: "a", label: "Approve and run the plan" });
207
246
  rows.push(
208
247
  { keys: ["j", "k"], label: "Next and previous block" },
209
248
  { keys: "c", label: "Comment on the current block" },
210
249
  { keys: "Enter", label: "Add the comment you are writing" },
211
- { keys: "s", label: "Save feedback and return to Pi" },
212
- { keys: "Esc", label: "Close the composer, the sheet, or the review" },
250
+ role === "owner"
251
+ ? { keys: "s", label: "Save feedback and return to Pi" }
252
+ : { keys: "s", label: "Post your comments to the plan" },
253
+ role === "owner"
254
+ ? { keys: "Esc", label: "Close the composer, the sheet, or the review" }
255
+ : { keys: "Esc", label: "Close the composer or this sheet" },
213
256
  { keys: "?", label: "Toggle this sheet" },
214
257
  );
215
258
  return rows;
@@ -231,6 +274,33 @@ function approveDialog(): string {
231
274
  </div>`;
232
275
  }
233
276
 
277
+ /**
278
+ * The owner's Share dialog: one row per reviewer link, the QR of the link just
279
+ * minted, and the two actions that change the set. The list is empty on purpose —
280
+ * the client fills it after the server mints a token, so a link never travels in
281
+ * the page source.
282
+ */
283
+ function shareDialog(): string {
284
+ return `<div class="bf-dialog" id="share-dialog" hidden>
285
+ <div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="share-title">
286
+ <div class="bf-dialog__head"><h2 id="share-title">Share this review</h2><span class="bf-spacer"></span><button type="button" class="bf-btn" id="share-close">Close</button></div>
287
+ <div class="bf-dialog__body"><ul class="review-share__list" id="share-links"></ul><div class="review-share__qr" id="share-qr"></div><p class="bf-dialog__hint" id="share-note"></p></div>
288
+ <div class="bf-dialog__actions"><button type="button" class="bf-btn" id="share-stop">Stop sharing</button><button type="button" class="bf-btn bf-btn--primary" id="share-new">New link</button></div>
289
+ </div>
290
+ </div>`;
291
+ }
292
+
293
+ /** The reviewer's name dialog: one line, which the link keeps after the first post. */
294
+ function nameDialog(): string {
295
+ return `<div class="bf-dialog" id="name-dialog" hidden>
296
+ <div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="name-title">
297
+ <div class="bf-dialog__head"><h2 id="name-title">Your name</h2></div>
298
+ <div class="bf-dialog__body"><p>Your comments are signed with this name, and this link keeps it after your first post.</p><p class="bf-field"><input class="bf-input" id="name-input" maxlength="40" placeholder="alice" aria-labelledby="name-title"></p></div>
299
+ <div class="bf-dialog__actions"><button type="button" class="bf-btn" id="name-cancel">Cancel</button><button type="button" class="bf-btn bf-btn--primary" id="name-save">Save name</button></div>
300
+ </div>
301
+ </div>`;
302
+ }
303
+
234
304
  /**
235
305
  * Render the review page for `data`. The markup is a fixed shell; the client
236
306
  * fills every dynamic field, so this function is the only place that needs the
@@ -246,18 +316,28 @@ export function renderReviewPage(data: ReviewPageData): string {
246
316
  <div class="bf-panel review-panel">
247
317
  ${reviewFacts()}
248
318
  <nav class="review-nav" aria-labelledby="outline-title"><p class="review-nav__title" id="outline-title">Sections</p><ul id="outline-list"></ul></nav>
249
- ${reviewHints(data.canApprove)}
319
+ ${reviewHints(data.role, data.canApprove)}
250
320
  </div>
251
321
  </details>
252
322
  `
253
323
  : "";
254
- // Approve is the decisive action, so it takes the primary button and Save is
255
- // demoted; a comment-only review has no approve button and keeps Save primary.
324
+ // Approve is the decisive action, so it takes the primary button and the
325
+ // owner's Save is demoted; a comment-only review has no approve button and
326
+ // keeps Save primary. A reviewer's one write action is Post, and nothing on
327
+ // their page closes the review or mints another link.
328
+ const isOwner = data.role === "owner";
256
329
  const approveButton = data.canApprove
257
330
  ? `
258
331
  <button type="button" class="bf-btn bf-btn--primary" id="approve">Approve &amp; run</button>`
259
332
  : "";
260
- const saveButton = `<button type="button" class="${data.canApprove ? "bf-btn" : "bf-btn bf-btn--primary"}" id="save">Save feedback</button>`;
333
+ const saveControl = isOwner
334
+ ? `<button type="button" class="${data.canApprove ? "bf-btn" : "bf-btn bf-btn--primary"}" id="save">Save feedback</button>`
335
+ : `<button type="button" class="bf-btn bf-btn--primary" id="post">Post comments</button>`;
336
+ const closeControl = isOwner ? `<button type="button" class="bf-btn" id="cancel">Back to Pi</button>` : "";
337
+ // Sharing belongs to the owner; a reviewer gets the name they comment under.
338
+ const shareControl = isOwner
339
+ ? `<button type="button" class="bf-btn" id="share">Share</button>`
340
+ : `<button type="button" class="bf-btn" id="reviewer-name" hidden></button>`;
261
341
  return `<!doctype html>
262
342
  <html lang="en">
263
343
  <head>
@@ -274,16 +354,15 @@ ${brandLockup("plan review")}
274
354
  <span class="bf-status review-status" id="status" role="status" aria-live="polite"></span>
275
355
  <div class="review-controls">
276
356
  ${themeToggle()}
277
- <button type="button" class="bf-btn bf-btn--icon" id="shortcuts" aria-label="Keyboard shortcuts" title="Keyboard shortcuts">?</button>${approveButton}
278
- ${saveButton}
279
- <button type="button" class="bf-btn" id="cancel">Back to Pi</button>
357
+ <button type="button" class="bf-btn bf-btn--icon" id="shortcuts" aria-label="Keyboard shortcuts" title="Keyboard shortcuts">?</button>${shareControl}${approveButton}${saveControl}${closeControl}
280
358
  </div>
281
359
  </div><div class="review-progress" aria-hidden="true"><span class="review-progress__fill" id="progress-fill"></span></div></header>
282
360
  <div class="${bodyClass}">
283
361
  ${outlinePanel}<main class="bf-app__main bf-review" id="plan" aria-label="Plan"></main>
284
362
  </div>
285
- ${keysSheet(reviewKeys(data.canApprove))}
363
+ ${keysSheet(reviewKeys(data.role, data.canApprove))}
286
364
  ${data.canApprove ? approveDialog() : ""}
365
+ ${data.role === "owner" ? shareDialog() : nameDialog()}
287
366
  <script type="application/json" id="review-data">${island}</script>
288
367
  <script src="/client.js" defer></script>
289
368
  </body>