@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 (324) hide show
  1. package/README.md +66 -52
  2. package/README.zh-CN.md +56 -46
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +507 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
  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 +561 -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} +62 -52
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +298 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +254 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
  25. package/extensions/board/commands.ts +105 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +170 -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 +388 -0
  32. package/extensions/board/result.ts +108 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +90 -0
  35. package/extensions/board/seat.ts +28 -0
  36. package/extensions/board/snapshot.ts +42 -0
  37. package/extensions/board/tools.ts +444 -0
  38. package/extensions/board/transport/router.ts +91 -0
  39. package/extensions/board/transport/server.ts +138 -0
  40. package/extensions/board/types.ts +121 -0
  41. package/extensions/board/view/client.ts +51 -0
  42. package/extensions/board/view/dialog.ts +43 -0
  43. package/extensions/board/view/keys.ts +125 -0
  44. package/extensions/board/view/live.ts +231 -0
  45. package/extensions/board/view/names.ts +91 -0
  46. package/extensions/board/view/page.ts +118 -0
  47. package/extensions/board/view/render.ts +519 -0
  48. package/extensions/board/view/styles.ts +228 -0
  49. package/extensions/board/view/team.ts +327 -0
  50. package/extensions/bytefly/footer/budget.ts +6 -5
  51. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  52. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  53. package/extensions/bytefly/footer/config.ts +19 -8
  54. package/extensions/bytefly/footer/price/price.ts +30 -8
  55. package/extensions/bytefly/footer/register.ts +5 -5
  56. package/extensions/bytefly/footer/render/format.ts +18 -0
  57. package/extensions/bytefly/footer/render/lines.ts +23 -7
  58. package/extensions/bytefly/footer/render/segments.ts +117 -31
  59. package/extensions/bytefly/footer/types.ts +29 -5
  60. package/extensions/bytefly/footer/unpriced.ts +42 -17
  61. package/extensions/bytefly/panel/commands.ts +67 -7
  62. package/extensions/bytefly/panel/filter.ts +68 -0
  63. package/extensions/bytefly/panel/format.ts +16 -15
  64. package/extensions/bytefly/panel/join.ts +56 -13
  65. package/extensions/bytefly/panel/prices.ts +43 -0
  66. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  67. package/extensions/bytefly/panel/settings.ts +75 -43
  68. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  69. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  70. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  71. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  72. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  73. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  74. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  75. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  76. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  77. package/extensions/bytefly/panel/tui.ts +306 -259
  78. package/extensions/bytefly/picker/discovery.ts +0 -2
  79. package/extensions/bytefly/picker/types.ts +0 -2
  80. package/extensions/bytefly/update/check.ts +39 -0
  81. package/extensions/bytefly/update/format.ts +15 -0
  82. package/extensions/bytefly/update/register.ts +73 -34
  83. package/extensions/bytefly/update/state.ts +6 -3
  84. package/extensions/check/commands.ts +2 -1
  85. package/extensions/context/tui.ts +1 -1
  86. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  87. package/extensions/file-browse/files/classify.ts +136 -0
  88. package/extensions/file-browse/files/files.ts +12 -0
  89. package/extensions/file-browse/files/index.ts +90 -0
  90. package/extensions/file-browse/files/list.ts +261 -0
  91. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  92. package/extensions/file-browse/files/tree.ts +101 -0
  93. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  94. package/extensions/file-browse/index.ts +4 -4
  95. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  96. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
  97. package/extensions/file-browse/transport/server.ts +110 -0
  98. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  99. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  100. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  101. package/extensions/file-browse/view/emit.ts +51 -0
  102. package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
  103. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  104. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
  105. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
  106. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  107. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  108. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  109. package/extensions/file-read/schema.ts +2 -1
  110. package/extensions/job/runtime.ts +4 -4
  111. package/extensions/memory/store.ts +12 -5
  112. package/extensions/plan/blocks.ts +23 -2
  113. package/extensions/plan/commands.ts +21 -16
  114. package/extensions/plan/feedback.ts +185 -52
  115. package/extensions/plan/format.ts +10 -3
  116. package/extensions/plan/index.ts +23 -5
  117. package/extensions/plan/plan-files.ts +88 -0
  118. package/extensions/plan/plan-index.ts +43 -9
  119. package/extensions/plan/plan-text.ts +33 -23
  120. package/extensions/plan/plans.ts +27 -34
  121. package/extensions/plan/policy.ts +30 -17
  122. package/extensions/plan/review.ts +3 -27
  123. package/extensions/plan/runtime.ts +52 -11
  124. package/extensions/plan/sections.ts +112 -0
  125. package/extensions/plan/steps.ts +3 -22
  126. package/extensions/plan/todos.ts +51 -3
  127. package/extensions/plan/tools/exit.ts +20 -95
  128. package/extensions/plan/tools/todo-seed.ts +4 -2
  129. package/extensions/plan/tools/write.ts +12 -1
  130. package/extensions/plan/tui.ts +5 -34
  131. package/extensions/plan/types.ts +15 -8
  132. package/extensions/plan/web-blocks.ts +89 -0
  133. package/extensions/plan/web-chrome.ts +3 -5
  134. package/extensions/plan/web-client.ts +179 -330
  135. package/extensions/plan/web-collab.ts +3 -3
  136. package/extensions/plan/web-comments.ts +18 -8
  137. package/extensions/plan/web-composer.ts +80 -0
  138. package/extensions/plan/web-draft.ts +40 -0
  139. package/extensions/plan/web-keys.ts +134 -0
  140. package/extensions/plan/web-outline.ts +192 -0
  141. package/extensions/plan/web-page.ts +4 -8
  142. package/extensions/plan/web-plan.ts +150 -0
  143. package/extensions/plan/web-router.ts +3 -58
  144. package/extensions/plan/web-settle.ts +194 -0
  145. package/extensions/plan/web-share-session.ts +24 -30
  146. package/extensions/plan/web-streams.ts +14 -14
  147. package/extensions/plan/web-tokens.ts +2 -2
  148. package/extensions/plan/web.ts +66 -120
  149. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  150. package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
  151. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  152. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  153. package/extensions/rewind/index.ts +3 -3
  154. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  155. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  156. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  157. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  158. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  159. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  160. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  161. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  162. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  163. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  164. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  165. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  166. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  167. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  168. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  169. package/extensions/session-bus/commands.ts +17 -69
  170. package/extensions/session-bus/format.ts +22 -45
  171. package/extensions/session-bus/index.ts +13 -7
  172. package/extensions/session-bus/join-request.ts +40 -0
  173. package/extensions/session-bus/mail-watch.ts +134 -0
  174. package/extensions/session-bus/notice.ts +32 -4
  175. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  176. package/extensions/session-bus/protocol.ts +19 -49
  177. package/extensions/session-bus/resolve.ts +67 -0
  178. package/extensions/session-bus/result.ts +16 -51
  179. package/extensions/session-bus/rows.ts +11 -5
  180. package/extensions/session-bus/runtime.ts +220 -267
  181. package/extensions/session-bus/store.ts +55 -7
  182. package/extensions/session-bus/tools.ts +31 -66
  183. package/extensions/session-bus/tui.ts +5 -5
  184. package/extensions/session-bus/types.ts +23 -14
  185. package/extensions/subagent/agents.ts +9 -8
  186. package/extensions/subagent/commands.ts +2 -1
  187. package/extensions/subagent/invocation.ts +4 -2
  188. package/extensions/team/commands.ts +145 -0
  189. package/extensions/team/dispatch.ts +133 -0
  190. package/extensions/team/format.ts +119 -0
  191. package/extensions/team/index.ts +34 -0
  192. package/extensions/team/schema.ts +29 -0
  193. package/extensions/team/tools.ts +267 -0
  194. package/extensions/team/types.ts +65 -0
  195. package/extensions/todo/index.ts +4 -1
  196. package/extensions/todo/nudge.ts +8 -2
  197. package/extensions/web-access/fetch/page.ts +4 -1
  198. package/extensions/web-access/fetch/structured.ts +447 -0
  199. package/extensions/web-access/fetch/tool.ts +2 -1
  200. package/extensions/web-access/search/tool.ts +2 -1
  201. package/extensions/wechat/bridge.ts +106 -251
  202. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  203. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  204. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  205. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  206. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  207. package/extensions/wechat/dialog/channels.ts +136 -0
  208. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  209. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  210. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  211. package/extensions/wechat/index.ts +10 -10
  212. package/extensions/wechat/intake.ts +245 -0
  213. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  214. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  215. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  216. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  217. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  218. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  219. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  220. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  221. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  222. package/extensions/wechat/tools.ts +29 -12
  223. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  224. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  225. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  226. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  227. package/extensions/wechat/transport/outbound.ts +142 -0
  228. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  229. package/extensions/wechat/types.ts +10 -1
  230. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  231. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  232. package/extensions/worktree/commands.ts +24 -17
  233. package/extensions/worktree/exit.ts +12 -6
  234. package/extensions/worktree/git.ts +22 -14
  235. package/extensions/worktree/guard.ts +18 -290
  236. package/extensions/worktree/handoff.ts +10 -2
  237. package/extensions/worktree/index.ts +2 -1
  238. package/extensions/worktree/lifecycle.ts +2 -0
  239. package/extensions/worktree/registry.ts +20 -11
  240. package/extensions/worktree/root-tools.ts +13 -3
  241. package/extensions/worktree/runtime.ts +8 -2
  242. package/extensions/worktree/screen.ts +15 -8
  243. package/extensions/worktree/shell.ts +303 -0
  244. package/extensions/worktree/state.ts +1 -1
  245. package/extensions/worktree/status.ts +42 -9
  246. package/extensions/worktree/tools.ts +19 -13
  247. package/lib/README.md +11 -8
  248. package/lib/board/card.ts +479 -0
  249. package/lib/board/links.ts +141 -0
  250. package/lib/board/page-pointer.ts +310 -0
  251. package/lib/board/round-text.ts +81 -0
  252. package/lib/board/round.ts +94 -0
  253. package/lib/board/rules.ts +437 -0
  254. package/lib/board/session.ts +105 -0
  255. package/lib/board/store.ts +497 -0
  256. package/lib/board/view.ts +234 -0
  257. package/lib/errors.ts +12 -4
  258. package/lib/format.ts +35 -0
  259. package/lib/http/guard.ts +112 -0
  260. package/lib/http/index.ts +16 -5
  261. package/lib/http/serve.ts +237 -0
  262. package/lib/http/server.ts +12 -10
  263. package/lib/http/sink.ts +135 -0
  264. package/lib/http/token.ts +38 -0
  265. package/lib/plan-entry.ts +11 -0
  266. package/lib/policy.ts +25 -2
  267. package/lib/requirements.ts +6 -7
  268. package/lib/session-label.ts +64 -0
  269. package/lib/session-roster.ts +201 -0
  270. package/lib/session-tool.ts +184 -0
  271. package/lib/spawn.ts +37 -3
  272. package/lib/stray-fields.ts +45 -0
  273. package/lib/tool-names.ts +38 -2
  274. package/lib/tui/glyphs.ts +3 -2
  275. package/lib/web/README.md +57 -4
  276. package/lib/web/client-runtime.ts +123 -0
  277. package/lib/web/components.ts +5 -0
  278. package/lib/web/index.ts +4 -2
  279. package/lib/web/markup.ts +88 -1
  280. package/lib/web/names.ts +42 -0
  281. package/lib/web/runtime.ts +7 -1
  282. package/lib/web/tokens.ts +25 -2
  283. package/package.json +26 -14
  284. package/skills/next/SKILL.md +4 -3
  285. package/skills/project-docs/SKILL.md +133 -0
  286. package/skills/project-docs/references/audit.md +131 -0
  287. package/skills/project-docs/references/changelog.md +105 -0
  288. package/skills/project-docs/references/discovery.md +127 -0
  289. package/skills/project-docs/references/readme.md +80 -0
  290. package/skills/project-docs/references/release.md +84 -0
  291. package/skills/project-docs/references/roadmap.md +93 -0
  292. package/skills/project-docs/scripts/state.ts +316 -0
  293. package/skills/refactor/SKILL.md +100 -125
  294. package/skills/refactor/references/handoff.md +35 -0
  295. package/skills/refactor/references/signals.md +54 -0
  296. package/skills/refactor/scripts/inventory.ts +587 -0
  297. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  298. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  299. package/extensions/file-browse/README.md +0 -506
  300. package/extensions/file-browse/files.ts +0 -564
  301. package/extensions/file-browse/server.ts +0 -141
  302. package/extensions/job/process.ts +0 -35
  303. package/extensions/plan/split.ts +0 -87
  304. package/extensions/session-bus/README.md +0 -247
  305. package/extensions/session-bus/team.ts +0 -102
  306. package/extensions/wechat/outbound.ts +0 -100
  307. package/lib/plans/sections.ts +0 -172
  308. package/lib/plans/setting.ts +0 -52
  309. package/lib/plans/split.ts +0 -215
  310. package/lib/plans/store.ts +0 -19
  311. package/lib/plans/team.ts +0 -151
  312. package/skills/changelog/SKILL.md +0 -148
  313. package/skills/parallel-dev/SKILL.md +0 -150
  314. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  315. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  316. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  317. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  318. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  319. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  320. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  321. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  322. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  323. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  324. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -8,10 +8,7 @@
