@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,496 @@
1
+ /**
2
+ * The client's renderer: snapshot in, DOM out — the toolbar, the lanes, the cards,
3
+ * and the card dialog's contents.
4
+ *
5
+ * This file runs in the browser, where no type checker sees it, so it holds to one
6
+ * rule: **every value from the snapshot is set as `textContent`** — never as
7
+ * `innerHTML`, never into a template string. A card title is model-written and a note
8
+ * can be anything a person typed, so the only safe renderer is one that never parses
9
+ * them.
10
+ *
11
+ * A frame replaces what changed and leaves the rest alone: a lane is rebuilt only
12
+ * when its cards' revisions moved, so the filter input, a collapsed lane, the scroll
13
+ * offset, and an open dialog all survive a peer's write.
14
+ *
15
+ * The client modules share one namespace, `window.bfBoard`: this one paints,
16
+ * `team.ts` draws the round, `actions.ts` posts, `keys.ts` binds, and `live.ts` owns
17
+ * the frame stream and the wiring.
18
+ */
19
+
20
+ export const BOARD_RENDER_JS = String.raw`(function () {
21
+ "use strict";
22
+ var board = window.bfBoard || (window.bfBoard = {});
23
+
24
+ /** Lanes in the order the server sends them, with the words a reader expects. */
25
+ var LANE_LABEL = { todo: "To do", doing: "Doing", blocked: "Blocked", done: "Done" };
26
+ var LANE_EMPTY = { todo: "nothing waiting", doing: "nothing in flight", blocked: "nothing blocked", done: "nothing finished yet" };
27
+ /** Path chips a lane card shows before it counts the rest. */
28
+ var PATH_CHIPS = 3;
29
+
30
+ /** One element with optional class and text. */
31
+ function el(tag, className, text) {
32
+ var node = document.createElement(tag);
33
+ if (className) node.className = className;
34
+ if (text !== undefined && text !== null) node.textContent = String(text);
35
+ return node;
36
+ }
37
+
38
+ /**
39
+ * One button that calls back, so nothing on the page is a click handler in markup.
40
+ *
41
+ * 'plain' is the bordered kit button, 'ghost' the text one: a card's primary
42
+ * action (Claim, Move) is bordered and the rest are text, which is what makes one
43
+ * of three controls on a card the obvious one.
44
+ */
45
+ function actionButton(label, onClick, variant) {
46
+ var kind = variant === "plain" ? "" : variant ? " bf-btn--" + variant : " bf-btn--ghost";
47
+ var node = el("button", "bf-btn" + kind, label);
48
+ node.type = "button";
49
+ node.addEventListener("click", onClick);
50
+ return node;
51
+ }
52
+
53
+ /** A kit chip, with a tone when the meaning is a state. */
54
+ function chip(text, tone) {
55
+ return el("span", "bf-chip" + (tone ? " bf-chip--" + tone : ""), text);
56
+ }
57
+
58
+ /** "12m", "3h", "2d" — the age of a card's last write, compact enough for a meta line. */
59
+ function ago(at) {
60
+ var seconds = Math.max(0, Math.round((Date.now() - at) / 1000));
61
+ if (seconds < 45) return "now";
62
+ var minutes = Math.round(seconds / 60);
63
+ if (minutes < 60) return minutes + "m";
64
+ var hours = Math.round(minutes / 60);
65
+ if (hours < 24) return hours + "h";
66
+ return Math.round(hours / 24) + "d";
67
+ }
68
+
69
+ /** Bytes, the way the footer says them. */
70
+ function size(bytes) {
71
+ if (bytes < 1024) return bytes + " B";
72
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " kB";
73
+ return (bytes / (1024 * 1024)).toFixed(1) + " MB";
74
+ }
75
+
76
+ /** Every card the snapshot carries, in lane order. */
77
+ function cards(view) {
78
+ var all = [];
79
+ for (var i = 0; i < view.lanes.length; i++) all = all.concat(view.lanes[i].cards);
80
+ return all;
81
+ }
82
+
83
+ /** One card by id, or undefined when the last frame no longer has it. */
84
+ function findCard(view, id) {
85
+ var all = cards(view);
86
+ for (var i = 0; i < all.length; i++) if (all[i].id === id) return all[i];
87
+ return undefined;
88
+ }
89
+
90
+ /** Whether this session holds the card. */
91
+ function mine(card, me) {
92
+ if (!card.owner) return false;
93
+ if (card.owner.id !== undefined && me.id !== undefined) return card.owner.id === me.id;
94
+ return String(card.owner.label).toLowerCase() === String(me.label).toLowerCase();
95
+ }
96
+
97
+ /** A card's priority; an older card that never recorded one is ordinary. */
98
+ function priorityOf(card) {
99
+ return card.priority || "normal";
100
+ }
101
+
102
+ /**
103
+ * The cards this one waits for that are not finished yet.
104
+ *
105
+ * The board is the index: a link is one id in the waiting card's file, so the other
106
+ * card's lane can only be read from the frame. A finished dependency has stopped
107
+ * explaining anything, so it drops out of the answer.
108
+ */
109
+ function blockersOf(card, view) {
110
+ var ids = card.dependsOn || [];
111
+ if (ids.length === 0 || !view) return [];
112
+ var all = cards(view);
113
+ var open = [];
114
+ for (var i = 0; i < ids.length; i++) {
115
+ for (var j = 0; j < all.length; j++) {
116
+ if (all[j].id === ids[i] && all[j].status !== "done") open.push(all[j]);
117
+ }
118
+ }
119
+ return open;
120
+ }
121
+
122
+ /** How the board says what a card waits for: the id and the lane that explains it. */
123
+ function waitingText(card, view) {
124
+ return blockersOf(card, view)
125
+ .map(function (blocker) { return "#" + blocker.id + " (" + blocker.status + ")"; })
126
+ .join(", ");
127
+ }
128
+
129
+ /**
130
+ * Whether this reader may do one verb to one card.
131
+ *
132
+ * The one table behind three surfaces: a card face, the dialog's footer, and a queue
133
+ * row each draw a different subset — a face offers two actions, the dialog five — but
134
+ * a verb is allowed for the same reader on all of them, and it is the same rule the
135
+ * server applies in lib/board/rules.ts. Open is always allowed: reading is not a write.
136
+ */
137
+ function may(card, view, verb) {
138
+ // Reading is not a write, so it survives a viewer's page; everything else is what
139
+ // the document's authority says it may be.
140
+ if (verb === "open") return true;
141
+ if (!canWrite()) return false;
142
+ var isMine = mine(card, view.me);
143
+ var waiting = blockersOf(card, view).length > 0;
144
+ var finished = card.status === "done";
145
+ if (verb === "claim") return card.status === "todo" && !waiting;
146
+ if (verb === "move") return isMine && !finished;
147
+ if (verb === "release") return isMine && !finished;
148
+ if (verb === "edit") return card.owner === undefined || isMine;
149
+ if (verb === "drop") return card.status === "todo" || finished || isMine;
150
+ if (verb === "reclaim") return card.owner !== undefined && !finished;
151
+ // Recording an owner is what a page can do where it cannot message: an unowned card
152
+ // that is not waiting for another, which is the same card a claim is offered.
153
+ if (verb === "assign") return card.owner === undefined && !finished && !waiting;
154
+ return false;
155
+ }
156
+
157
+ /**
158
+ * Whether this page may change a card at all.
159
+ *
160
+ * The document is rendered per request, so a viewer's shell says data-writes="0":
161
+ * one attribute decides every affordance, and the server refuses anyway — a page
162
+ * that offered what it would be turned down for teaches a reader the wrong thing.
163
+ */
164
+ function canWrite() {
165
+ return document.documentElement.getAttribute("data-writes") !== "0";
166
+ }
167
+
168
+ /** The one line under a card's meta: why it sits where it does. */
169
+ function reasonOf(card, view) {
170
+ var last = card.notes.length > 0 ? card.notes[card.notes.length - 1] : undefined;
171
+ // An order is the strongest reason a card is not being worked on, and the one a
172
+ // reader cannot see from the lane: it comes before the note that says the same.
173
+ var waiting = waitingText(card, view);
174
+ if (waiting !== "") return { text: "waits on " + waiting, kind: "waiting" };
175
+ if (card.status === "blocked") {
176
+ // The lane already says blocked and the tone already says danger; the line says why.
177
+ return { text: last ? last.text : "blocked, with nothing recorded", kind: "blocked" };
178
+ }
179
+ if (card.status === "doing" && !card.plan) {
180
+ return { text: "no plan recorded", kind: "unplanned" };
181
+ }
182
+ if (card.status === "doing") return { text: "planned", kind: "plan" };
183
+ if (card.status === "done") return { text: "✓ done" + (card.owner ? " by " + card.owner.label : ""), kind: "done" };
184
+ return last ? { text: last.text, kind: "note" } : undefined;
185
+ }
186
+
187
+ /** A path hint, or the +N chip that points at the rest. */
188
+ function paths(card, limit) {
189
+ var list = el("ul", "board-card__paths");
190
+ var shown = card.paths.slice(0, limit);
191
+ for (var i = 0; i < shown.length; i++) {
192
+ var item = el("li", "");
193
+ item.appendChild(chip(shown[i]));
194
+ list.appendChild(item);
195
+ }
196
+ if (card.paths.length > limit) {
197
+ var rest = el("li", "");
198
+ rest.appendChild(chip("+" + (card.paths.length - limit)));
199
+ list.appendChild(rest);
200
+ }
201
+ return list;
202
+ }
203
+
204
+ /** One card in its lane. */
205
+ function cardNode(card, view) {
206
+ var item = el("li", "board-card");
207
+ item.setAttribute("data-card", card.id);
208
+ item.setAttribute("data-status", card.status);
209
+ item.setAttribute("data-rev", String(card.rev));
210
+
211
+ var head = el("p", "board-card__head");
212
+ head.appendChild(el("span", "board-card__id", "#" + card.id));
213
+ var title = actionButton(card.title, function () {
214
+ board.openCard(card.id);
215
+ });
216
+ title.className = "board-card__title";
217
+ head.appendChild(title);
218
+ item.appendChild(head);
219
+
220
+ var meta = el("p", "board-card__meta");
221
+ meta.appendChild(card.owner ? chip(card.owner.label, "accent") : el("span", "", "unassigned"));
222
+ // The order inside a lane is the priority, so an urgent card says so: an order
223
+ // nobody can see reads as an accident. The lane's colour stays the card's own.
224
+ if (priorityOf(card) !== "normal") meta.appendChild(chip(priorityOf(card)));
225
+ // The stamp, so the clock can keep the age honest between frames: a board can be
226
+ // quiet for ten minutes while the page still says "live".
227
+ var age = el("span", "", ago(card.updatedAt));
228
+ age.setAttribute("data-at", String(card.updatedAt));
229
+ meta.appendChild(age);
230
+ item.appendChild(meta);
231
+
232
+ if (card.paths.length > 0) item.appendChild(paths(card, PATH_CHIPS));
233
+
234
+ var reason = reasonOf(card, view);
235
+ if (reason) {
236
+ var kind = reason.kind === "blocked" || reason.kind === "unplanned" ? " board-card__reason--" + reason.kind : "";
237
+ var line = el("p", "board-card__reason" + kind, reason.text);
238
+ // It clamps to one line on the card, so the full sentence is the tooltip.
239
+ line.title = reason.text;
240
+ item.appendChild(line);
241
+ }
242
+
243
+ var actions = el("div", "board-card__actions");
244
+ // A card that waits cannot be claimed, so the card face does not offer it: the reason
245
+ // line above is what it says instead. Move is not a permission but the way into the
246
+ // dialog, so it is drawn for any card still in flight, whoever holds it.
247
+ if (may(card, view, "claim")) {
248
+ actions.appendChild(actionButton("Claim", function () { board.act("claim", card); }, "plain"));
249
+ } else if (canWrite() && card.status !== "todo" && card.status !== "done") {
250
+ // Move is the way into the dialog, not a permission, but a dialog with no actions
251
+ // in it is not worth opening.
252
+ actions.appendChild(actionButton("Move", function () { board.openCard(card.id, "move"); }, "plain"));
253
+ }
254
+ // A note is a write, and a viewer's dialog has no field for one.
255
+ if (canWrite()) actions.appendChild(actionButton("Note", function () { board.openCard(card.id, "note"); }));
256
+ actions.appendChild(actionButton("Open", function () { board.openCard(card.id); }));
257
+ item.appendChild(actions);
258
+ return item;
259
+ }
260
+
261
+ /** The toolbar's identity, progress, and footer. */
262
+ function renderChrome(view) {
263
+ var me = document.getElementById("board-me");
264
+ if (me) {
265
+ me.textContent = view.me.label;
266
+ me.title = "Writes from this page are made as " + view.me.label;
267
+ }
268
+ var progress = document.getElementById("board-progress");
269
+ if (progress) {
270
+ var total = view.counts.total;
271
+ // One or two cards is not a ratio: an empty bar beside "0/1 done" is noise.
272
+ progress.hidden = total < 3;
273
+ var fill = document.getElementById("board-progress-fill");
274
+ if (fill) fill.style.width = total === 0 ? "0%" : Math.round((view.counts.done / total) * 100) + "%";
275
+ var text = document.getElementById("board-progress-text");
276
+ if (text) text.textContent = view.counts.done + "/" + total + " done";
277
+ }
278
+ // A viewer gets the board, not the chance to make one: the composer and the empty
279
+ // state's way into it belong to whoever serves the page.
280
+ var create = document.getElementById("board-new");
281
+ if (create) create.hidden = !canWrite();
282
+ var first = document.getElementById("board-first-card");
283
+ if (first) first.hidden = !canWrite();
284
+ var where = document.getElementById("board-where");
285
+ if (where) {
286
+ where.textContent = "store " + view.where + " · " + plural(view.counts.total, "card") + " · " + size(view.bytes);
287
+ }
288
+ }
289
+
290
+ /** 1 card, 2 cards. */
291
+ function plural(count, noun) {
292
+ return count + " " + noun + (count === 1 ? "" : "s");
293
+ }
294
+
295
+ /**
296
+ * Rewrite every age already on the page from its own timestamp.
297
+ *
298
+ * A frame carries the ages of the moment it was built, so on a board nobody is
299
+ * writing to the cards would slowly lie about how long they have sat there. The
300
+ * ticker calls this; it touches nothing else, so a filter, a folded lane, or an
301
+ * open dialog survives it.
302
+ */
303
+ function refreshAges() {
304
+ var nodes = document.querySelectorAll("[data-at]");
305
+ for (var i = 0; i < nodes.length; i++) {
306
+ var next = ago(Number(nodes[i].getAttribute("data-at")));
307
+ if (nodes[i].textContent !== next) nodes[i].textContent = next;
308
+ }
309
+ }
310
+
311
+ /** Whether the filter keeps a card: title, a path hint, or the holder. */
312
+ function matches(card, query) {
313
+ if (!query) return true;
314
+ var needle = query.toLowerCase();
315
+ if (card.title.toLowerCase().indexOf(needle) !== -1) return true;
316
+ if (card.owner && card.owner.label.toLowerCase().indexOf(needle) !== -1) return true;
317
+ for (var i = 0; i < card.paths.length; i++) if (card.paths[i].toLowerCase().indexOf(needle) !== -1) return true;
318
+ return false;
319
+ }
320
+
321
+ /** The lane section for one status, created once and then reused. */
322
+ function laneNode(lane) {
323
+ var section = document.querySelector('.board-lane[data-status="' + lane.status + '"]');
324
+ if (section) return section;
325
+ section = el("section", "board-lane");
326
+ section.setAttribute("data-status", lane.status);
327
+ var head = el("h2", "board-lane__head");
328
+ var toggle = actionButton(LANE_LABEL[lane.status] || lane.status, function () {
329
+ board.toggleLane(lane.status);
330
+ });
331
+ toggle.className = "board-lane__toggle";
332
+ toggle.setAttribute("aria-expanded", "true");
333
+ toggle.setAttribute("aria-controls", "lane-" + lane.status);
334
+ toggle.id = "lane-head-" + lane.status;
335
+ head.appendChild(el("span", "board-lane__dot"));
336
+ head.appendChild(toggle);
337
+ head.appendChild(el("span", "board-lane__count"));
338
+ section.appendChild(head);
339
+ var list = el("ol", "board-lane__list");
340
+ list.id = "lane-" + lane.status;
341
+ section.appendChild(list);
342
+ section.appendChild(el("p", "board-lane__empty", LANE_EMPTY[lane.status] || "nothing"));
343
+ return section;
344
+ }
345
+
346
+ /** Draw the lanes, rebuilding only one whose cards actually moved. */
347
+ function renderLanes(view) {
348
+ var lanes = document.getElementById("board-lanes");
349
+ if (!lanes) return;
350
+ if (lanes.getAttribute("data-skeleton") === "1") {
351
+ // The placeholders have done their job; the real lanes replace them whole.
352
+ lanes.textContent = "";
353
+ lanes.removeAttribute("data-skeleton");
354
+ }
355
+ var query = board.query || "";
356
+ var empty = document.getElementById("board-no-cards");
357
+ if (empty) empty.hidden = view.counts.total !== 0;
358
+ lanes.hidden = view.counts.total === 0;
359
+ for (var i = 0; i < view.lanes.length; i++) {
360
+ var lane = view.lanes[i];
361
+ var section = laneNode(lane);
362
+ if (section.parentNode !== lanes) lanes.appendChild(section);
363
+ var shown = lane.cards.filter(function (card) { return matches(card, query); });
364
+ var sig = lane.cards.map(function (card) { return card.id + ":" + card.rev; }).join(",") + "|" + query;
365
+ if (section.getAttribute("data-sig") !== sig) {
366
+ section.setAttribute("data-sig", sig);
367
+ var list = section.querySelector(".board-lane__list");
368
+ list.textContent = "";
369
+ for (var j = 0; j < shown.length; j++) list.appendChild(cardNode(shown[j], view));
370
+ }
371
+ var count = section.querySelector(".board-lane__count");
372
+ if (count) {
373
+ count.textContent = query === "" || shown.length === lane.cards.length
374
+ ? String(lane.cards.length)
375
+ : shown.length + "/" + lane.cards.length;
376
+ }
377
+ var none = section.querySelector(".board-lane__empty");
378
+ if (none) {
379
+ none.hidden = shown.length !== 0;
380
+ none.textContent = query !== "" && lane.cards.length > 0 ? "no match" : LANE_EMPTY[lane.status] || "nothing";
381
+ }
382
+ var collapsed = board.collapsed && board.collapsed[lane.status] === true;
383
+ var list = section.querySelector(".board-lane__list");
384
+ if (list) list.hidden = collapsed;
385
+ var toggle = section.querySelector(".board-lane__toggle");
386
+ if (toggle) toggle.setAttribute("aria-expanded", String(!collapsed));
387
+ }
388
+ renderPicker(view);
389
+ }
390
+
391
+ /** The lane selector the narrow layout shows instead of four columns. */
392
+ function renderPicker(view) {
393
+ var picker = document.getElementById("board-picker");
394
+ if (!picker) return;
395
+ picker.textContent = "";
396
+ for (var i = 0; i < view.lanes.length; i++) {
397
+ (function (lane) {
398
+ var item = el("button", "bf-segmented__item", (LANE_LABEL[lane.status] || lane.status) + " " + lane.cards.length);
399
+ item.type = "button";
400
+ item.setAttribute("role", "tab");
401
+ item.setAttribute("data-lane", lane.status);
402
+ item.addEventListener("click", function () { board.showLane(lane.status); });
403
+ picker.appendChild(item);
404
+ })(view.lanes[i]);
405
+ }
406
+ board.applyLane();
407
+ }
408
+
409
+ /** Fill the card dialog's contents from a card, without opening or closing it. */
410
+ function fillDialog(card, view) {
411
+ document.getElementById("card-dialog-title").textContent = card.title;
412
+ // The lane is a status of its own, not a word inside a sentence: the same dot and
413
+ // label a lane head wears, so the dialog and the board speak the same vocabulary.
414
+ var state = document.getElementById("card-dialog-state");
415
+ state.setAttribute("data-status", card.status);
416
+ state.textContent = card.status;
417
+ document.getElementById("card-dialog-meta").textContent =
418
+ "#" + card.id + " · updated " + ago(card.updatedAt) + " · last edited by " + card.by;
419
+ var owner = document.getElementById("card-dialog-owner");
420
+ owner.textContent = "";
421
+ owner.appendChild(card.owner ? chip(card.owner.label, "accent") : document.createTextNode("unassigned"));
422
+ if (priorityOf(card) !== "normal") owner.appendChild(chip(priorityOf(card)));
423
+
424
+ var pathList = document.getElementById("card-dialog-paths");
425
+ pathList.textContent = "";
426
+ for (var i = 0; i < card.paths.length; i++) {
427
+ var item = el("li", "");
428
+ item.appendChild(chip(card.paths[i]));
429
+ pathList.appendChild(item);
430
+ }
431
+
432
+ var waits = document.getElementById("card-dialog-depends");
433
+ if (waits) {
434
+ var linked = (card.dependsOn || []).map(function (id) { return "#" + id; }).join(", ");
435
+ waits.hidden = linked === "";
436
+ waits.textContent = linked === "" ? "" : "waits on " + linked;
437
+ }
438
+
439
+ var plan = document.getElementById("card-dialog-plan");
440
+ plan.hidden = !card.plan;
441
+ plan.textContent = card.plan ? "plan " + card.plan : "";
442
+
443
+ var notes = document.getElementById("card-dialog-notes");
444
+ notes.textContent = "";
445
+ for (var j = 0; j < card.notes.length; j++) {
446
+ var note = card.notes[j];
447
+ // Who and when on their own line, the sentence under it, a rule between notes:
448
+ // wrapped prose used to run one note into the next with nothing to separate them.
449
+ var row = el("li", "board-dialog__note");
450
+ var head = el("p", "board-dialog__note-head");
451
+ head.appendChild(el("span", "board-dialog__note-by", note.by));
452
+ head.appendChild(el("span", "board-dialog__note-at", ago(note.at)));
453
+ row.appendChild(head);
454
+ row.appendChild(el("p", "board-dialog__note-text", note.text));
455
+ notes.appendChild(row);
456
+ }
457
+ var earlier = document.getElementById("card-dialog-earlier");
458
+ earlier.hidden = card.notesDropped === 0;
459
+ earlier.textContent = card.notesDropped === 0 ? "" : card.notesDropped + " earlier notes dropped";
460
+
461
+ // A note is a write, so a viewer's dialog carries none — and its actions are what
462
+ // board.may leaves, which for a viewer is Close alone.
463
+ var noteComposer = document.getElementById("card-dialog-note-composer");
464
+ if (noteComposer) noteComposer.hidden = !canWrite();
465
+ board.fillDialogActions(card, view);
466
+ }
467
+
468
+ /** Paint a whole snapshot. */
469
+ board.render = function (view) {
470
+ board.view = view;
471
+ renderChrome(view);
472
+ renderLanes(view);
473
+ board.renderTeam(view);
474
+ if (board.openId !== undefined) {
475
+ var card = findCard(view, board.openId);
476
+ if (card) fillDialog(card, view);
477
+ else board.closeCard();
478
+ }
479
+ };
480
+
481
+ board.el = el;
482
+ board.chip = chip;
483
+ board.actionButton = actionButton;
484
+ board.ago = ago;
485
+ board.findCard = findCard;
486
+ board.mine = mine;
487
+ board.matches = matches;
488
+ board.canWrite = canWrite;
489
+ board.priorityOf = priorityOf;
490
+ board.blockersOf = blockersOf;
491
+ board.waitingText = waitingText;
492
+ board.may = may;
493
+ board.fillDialog = fillDialog;
494
+ board.refreshAges = refreshAges;
495
+ board.plural = plural;
496
+ })();`;