@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,397 @@
1
+ /**
2
+ * The client's wiring: the tabs, the lane picker, the new-card dialog, the filter,
3
+ * and the live frame stream.
4
+ *
5
+ * A frame is a whole snapshot, so every message repaints from it rather than patching
6
+ * it — the renderer decides what actually changed. The only client state is which tab
7
+ * is showing, the filter text, which lanes are collapsed, and which card's dialog is
8
+ * open; none of it is authoritative, and none of it is sent anywhere.
9
+ */
10
+
11
+ export const BOARD_LIVE_JS = String.raw`(function () {
12
+ "use strict";
13
+ var board = window.bfBoard || (window.bfBoard = {});
14
+ var token = new URLSearchParams(window.location.search).get("token") || "";
15
+ var LANE_LABEL = { todo: "To do", doing: "Doing", blocked: "Blocked", done: "Done" };
16
+
17
+ /** The lane's word, for the dialog's move buttons. */
18
+ board.laneLabel = function (lane) {
19
+ return LANE_LABEL[lane] || lane;
20
+ };
21
+
22
+ /** The badge's text and its kit status role: connecting, live, reconnecting. */
23
+ function badge(state, at) {
24
+ var node = document.getElementById("board-status");
25
+ if (!node) return;
26
+ node.setAttribute("data-state", state);
27
+ node.className = "bf-status board-status" +
28
+ (state === "live" ? " bf-status--saved" : state === "reconnecting" ? " bf-status--error" : "");
29
+ // "now" would read as "live · now ago", so a fresh frame says only "live".
30
+ if (state === "live") {
31
+ var age = at === undefined ? "now" : board.ago(at);
32
+ node.textContent = age === "now" ? "live" : "live · " + age;
33
+ }
34
+ else if (state === "reconnecting") node.textContent = "reconnecting…";
35
+ else node.textContent = "connecting…";
36
+ }
37
+
38
+ /** Show one tab: the panel, aria-current, and the address so a link is shareable. */
39
+ function showView(view) {
40
+ var team = view === "team";
41
+ document.documentElement.setAttribute("data-view", team ? "team" : "board");
42
+ document.getElementById("panel-board").hidden = team;
43
+ document.getElementById("panel-team").hidden = !team;
44
+ var tabs = document.querySelectorAll("[data-view][role='tab']");
45
+ for (var i = 0; i < tabs.length; i++) {
46
+ var current = tabs[i].getAttribute("data-view") === (team ? "team" : "board");
47
+ if (current) tabs[i].setAttribute("aria-current", "page");
48
+ else tabs[i].removeAttribute("aria-current");
49
+ }
50
+ var url = new URL(window.location.href);
51
+ url.searchParams.set("view", team ? "team" : "board");
52
+ window.history.replaceState(null, "", url.toString());
53
+ }
54
+
55
+ board.viewName = function () {
56
+ return document.documentElement.getAttribute("data-view") === "team" ? "team" : "board";
57
+ };
58
+
59
+ /** The narrow layout shows one lane; the picker is the only way to change it. */
60
+ function applyLane() {
61
+ var narrow = window.matchMedia && window.matchMedia("(max-width: 860px)").matches;
62
+ var lanes = document.querySelectorAll(".board-lane");
63
+ for (var i = 0; i < lanes.length; i++) {
64
+ var selected = lanes[i].getAttribute("data-status") === board.lane;
65
+ lanes[i].hidden = narrow && !selected;
66
+ }
67
+ var items = document.querySelectorAll("#board-picker [data-lane]");
68
+ for (var j = 0; j < items.length; j++) {
69
+ if (items[j].getAttribute("data-lane") === board.lane) items[j].setAttribute("aria-current", "page");
70
+ else items[j].removeAttribute("aria-current");
71
+ }
72
+ }
73
+
74
+ board.applyLane = applyLane;
75
+
76
+ board.showLane = function (lane) {
77
+ board.lane = lane;
78
+ applyLane();
79
+ };
80
+
81
+ /** Fold a lane's cards away and leave its head, count and all. */
82
+ board.toggleLane = function (status) {
83
+ board.collapsed = board.collapsed || {};
84
+ board.collapsed[status] = board.collapsed[status] !== true;
85
+ if (board.view) board.render(board.view);
86
+ };
87
+
88
+ board.showView = showView;
89
+
90
+ /**
91
+ * The composer writes one card: a new one, or the one the card dialog was showing.
92
+ *
93
+ * The mode lives here rather than in the form, because the form is a draft: leaving
94
+ * edit mode puts the dialog back the way the toolbar's button opens it, and the text
95
+ * it held was the card's, not the reader's.
96
+ */
97
+ var composer = { mode: "add", card: undefined };
98
+
99
+ /** The title's character count, shown from the first character typed. */
100
+ function showTitleCount() {
101
+ var field = document.getElementById("new-title");
102
+ var count = document.getElementById("new-title-count");
103
+ if (!field || !count) return;
104
+ var max = Number(field.getAttribute("maxlength"));
105
+ if (!max) return;
106
+ var used = field.value.length;
107
+ count.textContent = used + "/" + max;
108
+ count.hidden = used === 0;
109
+ }
110
+
111
+ /** Dress the dialog for a new card or for the one being edited, and fill it in. */
112
+ function setComposerMode(mode, card) {
113
+ var heading = document.getElementById("board-new-title");
114
+ var submit = document.getElementById("new-submit");
115
+ var hint = document.getElementById("composer-hint");
116
+ var form = document.getElementById("board-composer");
117
+ var was = composer.mode;
118
+ composer.mode = mode;
119
+ composer.card = mode === "edit" ? card : undefined;
120
+ if (mode === "edit") {
121
+ var title = document.getElementById("new-title");
122
+ var paths = document.getElementById("new-paths");
123
+ var priority = document.getElementById("new-priority");
124
+ if (title) title.value = card.title;
125
+ if (paths) paths.value = (card.paths || []).join(" ");
126
+ if (priority) priority.value = board.priorityOf(card);
127
+ if (heading) heading.textContent = "Edit card #" + card.id;
128
+ if (submit) submit.textContent = "Save";
129
+ if (hint) hint.textContent = "links are recorded with the board tool, while the card is being planned";
130
+ showTitleCount();
131
+ return;
132
+ }
133
+ // Leaving edit mode throws its text away; a draft written here survives Esc.
134
+ if (was === "edit" && form) form.reset();
135
+ if (heading) heading.textContent = "New card";
136
+ if (submit) submit.textContent = "Add card";
137
+ if (hint && board.composerHint !== undefined) hint.textContent = board.composerHint;
138
+ showTitleCount();
139
+ }
140
+
141
+ /** Open whichever mode the composer is dressed for, with the caret in the title. */
142
+ function openComposerForm() {
143
+ if (!window.bfNew) return;
144
+ // The kit focuses a dialog's first control, which here is its close button; a card
145
+ // starts with a title, so the caret belongs in that field.
146
+ window.bfNew.open();
147
+ var input = document.getElementById("new-title");
148
+ if (input) input.focus();
149
+ }
150
+
151
+ /** Open the new-card dialog. */
152
+ board.openComposer = function () {
153
+ if (composer.mode === "edit") setComposerMode("add");
154
+ openComposerForm();
155
+ };
156
+
157
+ /** Open it on one card: the same two fields, prefilled, with Save in place of Add. */
158
+ board.editCard = function (card) {
159
+ if (!window.bfNew) return;
160
+ board.closeCard();
161
+ setComposerMode("edit", card);
162
+ openComposerForm();
163
+ var title = document.getElementById("new-title");
164
+ // The end of the title is where a typo is, so that is where the caret starts.
165
+ if (title && title.setSelectionRange) title.setSelectionRange(title.value.length, title.value.length);
166
+ };
167
+
168
+ /** Close it and keep the draft: only Cancel throws one away. */
169
+ board.closeComposer = function () {
170
+ if (window.bfNew) window.bfNew.close();
171
+ };
172
+
173
+ /** Discard the draft and close, which is what its own Cancel button does. */
174
+ board.cancelComposer = function () {
175
+ var form = document.getElementById("board-composer");
176
+ if (form) form.reset();
177
+ setComposerMode("add");
178
+ board.closeComposer();
179
+ };
180
+
181
+ board.composerOpen = function () {
182
+ return window.bfNew ? window.bfNew.isOpen() : false;
183
+ };
184
+
185
+ board.focusComposer = function () {
186
+ board.openComposer();
187
+ };
188
+ board.focusFilter = function () {
189
+ var input = document.getElementById("board-filter");
190
+ if (input) {
191
+ input.focus();
192
+ input.select();
193
+ }
194
+ };
195
+
196
+ /** Apply the filter box's text to the board, without waiting for a frame. */
197
+ board.applyFilter = function () {
198
+ var input = document.getElementById("board-filter");
199
+ board.query = input ? input.value.trim() : "";
200
+ if (board.view) board.render(board.view);
201
+ };
202
+
203
+ board.hasFilter = function () {
204
+ return board.query !== undefined && board.query !== "";
205
+ };
206
+
207
+ board.clearFilter = function () {
208
+ var input = document.getElementById("board-filter");
209
+ if (input && input.value !== "") {
210
+ input.value = "";
211
+ board.applyFilter();
212
+ }
213
+ };
214
+
215
+ board.openKeys = function () {
216
+ if (window.bfKeys) window.bfKeys.open();
217
+ };
218
+ board.closeKeys = function () {
219
+ if (window.bfKeys) window.bfKeys.close();
220
+ };
221
+ board.keysOpen = function () {
222
+ return window.bfKeys ? window.bfKeys.isOpen() : false;
223
+ };
224
+
225
+ /**
226
+ * The grey cards a page shows while it waits for its first frame.
227
+ *
228
+ * They are marked as placeholders rather than given a lane status: the renderer
229
+ * looks lanes up by data-status, so a skeleton that carried one would be adopted
230
+ * as the real lane and the cards would never be drawn into it.
231
+ */
232
+ function skeletons() {
233
+ var lanes = document.getElementById("board-lanes");
234
+ if (!lanes || lanes.childElementCount > 0) return;
235
+ lanes.setAttribute("data-skeleton", "1");
236
+ for (var i = 0; i < 4; i++) {
237
+ var lane = board.el("section", "board-lane");
238
+ for (var j = 0; j < 2; j++) lane.appendChild(board.el("div", "board-skeleton"));
239
+ lanes.appendChild(lane);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * The lane a narrow reader lands on: the first with a card in it, else the first
245
+ * lane. Opening on an empty To do while the only card sits in Blocked is a blank
246
+ * page with a picker nobody has a reason to press.
247
+ */
248
+ function laneWithWork(view) {
249
+ for (var i = 0; i < view.lanes.length; i++) if (view.lanes[i].cards.length > 0) return view.lanes[i].status;
250
+ return view.lanes[0] ? view.lanes[0].status : "todo";
251
+ }
252
+
253
+ /** One frame: paint and record when it arrived. */
254
+ function paint(view) {
255
+ board.lastFrame = Date.now();
256
+ board.lane = board.lane || laneWithWork(view);
257
+ badge("live", board.lastFrame);
258
+ board.render(view);
259
+ }
260
+
261
+ /** The live channel: a reconnect is the browser's own retry, not a second request. */
262
+ function stream() {
263
+ var source = new EventSource("/events?token=" + encodeURIComponent(token));
264
+ source.onopen = function () {
265
+ badge("live", board.lastFrame);
266
+ };
267
+ source.onmessage = function (event) {
268
+ var view;
269
+ try {
270
+ view = JSON.parse(event.data);
271
+ } catch (error) {
272
+ return;
273
+ }
274
+ paint(view);
275
+ };
276
+ source.onerror = function () {
277
+ badge("reconnecting");
278
+ };
279
+ }
280
+
281
+ /** The new-card dialog: a title, space-separated path hints, a priority, and Add. */
282
+ function wireComposer() {
283
+ var form = document.getElementById("board-composer");
284
+ if (!form) return;
285
+ // The hint is the page's, not this module's: a second copy of the sentence would drift.
286
+ var hint = document.getElementById("composer-hint");
287
+ board.composerHint = hint ? hint.textContent : "";
288
+ // The field is capped at 120 characters, and a cap nobody can see reads as a bug the
289
+ // first time typing stops; the count appears with the first character.
290
+ var titleField = document.getElementById("new-title");
291
+ if (titleField) titleField.addEventListener("input", showTitleCount);
292
+ form.addEventListener("submit", function (event) {
293
+ event.preventDefault();
294
+ var title = document.getElementById("new-title");
295
+ if (!title.value.trim()) {
296
+ title.focus();
297
+ return;
298
+ }
299
+ var paths = document.getElementById("new-paths").value.split(/\s+/).filter(Boolean);
300
+ var priorityField = document.getElementById("new-priority");
301
+ var priority = priorityField ? priorityField.value : "normal";
302
+ if (composer.mode === "edit") {
303
+ var card = composer.card;
304
+ if (!card) return;
305
+ // Every field goes, including the unchanged ones: an edit means "this is what it
306
+ // should say", and a priority changed back to normal is a change too.
307
+ board.act("edit", card, { title: title.value.trim(), paths: paths, priority: priority }).then(function (payload) {
308
+ if (payload) board.cancelComposer();
309
+ });
310
+ return;
311
+ }
312
+ var input = { title: title.value.trim(), paths: paths };
313
+ if (priority !== "normal") input.priority = priority;
314
+ board.add(input).then(function (payload) {
315
+ if (!payload) return;
316
+ // The dialog stays open: a lead publishes several cards in a row.
317
+ form.reset();
318
+ showTitleCount();
319
+ title.focus();
320
+ });
321
+ });
322
+ var trigger = document.getElementById("board-new");
323
+ if (trigger) {
324
+ trigger.addEventListener("click", function () {
325
+ if (board.composerOpen()) board.closeComposer();
326
+ else board.openComposer();
327
+ });
328
+ }
329
+ // The dialog's single closer: Esc and the backdrop keep the draft, this does not.
330
+ var cancel = document.getElementById("board-cancel");
331
+ if (cancel) {
332
+ cancel.addEventListener("click", function () {
333
+ board.cancelComposer();
334
+ });
335
+ }
336
+ var first = document.getElementById("board-first-card");
337
+ if (first) {
338
+ first.addEventListener("click", function () {
339
+ board.openComposer();
340
+ });
341
+ }
342
+ }
343
+
344
+ /** The filter, the note button, and the two tab sets. */
345
+ function wireChrome() {
346
+ var filter = document.getElementById("board-filter");
347
+ if (filter) {
348
+ filter.addEventListener("input", function () {
349
+ board.applyFilter();
350
+ });
351
+ }
352
+ var noteAdd = document.getElementById("card-dialog-note-add");
353
+ if (noteAdd) noteAdd.addEventListener("click", function () { board.addNote(); });
354
+
355
+ var tabs = document.querySelectorAll("[data-view][role='tab']");
356
+ for (var i = 0; i < tabs.length; i++) {
357
+ tabs[i].addEventListener("click", function (event) {
358
+ showView(event.currentTarget.getAttribute("data-view"));
359
+ });
360
+ }
361
+ var picker = document.getElementById("board-picker");
362
+ if (picker) {
363
+ picker.addEventListener("click", function (event) {
364
+ var lane = event.target && event.target.getAttribute ? event.target.getAttribute("data-lane") : null;
365
+ if (lane) board.showLane(lane);
366
+ });
367
+ }
368
+ if (window.matchMedia) {
369
+ var query = window.matchMedia("(max-width: 860px)");
370
+ var onChange = function () {
371
+ applyLane();
372
+ };
373
+ if (query.addEventListener) query.addEventListener("change", onChange);
374
+ else if (query.addListener) query.addListener(onChange);
375
+ }
376
+ // The badge says how fresh the last frame is, which is the one thing a quiet
377
+ // board cannot show by changing; the ages are the other, so the same tick keeps
378
+ // them honest without waiting for somebody to write.
379
+ window.setInterval(function () {
380
+ if (board.lastFrame !== undefined) badge("live", board.lastFrame);
381
+ board.refreshAges();
382
+ }, 1000);
383
+ }
384
+
385
+ board.start = function () {
386
+ board.lane = undefined;
387
+ board.openId = undefined;
388
+ board.query = "";
389
+ board.collapsed = {};
390
+ showView(new URLSearchParams(window.location.search).get("view") === "team" ? "team" : "board");
391
+ wireChrome();
392
+ wireComposer();
393
+ skeletons();
394
+ board.bindKeys(document);
395
+ stream();
396
+ };
397
+ })();`;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The board page's document: an inert shell the client fills from the snapshot.
3
+ *
4
+ * The page carries no card text in its markup — not even escaped — so there is no
5
+ * path from a card file to the document's HTML. Every field the client shows is set
6
+ * as `textContent` from the JSON frame, which is the one property a page with a
7
+ * model-writable store should have.
8
+ *
9
+ * The shell is complete before the script runs: the toolbar, both panels, the three
10
+ * kit dialogs, the keyboard sheet, and the live region are in the markup, so a slow
11
+ * frame shows a skeleton rather than an empty page. Every id here is a contract with
12
+ * the client (`render.ts`, `team.ts`, `live.ts`, `actions.ts`), and the class
13
+ * contract test holds every class to the kit or to this page's stylesheet.
14
+ */
15
+
16
+ import { brandLockup, button, docHead, keysSheet, themeToggle } from "../../../lib/web/index.ts";
17
+ import { BOARD_KEYS } from "./keys.ts";
18
+ import { BOARD_CSS } from "./styles.ts";
19
+
20
+ /** The two views the page offers. */
21
+ export type PageView = "board" | "team";
22
+
23
+ /**
24
+ * Render the document for one initial view.
25
+ *
26
+ * Both panels are rendered and one is `hidden`, so switching tabs costs no request
27
+ * and a shared link opens the tab it names. `writes` is the credential's authority:
28
+ * a viewer's document says `data-writes="0"`, which is what makes the client draw a
29
+ * page with no button the server would refuse.
30
+ */
31
+ export function renderBoardPage(input: { view: PageView; writes?: boolean }): string {
32
+ const team = input.view === "team";
33
+ return `<!doctype html>
34
+ <html lang="en" data-view="${team ? "team" : "board"}" data-writes="${input.writes === false ? "0" : "1"}">
35
+ <head>
36
+ ${docHead({ title: "board", css: BOARD_CSS })}
37
+ </head>
38
+ <body class="bf-app">
39
+ <h1 class="sr-only">board — the shared feature list for this repository</h1>
40
+ <a class="skip" href="#main">Skip to the cards</a>
41
+ <header class="bf-app__header"><div class="bf-toolbar board-toolbar">
42
+ ${brandLockup("board")}
43
+ <span class="bf-chip bf-chip--accent" id="board-me" title="Writes from this page are made as this session">board</span>
44
+ <p class="bf-status board-status" id="board-status" data-state="connecting" aria-live="polite">connecting…</p>
45
+ <span class="bf-spacer"></span>
46
+ <div class="board-controls">
47
+ <div class="bf-segmented" role="tablist" aria-label="View">
48
+ <button type="button" class="bf-segmented__item" role="tab" id="tab-board" data-view="board" aria-controls="panel-board"${team ? "" : ' aria-current="page"'}>Board</button>
49
+ <button type="button" class="bf-segmented__item" role="tab" id="tab-team" data-view="team" aria-controls="panel-team"${team ? ' aria-current="page"' : ""}>Team</button>
50
+ </div>
51
+ <p class="bf-progress board-progress" id="board-progress" hidden>
52
+ <span class="bf-progress__track"><span class="bf-progress__fill" id="board-progress-fill"></span></span>
53
+ <span id="board-progress-text">0/0</span>
54
+ </p>
55
+ <label class="bf-field board-filter"><span class="sr-only">Filter cards</span>
56
+ <input class="bf-input" id="board-filter" type="search" placeholder="filter cards" autocomplete="off">
57
+ </label>
58
+ ${button({ label: "+ New card", id: "board-new", title: "New card — n", attrs: { "aria-haspopup": "dialog" } })}
59
+ ${button({ label: "?", variant: "icon", id: "keys-open", title: "Keyboard shortcuts", attrs: { "aria-haspopup": "dialog" } })}
60
+ ${themeToggle()}
61
+ </div>
62
+ </div></header>
63
+ <main class="bf-app__main board-main" id="main" tabindex="-1">
64
+ <p class="bf-note board-banner" id="board-banner" role="status" hidden></p>
65
+ <section class="board-panel" id="panel-board" role="tabpanel" aria-labelledby="tab-board"${team ? " hidden" : ""}>
66
+ <div class="bf-segmented board-picker" id="board-picker" role="tablist" aria-label="Lane"></div>
67
+ <div class="board-lanes" id="board-lanes"></div>
68
+ <div class="bf-empty board-no-cards" id="board-no-cards" hidden>
69
+ <p class="bf-empty__title">No cards yet</p>
70
+ <p class="bf-empty__sub">One card per feature: what it delivers, who holds it, and where to start.</p>
71
+ <button class="bf-btn bf-btn--primary" type="button" id="board-first-card">Add the first card</button>
72
+ </div>
73
+ </section>
74
+ <section class="board-panel" id="panel-team" role="tabpanel" aria-labelledby="tab-team"${team ? "" : " hidden"}>
75
+ <div class="board-team" id="board-team"></div>
76
+ </section>
77
+ </main>
78
+ <footer class="bf-app__footer"><span id="board-where"></span></footer>
79
+
80
+ <div class="bf-dialog" id="board-new-dialog" hidden>
81
+ <div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="board-new-title">
82
+ <div class="bf-dialog__head">
83
+ <h2 id="board-new-title">New card</h2>
84
+ </div>
85
+ <form class="board-composer" id="board-composer" autocomplete="off">
86
+ <div class="bf-dialog__body">
87
+ <div class="board-composer__fields">
88
+ <label class="board-composer__field board-composer__field--title">
89
+ <span class="board-composer__label">Title<span class="board-composer__count" id="new-title-count" hidden></span></span>
90
+ <span class="bf-field"><input class="bf-input" id="new-title" name="title" placeholder="what it delivers, in one line" maxlength="120" required></span>
91
+ </label>
92
+ <label class="board-composer__field">
93
+ <span class="board-composer__label">Paths</span>
94
+ <span class="bf-field"><input class="bf-input" id="new-paths" name="paths" placeholder="extensions/board lib/web" aria-describedby="composer-hint"></span>
95
+ </label>
96
+ <label class="board-composer__field">
97
+ <span class="board-composer__label">Priority</span>
98
+ <span class="bf-field"><select class="bf-input board-composer__select" id="new-priority" name="priority">
99
+ <option value="high">high</option>
100
+ <option value="normal" selected>normal</option>
101
+ <option value="low">low</option>
102
+ </select></span>
103
+ </label>
104
+ </div>
105
+ <p class="bf-dialog__hint" id="composer-hint">one card per feature — its steps live in the owner's plan and todo</p>
106
+ </div>
107
+ <div class="bf-dialog__actions">
108
+ <button class="bf-btn" type="button" id="board-cancel">Cancel</button>
109
+ <button class="bf-btn bf-btn--primary" type="submit" id="new-submit">Add card</button>
110
+ </div>
111
+ </form>
112
+ </div>
113
+ </div>
114
+
115
+ <div class="bf-dialog" id="board-card-dialog" hidden>
116
+ <div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="card-dialog-title" tabindex="-1">
117
+ <div class="bf-dialog__head">
118
+ <h2 id="card-dialog-title"></h2>
119
+ <span class="board-dialog__state" id="card-dialog-state"></span>
120
+ <span class="bf-spacer"></span>
121
+ <button type="button" class="bf-btn bf-btn--icon" id="card-dialog-close" aria-label="Close">×</button>
122
+ </div>
123
+ <div class="bf-dialog__body">
124
+ <p class="board-dialog__meta" id="card-dialog-meta"></p>
125
+ <p class="board-dialog__owner" id="card-dialog-owner"></p>
126
+ <ul class="board-card__paths" id="card-dialog-paths"></ul>
127
+ <p class="board-dialog__depends" id="card-dialog-depends" hidden></p>
128
+ <p class="board-dialog__plan" id="card-dialog-plan" hidden></p>
129
+ <ol class="board-dialog__notes" id="card-dialog-notes"></ol>
130
+ <p class="bf-dialog__hint" id="card-dialog-earlier" hidden></p>
131
+ <div class="bf-composer board-dialog__note-composer" id="card-dialog-note-composer">
132
+ <label class="bf-field"><span class="sr-only">Add a note</span>
133
+ <input class="bf-input" id="card-dialog-note" placeholder="add a note" maxlength="1000">
134
+ </label>
135
+ <div class="bf-composer__row"><button type="button" class="bf-btn" id="card-dialog-note-add">Note</button></div>
136
+ </div>
137
+ </div>
138
+ <div class="bf-dialog__actions" id="card-dialog-actions"></div>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="bf-dialog board-confirm" id="board-confirm" hidden>
143
+ <div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="confirm-title" tabindex="-1">
144
+ <div class="bf-dialog__head"><h2 id="confirm-title"></h2></div>
145
+ <div class="bf-dialog__body"><p class="board-confirm__body" id="confirm-body"></p></div>
146
+ <div class="bf-dialog__actions">
147
+ <button type="button" class="bf-btn" id="confirm-cancel">Cancel</button>
148
+ <button type="button" class="bf-btn bf-btn--primary" id="confirm-ok">Confirm</button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+
153
+ ${keysSheet(BOARD_KEYS)}
154
+ <script src="/app.js" defer></script>
155
+ </body>
156
+ </html>
157
+ `;
158
+ }