8
8
  */
9
9
 
10
10
  export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
11
- if (!statusEl) return;
12
- statusEl.textContent = text || "";
13
- statusEl.title = text || "";
14
- statusEl.className = kind ? "bf-status bf-status--" + kind : "bf-status";
11
+ if (statusEl) paintStatus(statusEl, text, kind);
15
12
  }
16
13
 
17
14
  // The always-visible header chip: connection, comment count, and whether
@@ -56,7 +53,8 @@ export const WEB_CHROME_JS = String.raw` function setStatus(text, kind) {
56
53
  titleEl.textContent = data.title;
57
54
  titleEl.title = data.title;
58
55
  }
59
- if (data.title) document.title = "Plan review \u00b7 " + data.title;
56
+ // The document's own <title> already names the plan, rendered with the page: a second
57
+ // copy of the same sentence in script is a tab title that can disagree with the head.
60
58
  }
61
59
 
62
60
  function updateProgress() {
@@ -1,23 +1,36 @@
1
1
  /**
2
2
  * The browser review client, served as `/client.js`.
3
3
  *
4
- * Kept as one self-contained script (like `file-browse/client.ts`) with no
5
- * build step. Its document half — the inline and block Markdown rendering — is
6
- * `web-markdown.ts`, a fragment of the same IIFE interpolated below. It renders
7
- * the plan from the JSON data island using DOM text nodes only — plan and
8
- * comment text never becomes markup — renders each block
9
- * with its comment threads indented directly beneath it, highlights code by
10
- * copying only `tok-*` spans through `DOMParser`, builds the section outline
11
- * and review facts, and saves the annotation list back to the server. An
12
- * `EventSource` keeps a connection open so the server can tell when the tab is
13
- * gone; the explicit buttons are the primary save/cancel path.
4
+ * Served as one script with no build step, assembled from typed source: the element builder,
5
+ * typing test, status paint, and JSON POST come from `lib/web/client-runtime.ts`, and every
6
+ * rule the page has lives in its own `web-*.ts` module, interpolated below — the outline,
7
+ * the blocks and their navigation, the composer, the draft guard, the frame, the settlement,
8
+ * the keymap, and the Markdown, chrome, comments, and collaboration halves.
9
+ *
10
+ * What is left here is the composition root, deliberately: the element handles, the state
11
+ * the rules are handed, one adapter per rule that names it without arguments, the listeners
12
+ * that attach them, the `EventSource` that keeps a tab's presence known, and the boot order.
13
+ * None of it decides anything a test could disagree with — a rule that does belongs in a
14
+ * module, where it can be typed and driven.
15
+ *
16
+ * It renders the plan from the JSON data island using DOM text nodes only — plan and comment
17
+ * text never becomes markup — with each block's comment threads indented beneath it, code
18
+ * highlighted by copying only `tok-*` spans through `DOMParser`, and the annotation list
19
+ * saved back to the server.
14
20
  */
15
21
 
16
- import { dialogScript, keysDialogScript, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
22
+ import { CLIENT_RUNTIME_JS, dialogScript, keysDialogScript, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
17
23
  import { WEB_CHROME_JS } from "./web-chrome.ts";
18
24
  import { WEB_COMMENTS_JS } from "./web-comments.ts";
19
25
  import { WEB_COLLAB_JS } from "./web-collab.ts";
20
26
  import { WEB_MARKDOWN_JS } from "./web-markdown.ts";
27
+ import { WEB_BLOCKS_JS } from "./web-blocks.ts";
28
+ import { WEB_COMPOSER_JS } from "./web-composer.ts";
29
+ import { WEB_DRAFT_JS } from "./web-draft.ts";
30
+ import { WEB_OUTLINE_JS } from "./web-outline.ts";
31
+ import { WEB_KEYS_JS } from "./web-keys.ts";
32
+ import { WEB_PLAN_JS } from "./web-plan.ts";
33
+ import { WEB_SETTLE_JS } from "./web-settle.ts";
21
34
 
22
35
  export const REVIEW_CLIENT_JS = String.raw`(function () {
23
36
  "use strict";
@@ -55,6 +68,15 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
55
68
  var pendingQuote = null;
56
69
  var activeIndex = -1;
57
70
  var outlineItems = {};
71
+ /** The island's blocks, or none: one place the client reads them for navigation. */
72
+ function blocks() {
73
+ return Array.isArray(data.blocks) ? data.blocks : [];
74
+ }
75
+
76
+ /** The island's outline, or none: the one place the client reads it for the outline. */
77
+ function outlineEntries() {
78
+ return Array.isArray(data.outline) ? data.outline : [];
79
+ }
58
80
  var stream = null;
59
81
 
60
82
  var planEl = document.getElementById("plan");
@@ -67,337 +89,155 @@ export const REVIEW_CLIENT_JS = String.raw`(function () {
67
89
  var outlineList = document.getElementById("outline-list");
68
90
  var progressEl = document.getElementById("progress-fill");
69
91
 
70
- function el(tag, className) {
71
- var node = document.createElement(tag);
72
- if (className) node.className = className;
73
- return node;
74
- }
92
+ ${CLIENT_RUNTIME_JS}
75
93
 
76
- ${WEB_CHROME_JS}
94
+ ${WEB_OUTLINE_JS}
77
95
 
78
- ${WEB_COMMENTS_JS}
96
+ ${WEB_BLOCKS_JS}
79
97
 
80
- ${WEB_COLLAB_JS}
98
+ ${WEB_KEYS_JS}
81
99
 
82
- ${WEB_MARKDOWN_JS}
83
- function closeComposer() {
84
- var open = planEl ? planEl.querySelector(".bf-composer") : null;
85
- if (open && open.parentNode) open.parentNode.removeChild(open);
86
- Array.prototype.forEach.call(planEl ? planEl.querySelectorAll(".bf-block--composing") : [], function (block) {
87
- block.classList.remove("bf-block--composing");
88
- });
89
- // The only status a composer leaves behind is the refusal to leave it; once
90
- // the draft is gone that sentence is stale.
91
- if (statusEl && statusEl.textContent === DRAFT_REFUSAL) setStatus("");
92
- // Closing is how a half-typed comment is discarded, so the stored draft follows
93
- // it. A close with nothing open must not rewrite — and so drop — a restored one.
94
- if (open) saveDraft();
95
- }
100
+ ${WEB_SETTLE_JS}
96
101
 
97
- function observeSections() {
98
- if (sectionObserver && sectionObserver.disconnect) sectionObserver.disconnect();
99
- sectionObserver = null;
100
- if (!outlineList || !window.IntersectionObserver) return;
101
- var links = {};
102
- Array.prototype.forEach.call(outlineList.querySelectorAll("a"), function (link) {
103
- links[link.getAttribute("data-id")] = link;
104
- });
105
- var headings = Array.prototype.slice.call(planEl.querySelectorAll(".bf-block[id]"));
106
- if (headings.length === 0) return;
107
- sectionObserver = new IntersectionObserver(function (entries) {
108
- entries.forEach(function (entry) {
109
- if (!entry.isIntersecting) return;
110
- Array.prototype.forEach.call(outlineList.querySelectorAll("a"), function (link) {
111
- link.removeAttribute("aria-current");
112
- });
113
- var link = links[entry.target.id];
114
- if (link) link.setAttribute("aria-current", "true");
115
- });
116
- }, { rootMargin: "-88px 0px -68% 0px", threshold: 0 });
117
- headings.forEach(function (heading) { sectionObserver.observe(heading); });
118
- }
102
+ ${WEB_PLAN_JS}
119
103
 
120
- function renderOutline() {
121
- outlineItems = {};
122
- if (!outlineList) return;
123
- outlineList.replaceChildren();
124
- var items = Array.isArray(data.outline) ? data.outline : [];
125
- items.forEach(function (item) {
126
- var li = el("li", "lvl-" + item.level);
127
- var link = el("a");
128
- link.href = "#" + item.id;
129
- link.setAttribute("data-id", item.id);
130
- var label = el("span", "review-nav__link");
131
- label.textContent = item.text;
132
- link.appendChild(label);
133
- if (item.changed) {
134
- var dot = el("span", "review-changed");
135
- dot.setAttribute("role", "img");
136
- dot.setAttribute("aria-label", "Changed since your last review");
137
- dot.title = "Changed since your last review";
138
- link.appendChild(dot);
139
- }
140
- var count = el("span", "review-nav__count");
141
- count.hidden = true;
142
- link.appendChild(count);
143
- li.appendChild(link);
144
- outlineList.appendChild(li);
145
- outlineItems[item.line] = count;
146
- });
147
- }
104
+ ${WEB_COMPOSER_JS}
148
105
 
149
- // The section a source line belongs to: the last heading at or before it.
150
- function sectionFor(line) {
151
- var items = Array.isArray(data.outline) ? data.outline : [];
152
- var found = null;
153
- items.forEach(function (item) { if (item.line <= line) found = item; });
154
- return found;
155
- }
106
+ ${WEB_DRAFT_JS}
156
107
 
157
- // Review facts: the plan's step progress, and the comments per section.
158
- function renderFacts() {
159
- // The server counts the same step list approval seeds, so this never
160
- // disagrees with the todo list over a checkbox outside the Steps section.
161
- var steps = data.steps || { done: 0, total: 0 };
162
- // A plan with no Steps section has no progress to show; 0/0 is noise.
163
- var stepFact = document.getElementById("step-fact");
164
- if (stepFact) stepFact.hidden = !steps.total;
165
- var stepFill = document.getElementById("step-fill");
166
- if (stepFill) stepFill.style.width = (steps.total > 0 ? Math.round((steps.done / steps.total) * 100) : 0) + "%";
167
- var stepCount = document.getElementById("step-count");
168
- if (stepCount) stepCount.textContent = steps.done + "/" + steps.total;
169
- var totalEl = document.getElementById("comment-total");
170
- if (totalEl) totalEl.textContent = String(annotations.length);
171
- Object.keys(outlineItems).forEach(function (line) { outlineItems[line].hidden = true; });
172
- annotations.forEach(function (annotation) {
173
- var section = sectionFor(annotation.anchor);
174
- if (!section) return;
175
- var count = outlineItems[section.line];
176
- if (!count) return;
177
- var value = count.hidden ? 0 : Number(count.textContent);
178
- count.textContent = String(value + 1);
179
- count.hidden = false;
180
- });
181
- updateState();
182
- }
108
+ ${WEB_CHROME_JS}
183
109
 
184
- function blockElements() {
185
- return planEl ? Array.prototype.slice.call(planEl.querySelectorAll(".bf-block")) : [];
186
- }
110
+ ${WEB_COMMENTS_JS}
187
111
 
188
- function setActive(index) {
189
- var blocks = blockElements();
190
- if (blocks.length === 0) return;
191
- activeIndex = Math.max(0, Math.min(blocks.length - 1, index));
192
- blocks.forEach(function (block, at) { block.classList.toggle("bf-block--active", at === activeIndex); });
193
- var current = blocks[activeIndex];
194
- if (current && current.scrollIntoView) current.scrollIntoView({ block: "nearest" });
195
- }
112
+ ${WEB_COLLAB_JS}
196
113
 
197
- function moveActive(delta) {
198
- var blocks = blockElements();
199
- if (blocks.length === 0) return;
200
- if (activeIndex < 0) { setActive(delta < 0 ? blocks.length - 1 : 0); return; }
201
- setActive(activeIndex + delta);
202
- }
114
+ ${WEB_MARKDOWN_JS}
203
115
 
204
- /** The rendered block and its island block for a source line, when both exist. */
205
- function blockAtLine(line) {
206
- var wrap = planEl ? planEl.querySelector('.bf-block[data-line="' + line + '"]') : null;
207
- var block = (data.blocks || []).filter(function (item) { return item.start === line; })[0];
208
- return wrap && block ? { wrap: wrap, block: block } : null;
116
+ // Review facts: the plan's step progress, and the comments per section.
117
+ function facts() {
118
+ // The server counts the same step list approval seeds, so this never disagrees with
119
+ // the todo list over a checkbox outside the Steps section.
120
+ renderFacts(data.steps || { done: 0, total: 0 }, annotations, outlineItems, outlineEntries());
121
+ updateState();
209
122
  }
210
123
 
211
- /** Reopen the composer a reload interrupted, once its block has rendered. */
212
- function reopenComposer(typing) {
213
- if (!typing || saved || approved || closed) return;
214
- var target = blockAtLine(typing.anchor);
215
- if (target) openComposer(target.wrap, target.block, typing.quote || "", typing.text);
124
+ /**
125
+ * What the composer lifecycle reads, built where its state is.
126
+ *
127
+ * The bundle holds the state and the element handles; web-composer.ts holds the rules,
128
+ * so each call hands it the things those rules ask for and nothing more.
129
+ */
130
+ function composer() {
131
+ return {
132
+ planEl: planEl,
133
+ statusEl: statusEl,
134
+ refusal: DRAFT_REFUSAL,
135
+ setStatus: setStatus,
136
+ saveDraft: saveDraft,
137
+ open: openComposer,
138
+ };
216
139
  }
217
140
 
218
- function commentOnActive() {
219
- if (blockElements().length === 0) return;
220
- if (activeIndex < 0) setActive(0);
221
- var wrap = blockElements()[Math.max(0, activeIndex)];
222
- if (!wrap) return;
223
- var target = blockAtLine(Number(wrap.getAttribute("data-line")));
224
- if (target) openComposer(target.wrap, target.block, "");
141
+ /**
142
+ * Close the composer, for the surfaces that never see the bundle.
143
+ *
144
+ * The comment fragments call this while they build the form; they own the DOM, not the
145
+ * four pieces the rule reads, so they name the action and this supplies the state.
146
+ */
147
+ function closeOpenComposer() {
148
+ closeComposer(composer());
225
149
  }
226
-
227
- function renderPlan() {
228
- if (!planEl) return;
229
- // The DOM below is thrown away and rebuilt. Focus is only put back when it was
230
- // inside this document to begin with: a repaint the stream caused must never
231
- // pull the keyboard away from the page the reviewer is working in.
232
- var focused = document.activeElement;
233
- var keepFocus = !!(focused && focused !== planEl && planEl.contains(focused));
234
- closeComposer();
235
- pendingQuote = null;
236
- var scrollTop = planEl.scrollTop;
237
- planEl.replaceChildren();
238
- var blocks = Array.isArray(data.blocks) ? data.blocks : [];
239
- var anchored = {};
240
- blocks.forEach(function (block) {
241
- if (block.kind === "blank") return;
242
- anchored[block.start] = true;
243
- var wrap = el("div", "bf-block");
244
- wrap.setAttribute("data-line", String(block.start));
245
- wrap.setAttribute("data-kind", String(block.kind));
246
- if (block.kind === "heading") wrap.id = "s-" + block.start;
247
- renderBlockContent(wrap, block);
248
- var mine = annotations.filter(function (annotation) { return annotation.anchor === block.start; });
249
- if (mine.length > 0) wrap.classList.add("bf-block--commented");
250
- var rail = el("div", "bf-block__rail");
251
- var add = el("button", "bf-block__add");
252
- add.type = "button";
253
- add.textContent = "+";
254
- add.setAttribute("aria-label", "Comment on this block");
255
- add.addEventListener("click", function () {
256
- var quote = pendingQuote && pendingQuote.wrap === wrap ? pendingQuote.quote : selectionIn(wrap);
257
- pendingQuote = null;
258
- openComposer(wrap, block, quote);
259
- });
260
- rail.appendChild(add);
261
- if (mine.length > 0) {
262
- var badge = el("span", "bf-block__count");
263
- badge.textContent = String(mine.length);
264
- badge.title = mine.length === 1 ? "1 review comment" : mine.length + " review comments";
265
- rail.appendChild(badge);
266
- }
267
- wrap.appendChild(rail);
268
- wrap.addEventListener("focusin", function () { setActive(blockElements().indexOf(wrap)); });
269
- planEl.appendChild(wrap);
270
- renderComments(planEl, block.start);
271
- });
272
- var orphans = annotations.filter(function (annotation) { return !anchored[annotation.anchor]; });
273
- if (orphans.length > 0) {
274
- var box = el("div", "bf-comments bf-comments--orphans");
275
- var note = el("p"); note.textContent = "Comments not attached to a current block";
276
- box.appendChild(note);
277
- renderCommentCards(box, orphans);
278
- planEl.appendChild(box);
279
- }
280
- planEl.scrollTop = scrollTop;
281
- // The DOM was rebuilt, so the active block's class is gone; restore it in
282
- // place rather than scrolling, so j/k resume where the reviewer was.
283
- var active = activeIndex >= 0 ? blockElements()[activeIndex] : null;
284
- if (active) active.classList.add("bf-block--active");
285
- // The focused element went with the old DOM, so put the tab stop back on the
286
- // block's comment control: that is where adding, editing, or deleting left it.
287
- if (keepFocus && active) {
288
- var stop = active.querySelector(".bf-block__add");
289
- if (stop) stop.focus();
290
- }
291
- renderFacts();
292
- observeSections();
293
- updateState();
150
+ /**
151
+ * What a repaint reads and changes, built where the state is.
152
+ *
153
+ * Reads are getters and mutations are callbacks, so the bundle keeps every variable it
154
+ * owns and web-plan.ts holds only the shape of one frame.
155
+ */
156
+ function planFrame() {
157
+ return {
158
+ planEl: planEl,
159
+ outlineList: outlineList,
160
+ focused: document.activeElement,
161
+ blocks: blocks,
162
+ annotations: function () { return annotations; },
163
+ activeIndex: function () { return activeIndex; },
164
+ setActiveIndex: function (index) { activeIndex = index; },
165
+ pendingQuote: function () { return pendingQuote; },
166
+ clearPendingQuote: function () { pendingQuote = null; },
167
+ composer: composer,
168
+ renderBlock: renderBlockContent,
169
+ renderComments: renderComments,
170
+ renderCommentCards: renderCommentCards,
171
+ selectionIn: selectionIn,
172
+ open: openComposer,
173
+ facts: facts,
174
+ updateState: updateState,
175
+ };
294
176
  }
295
177
 
178
+ function renderAll() {
179
+ sectionObserver = renderPlanFrame(planFrame(), closeOpenComposer, observeSections, sectionObserver);
180
+ }
296
181
 
297
182
  // Save, approve, and post share the same POST shape; what differs is whether
298
183
  // the review settles. Only the owner's save and approve resolve the session,
299
184
  // so only their callback marks the page terminal. A reviewer posts only their
300
185
  // own comments, so a page showing everyone's cannot re-post another writer's.
301
- function submit(url, okText, failText, onOk) {
302
- var payload = role === "reviewer" ? annotations.filter(isMine) : annotations;
303
- fetch(url + "?token=" + encodeURIComponent(token), {
304
- method: "POST",
305
- headers: { "Content-Type": "application/json" },
306
- body: JSON.stringify({ annotations: payload })
307
- }).then(function (response) {
308
- if (response.ok) {
309
- dirty = false;
310
- clearDraft();
311
- setStatus(okText, "saved");
312
- if (onOk) onOk();
313
- updateState();
314
- // The server binds a reviewer's name to her link on the first post; adopt it
315
- // so a second tab of the same link posts under the same name.
316
- return response.json().then(function (payload) {
317
- if (role === "reviewer" && payload && typeof payload.author === "string" && payload.author) adoptName(payload.author);
318
- }, function () {});
319
- }
320
- if (response.status === 409) {
321
- // A conflict is either a plan that moved under the page or a link already
322
- // bound to another name; a reload fixes both, and the server names which.
323
- if (approveButton) approveButton.disabled = true;
324
- if (saveButton) saveButton.disabled = true;
325
- if (postButton) postButton.disabled = true;
326
- offerReload();
327
- return response.json().then(
328
- function (payload) { setStatus(payload && payload.error ? payload.error : PLAN_MOVED, "error"); },
329
- function () { setStatus(PLAN_MOVED, "error"); }
330
- );
331
- }
332
- // Surface the server's own reason (a malformed comment, a stale body)
333
- // rather than a bare status code.
334
- return response.json().then(
335
- function (payload) { throw new Error(payload && payload.error ? payload.error : "server returned " + response.status); },
336
- function () { throw new Error("server returned " + response.status); }
337
- );
338
- }).catch(function (error) {
339
- setStatus(failText + error.message, "error");
340
- });
341
- }
342
-
343
186
  // A composer holds text the page has not committed, so the ways off the page
344
187
  // refuse while it does: leaving would drop what the reviewer typed.
345
- function draftTextarea() {
346
- var open = planEl ? planEl.querySelector(".bf-composer") : null;
347
- var area = open ? open.querySelector("textarea") : null;
348
- return area && area.value.trim() ? area : null;
188
+ /** The ways off the page refuse while a comment is half-written; this says so once. */
189
+ function confirmNoDraft() {
190
+ return guardDraft(planEl, statusEl, DRAFT_REFUSAL, paintStatus);
349
191
  }
350
- function guardDraft() {
351
- var area = draftTextarea();
352
- if (!area) return false;
353
- setStatus(DRAFT_REFUSAL, "error");
354
- area.focus();
355
- return true;
192
+
193
+ /**
194
+ * What the settlement reads and changes, built where the state is.
195
+ *
196
+ * The flags live here, the buttons live here, the stream lives here; web-settle.ts owns
197
+ * the rules about them and reports each change through these callbacks.
198
+ */
199
+ function settlement() {
200
+ return {
201
+ token: function () { return token; },
202
+ role: function () { return role; },
203
+ mine: function () { return annotations.filter(isMine); },
204
+ annotations: function () { return annotations; },
205
+ settled: function () { return saved || approved || closed; },
206
+ setStatus: setStatus,
207
+ buttons: function () { return { approve: approveButton, save: saveButton, post: postButton }; },
208
+ clearDraft: clearDraft,
209
+ markClean: function () { dirty = false; },
210
+ adoptName: adoptName,
211
+ offerReload: offerReload,
212
+ markSettled: function (kind) {
213
+ if (kind === "saved") saved = true;
214
+ else if (kind === "approved") approved = true;
215
+ else closed = true;
216
+ },
217
+ settleStream: settleStream,
218
+ lockActions: lockActions,
219
+ updateState: updateState,
220
+ confirmNoDraft: confirmNoDraft,
221
+ post: postJson,
222
+ send: function (url) { return fetch(url, { method: "POST" }); },
223
+ planMoved: PLAN_MOVED,
224
+ };
356
225
  }
357
226
 
358
227
  function save() {
359
- if (saved || approved || closed) return;
360
- if (guardDraft()) return;
361
- setStatus("Saving\u2026");
362
- submit("/api/feedback", "Saved \u2014 return to Pi to continue.", "Could not save: ", function () {
363
- saved = true;
364
- settleStream();
365
- lockActions();
366
- });
228
+ saveReview(settlement());
367
229
  }
368
230
 
369
- // A reviewer's write never settles the page: the server keeps the review open,
370
- // so the page keeps following it and she can keep editing her own comments.
371
231
  function postComments() {
372
- if (closed) return;
373
- if (guardDraft()) return;
374
- setStatus("Posting\u2026");
375
- submit("/api/comment", "Comments posted \u2014 the owner sees them now.", "Could not post: ", null);
232
+ postReviewComments(settlement());
376
233
  }
377
234
 
378
235
  function approve() {
379
- if (saved || approved || closed) return;
380
- setStatus("Approving\u2026");
381
- submit("/api/approve", "Approved \u2014 return to Pi to continue.", "Could not approve: ", function () {
382
- approved = true;
383
- settleStream();
384
- lockActions();
385
- });
236
+ approveReview(settlement());
386
237
  }
387
238
 
388
239
  function cancel() {
389
- // Closing the review is the owner's decision; a reviewer closes their tab.
390
- if (role !== "owner" || saved || approved || closed) return;
391
- if (guardDraft()) return;
392
- closed = true;
393
- settleStream();
394
- lockActions();
395
- setStatus("Closing\u2026");
396
- updateState();
397
- fetch("/api/cancel?token=" + encodeURIComponent(token), { method: "POST" }).catch(function () {}).then(function () {
398
- setStatus("Closed \u2014 return to Pi.");
399
- updateState();
400
- });
240
+ cancelReview(settlement());
401
241
  }
402
242
 
403
243
  if (postButton) postButton.addEventListener("click", postComments);
@@ -413,7 +253,7 @@ ${WEB_MARKDOWN_JS}
413
253
 
414
254
  function askApprove() {
415
255
  if (saved || approved || closed) return;
416
- if (guardDraft()) return;
256
+ if (confirmNoDraft()) return;
417
257
  if (!approveDialog || !window.bfApprove) { approve(); return; }
418
258
  window.bfApprove.open();
419
259
  }
@@ -450,38 +290,47 @@ ${WEB_MARKDOWN_JS}
450
290
  // Remember a selection made inside a block so clicking the rail button can
451
291
  // still quote it after the button press has collapsed the selection.
452
292
  document.addEventListener("mouseup", function (event) {
453
- if (!planEl || saved || approved || closed) return;
454
- var wrap = event.target && event.target.closest ? event.target.closest(".bf-block") : null;
455
- var quote = wrap && !wrap.querySelector(".bf-composer") ? selectionIn(wrap) : "";
456
- pendingQuote = wrap && quote ? { wrap: wrap, quote: quote } : null;
293
+ rememberSelection(event.target, {
294
+ planEl: planEl,
295
+ settled: function () { return saved || approved || closed; },
296
+ selectionIn: selectionIn,
297
+ setPending: function (quote) { pendingQuote = quote; },
298
+ });
457
299
  });
458
300
 
459
301
  // The sheet's own wiring — trigger, close button, backdrop, Tab trap, and Esc —
460
302
  // is shared with browse; the page keeps only its own keymap.
461
303
  ${keysDialogScript("shortcuts")}
462
304
 
463
- function typing(target) {
464
- var tag = target && target.tagName ? target.tagName : "";
465
- return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || (target && target.isContentEditable);
305
+ /**
306
+ * What a key may do, built where the state and the handlers are.
307
+ *
308
+ * The listener below stays in the bundle with the rest of the wiring; the map of key to
309
+ * action is web-keys.ts, which is the part with a rule to it.
310
+ */
311
+ function keyActions() {
312
+ return {
313
+ composerOpen: function () { return !!(planEl && planEl.querySelector(".bf-composer")); },
314
+ closeComposer: closeOpenComposer,
315
+ cancel: cancel,
316
+ sheetOpen: function () { return window.bfKeys.isOpen(); },
317
+ toggleSheet: function () { window.bfKeys.toggle(); },
318
+ moveActive: function (delta) {
319
+ var next = moveActive(planEl, activeIndex, delta);
320
+ if (next !== null) activeIndex = next;
321
+ },
322
+ commentOnActive: function () { activeIndex = commentOnActive(composer(), activeIndex, blocks()); },
323
+ askApprove: askApprove,
324
+ save: save,
325
+ postComments: postComments,
326
+ hasSaveControl: function () { return !!saveButton; },
327
+ hasApproveControl: function () { return !!approveButton; },
328
+ typing: typing,
329
+ };
466
330
  }
467
331
 
468
332
  document.addEventListener("keydown", function (event) {
469
- if (event.key === "Escape") {
470
- // The sheet owns its own Esc; this peels the composer, then the review.
471
- if (planEl && planEl.querySelector(".bf-composer")) { closeComposer(); return; }
472
- cancel();
473
- return;
474
- }
475
- if (typing(event.target)) return;
476
- if (event.metaKey || event.ctrlKey || event.altKey) return;
477
- if (event.key === "?") { event.preventDefault(); window.bfKeys.toggle(); return; }
478
- // While the shortcut sheet is open it owns the keyboard.
479
- if (window.bfKeys.isOpen()) return;
480
- if (event.key === "j") { event.preventDefault(); moveActive(1); return; }
481
- if (event.key === "k") { event.preventDefault(); moveActive(-1); return; }
482
- if (event.key === "c") { event.preventDefault(); commentOnActive(); return; }
483
- if (event.key === "a" && approveButton) { event.preventDefault(); askApprove(); return; }
484
- if (event.key === "s") { event.preventDefault(); if (saveButton) save(); else postComments(); return; }
333
+ handleKey(event, keyActions());
485
334
  });
486
335
 
487
336
  // A closed tab is detected by the stream below: the server sees the last
@@ -517,9 +366,9 @@ ${WEB_MARKDOWN_JS}
517
366
  var restoredTyping = restoreDraft();
518
367
  renderHeader();
519
368
  renderName();
520
- renderOutline();
521
- renderPlan();
522
- reopenComposer(restoredTyping);
369
+ outlineItems = renderOutline(outlineList, outlineEntries());
370
+ renderAll();
371
+ reopenComposer(composer(), restoredTyping, { saved: saved, approved: approved, closed: closed }, blocks());
523
372
  updateProgress();
524
373
  })();
525
374
  `;
@@ -131,7 +131,7 @@ export const WEB_COLLAB_JS = String.raw` /**
131
131
  reviewerName = value;
132
132
  try { window.localStorage.setItem("bf-review-name", reviewerName); } catch (error) {}
133
133
  renderName();
134
- renderPlan();
134
+ renderAll();
135
135
  }
136
136
 
137
137
  function askName() {
@@ -147,7 +147,7 @@ export const WEB_COLLAB_JS = String.raw` /**
147
147
  try { window.localStorage.setItem("bf-review-name", reviewerName); } catch (error) {}
148
148
  if (window.bfName) window.bfName.close();
149
149
  renderName();
150
- renderPlan();
150
+ renderAll();
151
151
  }
152
152
 
153
153
  /**
@@ -197,7 +197,7 @@ export const WEB_COLLAB_JS = String.raw` /**
197
197
  if (typeof next.author === "string" && next.author) adoptName(next.author);
198
198
  // This tab's own unsaved comments belong on top of what just arrived.
199
199
  restoreDraft();
200
- renderPlan();
200
+ renderAll();
201
201
  renderFacts();
202
202
  updateState();
203
203
  setStatus(kind === "plan" ? "The plan changed on disk; showing the new version." : "Updated with a new comment.");