@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 (298) hide show
  1. package/README.md +46 -30
  2. package/README.zh-CN.md +40 -25
  3. package/assets/screens/board.png +0 -0
  4. package/assets/screens/chrome.png +0 -0
  5. package/assets/screens/file-browse.png +0 -0
  6. package/assets/screens/plan-review.png +0 -0
  7. package/assets/screens/plan.png +0 -0
  8. package/docs/extensions/README.md +22 -23
  9. package/docs/extensions/board.md +397 -168
  10. package/docs/extensions/bytefly.md +234 -197
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +57 -63
  14. package/docs/extensions/file-open.md +9 -11
  15. package/docs/extensions/file-read.md +12 -14
  16. package/docs/extensions/goal.md +15 -17
  17. package/docs/extensions/job.md +54 -42
  18. package/docs/extensions/memory.md +17 -19
  19. package/docs/extensions/plan.md +197 -140
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +145 -0
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +83 -44
  26. package/docs/extensions/todo.md +18 -18
  27. package/docs/extensions/watch.md +31 -31
  28. package/docs/extensions/web-access.md +2 -4
  29. package/docs/extensions/wechat.md +34 -35
  30. package/docs/extensions/worktree.md +56 -50
  31. package/extensions/board/commands.ts +79 -25
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +17 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +5 -20
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +65 -93
  38. package/extensions/board/result.ts +30 -39
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +49 -11
  41. package/extensions/board/snapshot.ts +8 -6
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +154 -55
  44. package/extensions/board/transport/router.ts +3 -5
  45. package/extensions/board/transport/server.ts +59 -28
  46. package/extensions/board/types.ts +36 -42
  47. package/extensions/board/view/card.ts +229 -0
  48. package/extensions/board/view/client.ts +35 -16
  49. package/extensions/board/view/dialog.ts +169 -6
  50. package/extensions/board/view/empty.ts +47 -0
  51. package/extensions/board/view/keys.ts +13 -2
  52. package/extensions/board/view/live.ts +90 -28
  53. package/extensions/board/view/names.ts +13 -4
  54. package/extensions/board/view/page.ts +38 -13
  55. package/extensions/board/view/render.ts +219 -277
  56. package/extensions/board/view/styles.ts +130 -32
  57. package/extensions/board/view/team.ts +297 -76
  58. package/extensions/bytefly/block/fallback.ts +1 -1
  59. package/extensions/bytefly/doctor/runtime.ts +5 -2
  60. package/extensions/bytefly/footer/collect/label.ts +60 -0
  61. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  62. package/extensions/bytefly/footer/config.ts +7 -8
  63. package/extensions/bytefly/footer/register.ts +33 -4
  64. package/extensions/bytefly/footer/render/footer.ts +18 -4
  65. package/extensions/bytefly/footer/render/key.ts +46 -0
  66. package/extensions/bytefly/footer/render/layout.ts +13 -3
  67. package/extensions/bytefly/footer/render/lines.ts +10 -7
  68. package/extensions/bytefly/footer/render/segments.ts +41 -38
  69. package/extensions/bytefly/footer/render/status.ts +0 -11
  70. package/extensions/bytefly/footer/types.ts +7 -0
  71. package/extensions/bytefly/header/component.ts +4 -20
  72. package/extensions/bytefly/header/config.ts +1 -1
  73. package/extensions/bytefly/index.ts +32 -6
  74. package/extensions/bytefly/mode/editor.ts +115 -0
  75. package/extensions/bytefly/panel/commands.ts +210 -120
  76. package/extensions/bytefly/panel/format.ts +4 -4
  77. package/extensions/bytefly/panel/settings.ts +73 -6
  78. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  79. package/extensions/bytefly/update/cache.ts +1 -1
  80. package/extensions/bytefly/update/register.ts +4 -4
  81. package/extensions/bytefly/working/config.ts +5 -7
  82. package/extensions/bytefly/working/format.ts +10 -13
  83. package/extensions/bytefly/working/register.ts +22 -27
  84. package/extensions/check/commands.ts +3 -2
  85. package/extensions/check/render.ts +10 -1
  86. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  87. package/extensions/context/commands.ts +2 -2
  88. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  89. package/extensions/file-browse/config/config.ts +4 -19
  90. package/extensions/file-browse/index.ts +89 -79
  91. package/extensions/file-browse/live/live.ts +1 -1
  92. package/extensions/file-browse/transport/router.ts +12 -4
  93. package/extensions/file-browse/transport/server.ts +61 -28
  94. package/extensions/file-browse/view/chrome.ts +4 -4
  95. package/extensions/file-browse/view/client.ts +3 -1
  96. package/extensions/file-browse/view/html.ts +73 -40
  97. package/extensions/file-browse/view/live-client.ts +2 -2
  98. package/extensions/file-browse/view/markdown.ts +13 -14
  99. package/extensions/file-browse/view/palette.ts +2 -2
  100. package/extensions/file-browse/view/raw-html.ts +9 -9
  101. package/extensions/file-browse/view/tree.ts +1 -1
  102. package/extensions/file-browse/view/urls.ts +20 -6
  103. package/extensions/file-read/render.ts +4 -4
  104. package/extensions/goal/plan-route.ts +2 -2
  105. package/extensions/goal/tui.ts +12 -2
  106. package/extensions/job/child.ts +527 -0
  107. package/extensions/job/clock.ts +142 -0
  108. package/extensions/job/commands.ts +4 -2
  109. package/extensions/job/config.ts +8 -1
  110. package/extensions/job/format.ts +22 -1
  111. package/extensions/job/log-writer.ts +13 -2
  112. package/extensions/job/registry.ts +63 -7
  113. package/extensions/job/render.ts +34 -0
  114. package/extensions/job/runtime.ts +296 -655
  115. package/extensions/job/session.ts +132 -19
  116. package/extensions/job/store.ts +7 -7
  117. package/extensions/job/tools.ts +21 -9
  118. package/extensions/job/types.ts +47 -0
  119. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
  120. package/extensions/job/waiters.ts +44 -1
  121. package/extensions/memory/commands.ts +4 -2
  122. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  123. package/extensions/plan/commands.ts +12 -9
  124. package/extensions/plan/feedback.ts +62 -16
  125. package/extensions/plan/format.ts +8 -2
  126. package/extensions/plan/git-bash.ts +115 -16
  127. package/extensions/plan/index.ts +58 -51
  128. package/extensions/plan/plan-index.ts +4 -2
  129. package/extensions/plan/plan-text.ts +114 -0
  130. package/extensions/plan/plans.ts +5 -54
  131. package/extensions/plan/policy.ts +8 -10
  132. package/extensions/plan/review-lazy.ts +85 -0
  133. package/extensions/plan/review.ts +2 -2
  134. package/extensions/plan/runtime.ts +61 -41
  135. package/extensions/plan/sections.ts +1 -1
  136. package/extensions/plan/steps.ts +3 -3
  137. package/extensions/plan/todos.ts +1 -1
  138. package/extensions/plan/tools/cancel.ts +27 -62
  139. package/extensions/plan/tools/enter.ts +55 -81
  140. package/extensions/plan/tools/exit.ts +221 -238
  141. package/extensions/plan/tools/refusal.ts +18 -0
  142. package/extensions/plan/tools/steps.ts +75 -92
  143. package/extensions/plan/tools/write.ts +77 -120
  144. package/extensions/plan/tools.ts +188 -30
  145. package/extensions/plan/tui.ts +5 -5
  146. package/extensions/plan/types.ts +48 -5
  147. package/extensions/plan/web-blocks.ts +76 -5
  148. package/extensions/plan/web-chrome.ts +4 -0
  149. package/extensions/plan/web-client.ts +102 -2
  150. package/extensions/plan/web-collab.ts +4 -4
  151. package/extensions/plan/web-comments.ts +39 -8
  152. package/extensions/plan/web-composer.ts +14 -3
  153. package/extensions/plan/web-keys.ts +149 -25
  154. package/extensions/plan/web-lazy.ts +53 -0
  155. package/extensions/plan/web-markdown.ts +10 -68
  156. package/extensions/plan/web-outline.ts +22 -2
  157. package/extensions/plan/web-page.ts +56 -86
  158. package/extensions/plan/web-payload.ts +188 -0
  159. package/extensions/plan/web-plan.ts +23 -13
  160. package/extensions/plan/web-router.ts +18 -9
  161. package/extensions/plan/web-settle.ts +14 -7
  162. package/extensions/plan/web-share-session.ts +24 -55
  163. package/extensions/plan/web-share.ts +19 -37
  164. package/extensions/plan/web-write.ts +112 -0
  165. package/extensions/plan/web.ts +67 -131
  166. package/extensions/question/tools.ts +6 -2
  167. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  168. package/extensions/rewind/commands/commands.ts +6 -3
  169. package/extensions/rewind/state/runtime.ts +1 -1
  170. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  171. package/extensions/rewind/view/review-tui.ts +4 -4
  172. package/extensions/serve/config.ts +39 -0
  173. package/extensions/serve/index.ts +85 -0
  174. package/extensions/serve/registry.ts +260 -0
  175. package/extensions/session-bus/commands.ts +11 -6
  176. package/extensions/session-bus/join-request.ts +17 -9
  177. package/extensions/session-bus/mail-watch.ts +26 -17
  178. package/extensions/session-bus/protocol.ts +15 -3
  179. package/extensions/session-bus/resolve.ts +5 -7
  180. package/extensions/session-bus/result.ts +1 -27
  181. package/extensions/session-bus/runtime.ts +40 -12
  182. package/extensions/session-bus/store.ts +101 -19
  183. package/extensions/session-bus/tools.ts +5 -4
  184. package/extensions/session-bus/types.ts +19 -0
  185. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  186. package/extensions/subagent/agents.ts +1 -1
  187. package/extensions/subagent/commands.ts +4 -2
  188. package/extensions/subagent/format.ts +15 -15
  189. package/extensions/subagent/index.ts +1 -1
  190. package/extensions/subagent/render.ts +3 -3
  191. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  192. package/extensions/team/commands.ts +24 -5
  193. package/extensions/team/dispatch.ts +18 -4
  194. package/extensions/team/format.ts +17 -2
  195. package/extensions/team/index.ts +9 -8
  196. package/extensions/team/schema.ts +6 -5
  197. package/extensions/team/tools.ts +107 -45
  198. package/extensions/team/types.ts +3 -2
  199. package/extensions/todo/commands.ts +4 -2
  200. package/extensions/todo/format.ts +11 -0
  201. package/extensions/todo/render.ts +57 -0
  202. package/extensions/todo/runtime.ts +1 -1
  203. package/extensions/todo/tools.ts +1 -1
  204. package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
  205. package/extensions/todo/widget.ts +47 -0
  206. package/extensions/watch/commands.ts +4 -2
  207. package/extensions/watch/format.ts +36 -7
  208. package/extensions/watch/render.ts +57 -0
  209. package/extensions/watch/runtime.ts +27 -22
  210. package/extensions/watch/tools.ts +4 -4
  211. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  212. package/extensions/web-access/fetch/tool.ts +12 -4
  213. package/extensions/wechat/commands/commands.ts +18 -14
  214. package/extensions/wechat/index.ts +72 -37
  215. package/extensions/wechat/state/credentials.ts +1 -1
  216. package/extensions/wechat/state/state.ts +1 -1
  217. package/extensions/wechat/text/status.ts +112 -0
  218. package/extensions/wechat/types.ts +2 -2
  219. package/extensions/wechat/view/status-screen.ts +4 -103
  220. package/extensions/worktree/commands.ts +11 -9
  221. package/extensions/worktree/enter.ts +2 -2
  222. package/extensions/worktree/exit.ts +2 -2
  223. package/extensions/worktree/index.ts +5 -5
  224. package/extensions/worktree/screen.ts +3 -3
  225. package/extensions/worktree/status.ts +2 -2
  226. package/extensions/worktree/tools.ts +113 -116
  227. package/lib/README.md +28 -13
  228. package/lib/board/actions.ts +111 -0
  229. package/lib/board/artifact.ts +149 -0
  230. package/lib/board/card.ts +163 -52
  231. package/lib/board/config.ts +58 -0
  232. package/lib/board/contract.ts +113 -0
  233. package/lib/board/join.ts +38 -0
  234. package/lib/board/lanes.ts +182 -0
  235. package/lib/board/lead.ts +130 -0
  236. package/lib/board/limits.ts +78 -0
  237. package/lib/board/links.ts +31 -10
  238. package/lib/board/page-pointer.ts +30 -5
  239. package/lib/board/read.ts +83 -0
  240. package/lib/board/reason.ts +102 -0
  241. package/lib/board/round-text.ts +68 -6
  242. package/lib/board/round.ts +31 -15
  243. package/lib/board/rules.ts +151 -307
  244. package/lib/board/seat.ts +35 -0
  245. package/lib/board/session.ts +199 -51
  246. package/lib/board/skipped.ts +22 -0
  247. package/lib/board/steps.ts +487 -0
  248. package/lib/board/store.ts +119 -36
  249. package/lib/board/tone.ts +92 -0
  250. package/lib/board/view.ts +81 -63
  251. package/lib/bus-request.ts +83 -0
  252. package/{extensions/watch → lib}/child.ts +40 -21
  253. package/lib/env.ts +65 -7
  254. package/lib/format.ts +13 -0
  255. package/lib/http/index.ts +17 -11
  256. package/lib/http/mount.ts +142 -0
  257. package/lib/http/port.ts +30 -0
  258. package/lib/http/serve.ts +170 -46
  259. package/lib/http/server.ts +3 -3
  260. package/lib/lazy.ts +45 -0
  261. package/lib/plan-entry.ts +46 -21
  262. package/lib/plans/paths.ts +82 -0
  263. package/lib/policy.ts +9 -6
  264. package/lib/report.ts +3 -2
  265. package/lib/requirements.ts +10 -10
  266. package/lib/session-label.ts +129 -7
  267. package/lib/session-roster.ts +43 -1
  268. package/lib/session-tool.ts +39 -22
  269. package/lib/stray-fields.ts +7 -2
  270. package/lib/tool-names.ts +5 -10
  271. package/lib/tui/README.md +29 -11
  272. package/lib/tui/chrome.ts +16 -15
  273. package/lib/tui/cursor.ts +18 -3
  274. package/lib/tui/glyphs.ts +11 -11
  275. package/lib/tui/render-cache.ts +29 -0
  276. package/lib/tui/tokens.ts +33 -2
  277. package/lib/tui/tool.ts +33 -4
  278. package/lib/web/README.md +21 -0
  279. package/lib/web/client-runtime.ts +27 -1
  280. package/lib/web/components.ts +5 -1
  281. package/lib/web/index.ts +1 -0
  282. package/lib/web/markdown.ts +83 -0
  283. package/lib/web/markup.ts +20 -2
  284. package/lib/web/runtime.ts +9 -10
  285. package/package.json +8 -2
  286. package/skills/init/SKILL.md +5 -5
  287. package/skills/next/SKILL.md +3 -5
  288. package/extensions/board/config.ts +0 -36
  289. package/extensions/board/seat.ts +0 -28
  290. package/lib/tui/index.ts +0 -34
  291. package/skills/project-docs/SKILL.md +0 -133
  292. package/skills/project-docs/references/audit.md +0 -131
  293. package/skills/project-docs/references/changelog.md +0 -105
  294. package/skills/project-docs/references/discovery.md +0 -127
  295. package/skills/project-docs/references/readme.md +0 -80
  296. package/skills/project-docs/references/release.md +0 -84
  297. package/skills/project-docs/references/roadmap.md +0 -93
  298. package/skills/project-docs/scripts/state.ts +0 -316
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The client's renderer: snapshot in, DOM out — the toolbar, the lanes, the cards,
3
- * and the card dialog's contents.
2
+ * The client's renderer: snapshot in, DOM out — the toolbar, the lanes, the cards, and the
3
+ * card dialog's contents.
4
4
  *
5
5
  * This file runs in the browser, where no type checker sees it, so it holds to one
6
6
  * rule: **every value from the snapshot is set as `textContent`** — never as
@@ -9,36 +9,55 @@
9
9
  * them.
10
10
  *
11
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.
12
+ * when what its cards draw has moved — a revision of their own, or a blocker or a
13
+ * dependent the rest of the board decides — so the filter input, a collapsed lane, the
14
+ * scroll offset, and an open dialog all survive a peer's write.
14
15
  *
15
- * The client modules share one namespace, `window.bfBoard`: this one paints,
16
- * `team.ts` draws the round, `dialog.ts` opens a card, `keys.ts` binds, and `live.ts`
17
- * owns the frame stream and the wiring.
16
+ * The client modules share one namespace, `window.bfBoard`: this one paints the whole
17
+ * frame, `card.ts` draws one card's face, `team.ts` draws the round, `dialog.ts` fills and
18
+ * opens a card, `keys.ts` binds, and `live.ts` owns the frame stream and the wiring. The
19
+ * browser runtime (`el`, `typing`, …) is spliced in here, because a page's one global body
20
+ * is the runtime every fragment reads through `board.`; the note renderer a dialog reads
21
+ * with is spliced into that fragment instead, so nothing reaches `window` that this page
22
+ * does not place on `board.`.
18
23
  *
19
- * `plural`, `tabTitle`, `matchesQuery`, `el`, `typing`, and `paintStatus` are not written
20
- * here. Their one bodies live in `lib/` beside the rules they express, and the assembler
21
- * hands the source in — a second copy in the bundle is a second wording for `1 card`, a
22
- * second shape for the tab, a filter that finds a different card than the tool does, or an
23
- * element builder and a typing test that drift from the review page's.
24
+ * `plural`, `tabTitle`, `matchesQuery`, `reasonFor`, `el`, `typing`, and `paintStatus` are
25
+ * not written here. Their one bodies live in `lib/` beside the rules they express, and the
26
+ * assembler hands the source in — a second copy in the bundle is a second wording for
27
+ * `1 card`, a second shape for the tab, a filter that finds a different card than the tool
28
+ * does, a second reason for the same lane, or an element builder and a typing test that
29
+ * drift from the review page's.
24
30
  */
25
31
 
26
32
  /**
27
- * The renderer, with what it needs spliced in: the counts' wording, the tab title's
28
- * shape, what a query matches, and the browser runtime's element builder and typing test —
29
- * each written once in `lib/` and handed in as source.
33
+ * The renderer, with what it needs spliced in: the counts' wording, the tab title's shape,
34
+ * what a query matches, why a card sits where it does, how many placeholders the skeleton
35
+ * paints, and the browser runtime's element builder and typing test — each written once, in
36
+ * `lib/`, and handed in as source.
30
37
  */
31
38
  export function renderJs(rules: {
32
39
  plural: string;
33
40
  tabTitle: string;
34
41
  matches: string;
42
+ reason: string;
35
43
  age: string;
44
+ lanes: string;
45
+ skipped: string;
46
+ decisions: string;
47
+ decisionTones: string;
48
+ reasonTones: string;
36
49
  runtime: string;
37
50
  }): string {
38
51
  return renderTemplate
39
52
  .replace("__PLURAL__", rules.plural)
53
+ .replace("__SKIPPED__", rules.skipped)
40
54
  .replace("__TAB_TITLE__", rules.tabTitle)
41
55
  .replace("__MATCHES__", rules.matches)
56
+ .replace("__REASON__", rules.reason)
57
+ .replace("__LANE_COUNT__", rules.lanes)
58
+ .replace("__DECISION_RANK__", rules.decisions)
59
+ .replace("__DECISION_TONES__", rules.decisionTones)
60
+ .replace("__REASON_TONES__", rules.reasonTones)
42
61
  .replace("__AGE__", rules.age)
43
62
  .replace("__RUNTIME__", rules.runtime);
44
63
  }
@@ -47,34 +66,64 @@ const renderTemplate = String.raw`(function () {
47
66
  "use strict";
48
67
  var board = window.bfBoard || (window.bfBoard = {});
49
68
 
50
- /** Lanes in the order the server sends them, with the words a reader expects. */
51
- var LANE_LABEL = { todo: "To do", doing: "Doing", blocked: "Blocked", done: "Done" };
52
- var LANE_EMPTY = { todo: "nothing waiting", doing: "nothing in flight", blocked: "nothing blocked", done: "nothing finished yet" };
53
- /** Path chips a lane card shows before it counts the rest. */
54
- var PATH_CHIPS = 3;
55
69
  /**
56
- * Finished cards a lane may hold before it starts folded.
70
+ * The page holds no lane table: a lane's words, colour, limit, and count arrive with it
71
+ * from the snapshot, so a head, a picker button, and the transcript cannot name one lane
72
+ * two ways — the words are decided once, in lib/board/lanes.ts.
73
+ */
74
+ /** How many placeholder trays the skeleton paints: the lane table's own length, spliced in. */
75
+ var LANE_COUNT = __LANE_COUNT__;
76
+ /**
77
+ * How many rows a list may show before it starts folded, by list — one table, because two
78
+ * lists are the same pile and a second threshold is a second answer to "how much is history".
57
79
  *
58
80
  * A board of 26 cards is 19 of them finished, and an open Done lane is four times the
59
- * height of the three that carry work. Three or four is work a reader still checks;
60
- * past this it is history, and the count in the head says how much.
81
+ * height of the three that carry work: three or four is work a reader still checks, past
82
+ * six it is history, and the count says how much. The roster has the same problem for the
83
+ * same reason — a label that ever claimed a card stays an owner for good, so a long
84
+ * history is a wall of no-record rows — and three is where it stops being a team.
61
85
  */
62
- var DONE_FOLD_AT = 6;
63
- /** The two tones a reason line can take, by the kind reasonOf reports. */
64
- var REASON_TONE = { blocked: N.cardReasonBlocked, unplanned: N.cardReasonUnplanned };
86
+ var FOLD_AT = { done: 6, roster: 3 };
87
+
88
+ /** The same table, for the fragments painted after this one: team folds two lists. */
89
+ board.foldAt = FOLD_AT;
65
90
 
66
91
  __RUNTIME__
67
92
 
93
+ /**
94
+ * The queue's order, its colours, and the tone a reason line wears — spliced in from
95
+ * lib/board/tone.ts, which the terminal reads through extensions/board/tone.ts. One
96
+ * table, so a row cannot be the second-strongest reason and the quietest colour.
97
+ */
98
+ var DECISION_RANK = __DECISION_RANK__;
99
+ var DECISION_TONE = __DECISION_TONES__;
100
+ var REASON_TONE = __REASON_TONES__;
101
+
102
+ /** Where a kind sits in the queue: the order the table states, not a copy. */
103
+ function rankOf(kind) {
104
+ return DECISION_RANK.indexOf(kind);
105
+ }
106
+
107
+ /** The colour a decision row is drawn in — its edge and its reason line. */
108
+ function decisionTone(kind) {
109
+ return DECISION_TONE[kind] || "dim";
110
+ }
111
+
112
+ /** The colour a card's reason line is drawn in, by the kind the rule reported. */
113
+ function reasonTone(kind) {
114
+ return REASON_TONE[kind] || "muted";
115
+ }
116
+
117
+
68
118
  /**
69
119
  * One button that calls back, so nothing on the page is a click handler in markup.
70
120
  *
71
- * 'plain' is the bordered kit button, 'ghost' the text one: a card's primary
72
- * action (Claim, Move) is bordered and the rest are text, which is what makes one
73
- * of three controls on a card the obvious one.
121
+ * The class comes from the caller: a card's title and a lane's head are buttons drawn
122
+ * by the page's own rule, not by the kit's chip-with-border look — and a card draws no
123
+ * action button at all, since claiming, moving, and editing are the tools' work.
74
124
  */
75
- function actionButton(label, onClick, variant) {
76
- var kind = variant === "plain" ? "" : variant ? " bf-btn--" + variant : " bf-btn--ghost";
77
- var node = el("button", "bf-btn" + kind, label);
125
+ function actionButton(className, label, onClick) {
126
+ var node = el("button", className, label);
78
127
  node.type = "button";
79
128
  node.addEventListener("click", onClick);
80
129
  return node;
@@ -115,99 +164,6 @@ __RUNTIME__
115
164
  return undefined;
116
165
  }
117
166
 
118
- /** The one line under a card's meta: why it sits where it does. */
119
- function reasonOf(card, view) {
120
- var last = card.notes.length > 0 ? card.notes[card.notes.length - 1] : undefined;
121
- // An order is the strongest reason a card is not being worked on, and the one a
122
- // reader cannot see from the lane: it comes before the note that says the same.
123
- // The sentence is the snapshot's, so this line and the tool's refusal name one list.
124
- if (card.waits !== undefined) return { text: "waits on " + card.waits, kind: "waiting" };
125
- if (card.status === "blocked") {
126
- // The lane already says blocked and the tone already says danger; the line says why.
127
- // The recorded reason comes first: the note is what somebody happened to write last,
128
- // and it is the reason a move to blocked asks for. The slot carries the reason alone —
129
- // the lane head already says blocked, and a status prefix in it would be a second voice.
130
- if (card.blockedBy) return { text: card.blockedBy, kind: "blocked" };
131
- return { text: last ? last.text : "blocked, with nothing recorded", kind: "blocked" };
132
- }
133
- // An offer is a decision the lead already made once: the lane says nobody holds it, and
134
- // this says how long the broadcast has gone unanswered, which is the difference between
135
- // nobody having looked and nobody taking it.
136
- if (card.status === "todo" && card.offeredAt !== undefined) {
137
- return { text: "offered " + ago(card.offeredAt) + " ago · nobody took it", kind: "waiting" };
138
- }
139
- if (card.status === "doing" && !card.plan) {
140
- return { text: "no plan recorded", kind: "unplanned" };
141
- }
142
- if (card.status === "doing") return { text: "planned", kind: "plan" };
143
- if (card.status === "done") {
144
- var landed = card.landed ? " · landed " + card.landed.commit : "";
145
- return { text: "✓ done" + (card.owner ? " by " + card.owner.label : "") + landed, kind: "done" };
146
- }
147
- return last ? { text: last.text, kind: "note" } : undefined;
148
- }
149
-
150
- /** A path hint, or the +N chip that points at the rest. */
151
- function paths(card, limit) {
152
- var list = el("ul", N.cardPaths);
153
- var shown = card.paths.slice(0, limit);
154
- for (var i = 0; i < shown.length; i++) {
155
- var item = el("li", "");
156
- item.appendChild(chip(shown[i]));
157
- list.appendChild(item);
158
- }
159
- if (card.paths.length > limit) {
160
- var rest = el("li", "");
161
- rest.appendChild(chip("+" + (card.paths.length - limit)));
162
- list.appendChild(rest);
163
- }
164
- return list;
165
- }
166
-
167
- /** One card in its lane. */
168
- function cardNode(card, view) {
169
- var item = el("li", N.card);
170
- item.setAttribute("data-card", card.id);
171
- item.setAttribute("data-status", card.status);
172
- item.setAttribute("data-rev", String(card.rev));
173
-
174
- var head = el("p", N.cardHead);
175
- head.appendChild(el("span", N.cardId, "#" + card.id));
176
- var title = actionButton(card.title, function () {
177
- board.openCard(card.id);
178
- });
179
- title.className = N.cardTitle;
180
- head.appendChild(title);
181
- item.appendChild(head);
182
-
183
- var meta = el("p", N.cardMeta);
184
- meta.appendChild(card.owner ? chip(card.owner.label, "accent") : el("span", "", "unassigned"));
185
- // The order inside a lane is the priority, so an urgent card says so: an order
186
- // nobody can see reads as an accident. The lane's colour stays the card's own.
187
- if (card.priority !== "normal") meta.appendChild(chip(card.priority));
188
- // The stamp, so the clock can keep the age honest between frames: a board can be
189
- // quiet for ten minutes while the page still says "live".
190
- var age = el("span", "", ago(card.updatedAt));
191
- age.setAttribute("data-at", String(card.updatedAt));
192
- meta.appendChild(age);
193
- item.appendChild(meta);
194
-
195
- if (card.paths.length > 0) item.appendChild(paths(card, PATH_CHIPS));
196
-
197
- var reason = reasonOf(card, view);
198
- if (reason) {
199
- // The tone is a lookup, not a suffix: a name assembled from a prefix is a name no
200
- // test can compare with the stylesheet.
201
- var tone = REASON_TONE[reason.kind];
202
- var line = el("p", tone === undefined ? N.cardReason : N.cardReason + " " + tone, reason.text);
203
- // It clamps to one line on the card, so the full sentence is the tooltip.
204
- line.title = reason.text;
205
- item.appendChild(line);
206
- }
207
-
208
- return item;
209
- }
210
-
211
167
  /**
212
168
  * The cards as the page draws them.
213
169
  *
@@ -226,40 +182,55 @@ __RUNTIME__
226
182
  return seen;
227
183
  }
228
184
 
229
- /** The toolbar's identity, progress, footer, and the tab's own name. */
185
+ /** The toolbar's subject, its progress, the footer, and the tab's own name. */
230
186
  function renderChrome(view) {
231
- var me = document.getElementById("board-me");
232
- if (me) {
233
- me.textContent = view.me.label;
234
- me.title = "This page is served as " + view.me.label + ", and it cannot change a card";
187
+ // The board's subject, which the header carries: the repository's name, with the root's
188
+ // whole path on the hover, because a name alone cannot tell two clones of one repository
189
+ // apart. It arrives in the document too, so the toolbar is complete before the first
190
+ // frame; this keeps it true if a frame ever names a different store.
191
+ var repo = document.getElementById("board-repo");
192
+ if (repo) {
193
+ repo.textContent = view.repo;
194
+ repo.title = view.root;
235
195
  }
236
196
  var progress = document.getElementById("board-progress");
197
+ var done = laneOf(view, "done");
198
+ var finished = done === undefined ? 0 : done.cards.length;
237
199
  if (progress) {
238
- var total = view.counts.total;
200
+ var total = view.total;
239
201
  // One or two cards is not a ratio: an empty bar beside "0/1 done" is noise.
240
202
  progress.hidden = total < 3;
241
203
  var fill = document.getElementById("board-progress-fill");
242
- if (fill) fill.style.width = total === 0 ? "0%" : Math.round((view.counts.done / total) * 100) + "%";
204
+ if (fill) fill.style.width = total === 0 ? "0%" : Math.round((finished / total) * 100) + "%";
243
205
  var text = document.getElementById("board-progress-text");
244
- if (text) text.textContent = view.counts.done + "/" + total + " done";
206
+ if (text) text.textContent = finished + "/" + total + " done";
245
207
  }
246
208
  var where = document.getElementById("board-where");
247
209
  if (where) {
248
- // A footer that only counts read cards would say "2 cards" over a board holding
249
- // three files, so the unreadable ones are named beside the size.
210
+ // The footer adds only what the header does not: which store these cards are filed
211
+ // under, how many, and what they weigh. A footer that only counted read cards would
212
+ // say "2 cards" over a directory holding three files, so the refused ones are counted
213
+ // beside the size and named with the store's own reason on the hover — the line has no
214
+ // room for a file and a reason each, and a count alone is a fact a reader cannot act on.
250
215
  where.textContent =
251
- "store " + view.where + " · " + plural(view.counts.total, "card") + " · " + size(view.bytes) +
252
- (view.skipped > 0 ? " · " + plural(view.skipped, "unreadable") : "");
216
+ "store " + view.where + " · " + plural(view.total, "card") + " · " + size(view.bytes) +
217
+ (view.skipped.length > 0 ? " · " + plural(view.skipped.length, "card") + " could not be read" : "");
218
+ if (view.skipped.length > 0) where.title = skippedText(view.skipped);
219
+ else where.removeAttribute("title");
253
220
  }
254
- // The tab has to tell two boards apart, and a board's own name is the last segment of
255
- // its store: one board per repository, so that key is the subject. The count is what
256
- // a browser full of tabs is read for, and an empty board says nothing rather than 0.
257
- var open = view.counts.total - view.counts.done;
258
- document.title = tabTitle("board", view.where.split("/").pop(), open > 0 ? open + " open" : undefined);
221
+ // The tab has to tell two boards apart, and a board's subject is its repository: one
222
+ // board per repository, named the way the directory holding it is. The store key is a
223
+ // hash, which no reader can say out loud, so it is not what a tab leads with. The count
224
+ // is what a browser full of tabs is read for, and an empty board says nothing rather
225
+ // than 0.
226
+ var open = view.total - finished;
227
+ document.title = tabTitle("board", view.repo, open > 0 ? open + " open" : undefined);
259
228
  }
260
229
 
261
230
  /** 1 card, 2 cards — the one body, handed in by the bundle's assembler. */
262
231
  var plural = __PLURAL__;
232
+ /** The refused files, named with their reasons: the tool's own sentence, handed in. */
233
+ var skippedText = __SKIPPED__;
263
234
  /** board · my-pi-agent · 3 open, the tab's one shape, handed in the same way. */
264
235
  var tabTitle = __TAB_TITLE__;
265
236
 
@@ -270,44 +241,90 @@ __RUNTIME__
270
241
  * writing to the cards would slowly lie about how long they have sat there. The
271
242
  * ticker calls this; it touches nothing else, so a filter, a folded lane, or an
272
243
  * open dialog survives it.
244
+ *
245
+ * A stamp carries its own wording ("updated {age}", "last note {age} by …"), because a
246
+ * card now shows two ages and a ticker that assumed one of them would relabel the other.
273
247
  */
274
248
  function refreshAges() {
275
249
  var nodes = document.querySelectorAll("[data-at]");
276
250
  for (var i = 0; i < nodes.length; i++) {
277
251
  var next = ago(Number(nodes[i].getAttribute("data-at")));
278
252
  if (nodes[i].textContent !== next) nodes[i].textContent = next;
253
+ var wording = nodes[i].getAttribute("data-at-title");
254
+ if (wording !== null) nodes[i].title = board.sentence(wording, next);
279
255
  }
280
256
  }
281
257
 
282
258
  /** Whether the filter keeps a card: the module's one predicate, spliced in as source. */
283
259
  var matches = __MATCHES__;
284
260
 
285
- /** The lane section for one status, created once and then reused. */
261
+ /** Why a card sits where it does: one body, spliced in from lib/board/reason.ts. */
262
+ var reasonFor = __REASON__;
263
+
264
+ /** One lane from the snapshot, by its name; absent when the snapshot no longer has it. */
265
+ function laneOf(view, key) {
266
+ for (var i = 0; i < view.lanes.length; i++) if (view.lanes[i].key === key) return view.lanes[i];
267
+ return undefined;
268
+ }
269
+
270
+ /** A lane head's count: n/limit while the lane is pulled, and n otherwise. */
271
+ function headCount(lane) {
272
+ return lane.limit === undefined ? String(lane.count) : lane.count + "/" + lane.limit;
273
+ }
274
+
275
+ /**
276
+ * The same count, in words a screen reader can read.
277
+ *
278
+ * The pill prints "1/3" because an eye takes the slash in at a glance; spoken, it is
279
+ * punctuation between two numbers. The heading therefore names itself from the same
280
+ * facts -- "Plan, 1 of 3" -- and the pill keeps the shape.
281
+ */
282
+ function headLabel(lane, shown) {
283
+ if (shown !== undefined) return lane.label + ", " + shown + " of " + lane.cards.length;
284
+ if (lane.limit !== undefined) return lane.label + ", " + lane.count + " of " + lane.limit;
285
+ return lane.label + ", " + plural(lane.count, "card");
286
+ }
287
+
288
+ /** The lane section for one lane, created once and then reused. */
286
289
  function laneNode(lane) {
287
- var section = document.querySelector('.board-lane[data-status="' + lane.status + '"]');
290
+ var section = document.querySelector('.board-lane[data-lane="' + lane.key + '"]');
288
291
  if (section) return section;
289
292
  section = el("section", N.lane);
290
- section.setAttribute("data-status", lane.status);
293
+ section.setAttribute("data-lane", lane.key);
291
294
  var head = el("h2", N.laneHead);
292
- var toggle = actionButton(LANE_LABEL[lane.status] || lane.status, function () {
293
- board.toggleLane(lane.status);
295
+ var toggle = actionButton(N.laneToggle, lane.label, function () {
296
+ board.toggleFold(lane.key);
294
297
  });
295
- toggle.className = N.laneToggle;
298
+ toggle.setAttribute("data-fold", lane.key);
296
299
  toggle.setAttribute("aria-expanded", "true");
297
- toggle.setAttribute("aria-controls", "lane-" + lane.status);
298
- toggle.id = "lane-head-" + lane.status;
300
+ toggle.setAttribute("aria-controls", "lane-" + lane.key);
301
+ toggle.id = "lane-head-" + lane.key;
299
302
  head.appendChild(el("span", N.laneDot));
300
303
  head.appendChild(toggle);
301
304
  head.appendChild(el("span", N.laneCount));
302
305
  section.appendChild(head);
303
306
  var list = el("ol", N.laneList);
304
- list.id = "lane-" + lane.status;
307
+ list.id = "lane-" + lane.key;
305
308
  section.appendChild(list);
306
- section.appendChild(el("p", N.laneEmpty, LANE_EMPTY[lane.status] || "nothing"));
309
+ section.appendChild(el("p", N.laneEmpty, lane.empty));
307
310
  return section;
308
311
  }
309
312
 
310
- /** Draw the lanes, rebuilding only one whose cards actually moved. */
313
+ /**
314
+ * What one face is drawn from: the card's own revision, and the two facts about it that
315
+ * the rest of the board decides.
316
+ *
317
+ * A face prints what the card waits for and what it holds up, both derived from other
318
+ * cards, so a blocker finished in another lane moves nothing in this card's record — and
319
+ * a lane keyed on revisions alone kept a sentence the board had already left behind. The
320
+ * Team tab's queue carries its rows' reasons the same way: a signature lists the facts a
321
+ * row prints, not just the row's own file.
322
+ */
323
+ function faceSig(card) {
324
+ return card.id + ":" + card.rev + "|" + (card.waits || "") + "|" + (card.dependents || "");
325
+ }
326
+
327
+ /** Draw the lanes, rebuilding only one whose faces actually moved. */
311
328
  function renderLanes(view) {
312
329
  var lanes = document.getElementById("board-lanes");
313
330
  if (!lanes) return;
@@ -317,9 +334,7 @@ __RUNTIME__
317
334
  lanes.removeAttribute("data-skeleton");
318
335
  }
319
336
  var query = board.query || "";
320
- var empty = document.getElementById("board-no-cards");
321
- if (empty) empty.hidden = view.counts.total !== 0;
322
- lanes.hidden = view.counts.total === 0;
337
+ var shownTotal = 0;
323
338
  for (var i = 0; i < view.lanes.length; i++) {
324
339
  var lane = view.lanes[i];
325
340
  var section = laneNode(lane);
@@ -327,9 +342,9 @@ __RUNTIME__
327
342
  // Finished work starts folded, once: the default is what the page opens with, and
328
343
  // toggleLane writes a boolean from then on, so a reader who unfolds Done keeps it
329
344
  // unfolded through every frame a peer's write causes.
330
- if (lane.status === "done" && lane.cards.length >= DONE_FOLD_AT) {
345
+ if (lane.key === "done" && lane.cards.length >= FOLD_AT.done) {
331
346
  board.collapsed = board.collapsed || {};
332
- if (board.collapsed[lane.status] === undefined) board.collapsed[lane.status] = true;
347
+ if (board.collapsed[lane.key] === undefined) board.collapsed[lane.key] = true;
333
348
  }
334
349
  // A lane holding nothing is a rail: its dot, its word, and its count, in the width
335
350
  // the fact deserves. The filter never does this — filtering is a view of the cards,
@@ -340,25 +355,29 @@ __RUNTIME__
340
355
  // for want of a column the lane is not using. A count, never text.
341
356
  section.style.setProperty("--lane-count", String(lane.cards.length));
342
357
  var shown = lane.cards.filter(function (card) { return matches(card, query); });
343
- var sig = lane.cards.map(function (card) { return card.id + ":" + card.rev; }).join(",") + "|" + query;
358
+ shownTotal += shown.length;
359
+ var sig = lane.cards.map(faceSig).join(",") + "|" + query;
344
360
  if (section.getAttribute("data-sig") !== sig) {
345
361
  section.setAttribute("data-sig", sig);
346
362
  var list = section.querySelector(".board-lane__list");
347
363
  list.textContent = "";
348
- for (var j = 0; j < shown.length; j++) list.appendChild(cardNode(shown[j], view));
364
+ for (var j = 0; j < shown.length; j++) list.appendChild(board.cardNode(shown[j]));
349
365
  }
350
366
  var count = section.querySelector(".board-lane__count");
367
+ var head = section.querySelector(".board-lane__head");
351
368
  if (count) {
352
- count.textContent = query === "" || shown.length === lane.cards.length
353
- ? String(lane.cards.length)
354
- : shown.length + "/" + lane.cards.length;
369
+ var filtered = query !== "" && shown.length !== lane.cards.length;
370
+ count.textContent = filtered ? shown.length + "/" + lane.cards.length : headCount(lane);
371
+ // The visible pill and the spoken name come from the same numbers, so a filtered lane
372
+ // says "1 of 2" rather than reading as a board of one card.
373
+ if (head) head.setAttribute("aria-label", headLabel(lane, filtered ? shown.length : undefined));
355
374
  }
356
375
  var none = section.querySelector(".board-lane__empty");
357
376
  if (none) {
358
377
  none.hidden = shown.length !== 0;
359
- none.textContent = query !== "" && lane.cards.length > 0 ? "no match" : LANE_EMPTY[lane.status] || "nothing";
378
+ none.textContent = query !== "" && lane.cards.length > 0 ? "no match" : lane.empty;
360
379
  }
361
- var collapsed = board.collapsed && board.collapsed[lane.status] === true;
380
+ var collapsed = board.collapsed && board.collapsed[lane.key] === true;
362
381
  var list = section.querySelector(".board-lane__list");
363
382
  if (list) list.hidden = collapsed;
364
383
  // A folded lane shows nothing, so it takes the rail's width the way an empty one
@@ -368,6 +387,7 @@ __RUNTIME__
368
387
  var toggle = section.querySelector(".board-lane__toggle");
369
388
  if (toggle) toggle.setAttribute("aria-expanded", String(!collapsed));
370
389
  }
390
+ board.renderEmpty(view, query, shownTotal);
371
391
  renderPicker(view);
372
392
  }
373
393
 
@@ -378,118 +398,33 @@ __RUNTIME__
378
398
  picker.textContent = "";
379
399
  for (var i = 0; i < view.lanes.length; i++) {
380
400
  (function (lane) {
381
- var item = el("button", "bf-segmented__item", (LANE_LABEL[lane.status] || lane.status) + " " + lane.cards.length);
401
+ // A lane switch, not a tablist: it shows one lane and there is no tabpanel behind
402
+ // each button, so the pressed state is the honest one rather than a tab without its
403
+ // panel. The counts stay on the face, where a reader chooses by them.
404
+ var item = el("button", "bf-segmented__item", lane.label + " " + headCount(lane));
382
405
  item.type = "button";
383
- item.setAttribute("role", "tab");
384
- item.setAttribute("data-lane", lane.status);
385
- item.addEventListener("click", function () { board.showLane(lane.status); });
406
+ item.setAttribute("data-picker-lane", lane.key);
407
+ item.addEventListener("click", function () { board.showLane(lane.key); });
386
408
  picker.appendChild(item);
387
409
  })(view.lanes[i]);
388
410
  }
389
411
  board.applyLane();
390
412
  }
391
413
 
392
- /** Fill the card dialog's contents from a card, without opening or closing it. */
393
- function fillDialog(card, view) {
394
- document.getElementById("card-dialog-title").textContent = card.title;
395
- document.getElementById("card-dialog-id").textContent = "#" + card.id;
396
- // The lane is a status of its own, not a word inside a sentence: the same dot and
397
- // label a lane head wears, so the dialog and the board speak the same vocabulary.
398
- var state = document.getElementById("card-dialog-state");
399
- state.setAttribute("data-status", card.status);
400
- state.textContent = card.status;
401
-
402
- // One fact per row of the ladder, so a reader can pick out a value instead of
403
- // hunting for it in a sentence. A row with nothing to say takes its label with it:
404
- // an empty cell under a heading reads as a missing value, not as an absence.
405
- var pair = function (name, shown) {
406
- var dt = document.getElementById("card-dialog-" + name + "-label");
407
- var dd = document.getElementById("card-dialog-" + name);
408
- if (dt) dt.hidden = !shown;
409
- if (dd) dd.hidden = !shown;
410
- return dd;
411
- };
412
- var setRow = function (name, shown, value, full) {
413
- var dd = pair(name, shown);
414
- dd.textContent = shown ? value : "";
415
- if (full === undefined) dd.removeAttribute("title");
416
- else dd.setAttribute("title", full);
417
- };
418
-
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
- pair("owner", true);
423
-
424
- var priority = pair("priority", card.priority !== "normal");
425
- priority.textContent = "";
426
- if (card.priority !== "normal") priority.appendChild(chip(card.priority));
427
-
428
- setRow("updated", true, ago(card.updatedAt));
429
- // How long the card has been in the lane it is in now, which updatedAt cannot say:
430
- // a note filed against a blocked card moves that stamp without moving the card. Absent
431
- // for a card written before the log existed, so the row takes its label with it.
432
- setRow("lane", card.since !== undefined, card.status + " · " + ago(card.since));
433
- setRow("by", true, card.by);
434
- setRow("landed", card.landed !== undefined, card.landed ? card.landed.commit : "", card.landed ? card.landed.commit : undefined);
435
-
436
- // The dialog and the card face say one thing, from one field: the ids it was linked
437
- // to are the storage, and a reader is owed the lanes that make them a decision.
438
- setRow("depends", card.waits !== undefined, card.waits || "", card.waits);
439
- // The other side of the same link: what leaving this card undone is holding up, which
440
- // is the one thing a card's own row cannot say.
441
- setRow("dependents", card.dependents !== undefined, card.dependents || "", card.dependents);
442
-
443
- // A long plan path elides on one line, and the whole of it is on the hover title and
444
- // in the text, so it stays copyable: the reason to open a card is often its plan.
445
- setRow("plan", Boolean(card.plan), card.plan || "", card.plan || undefined);
446
-
447
- pair("paths", card.paths.length > 0);
448
- var pathList = document.getElementById("card-dialog-path-list");
449
- pathList.textContent = "";
450
- for (var i = 0; i < card.paths.length; i++) {
451
- var item = el("li", "");
452
- var hint = chip(card.paths[i]);
453
- hint.className += " " + N.dialogPath;
454
- hint.title = card.paths[i];
455
- item.appendChild(hint);
456
- pathList.appendChild(item);
457
- }
458
-
459
- var notes = document.getElementById("card-dialog-notes");
460
- notes.textContent = "";
461
- for (var j = 0; j < card.notes.length; j++) {
462
- var note = card.notes[j];
463
- // The prose is the record and reads at body size; who wrote it and when is its
464
- // footnote, so it follows the sentence instead of standing over it.
465
- var row = el("li", N.dialogNote);
466
- row.appendChild(el("p", N.dialogNoteText, note.text));
467
- var foot = el("p", N.dialogNoteFoot);
468
- foot.appendChild(el("span", N.dialogNoteBy, note.by));
469
- foot.appendChild(el("span", N.dialogNoteAt, ago(note.at)));
470
- row.appendChild(foot);
471
- notes.appendChild(row);
472
- }
473
- var count = document.getElementById("card-dialog-note-count");
474
- if (count) count.textContent = card.notes.length === 0 ? " · none" : " · " + card.notes.length;
475
- var earlier = document.getElementById("card-dialog-earlier");
476
- earlier.hidden = card.notesDropped === 0;
477
- earlier.textContent = card.notesDropped === 0 ? "" : card.notesDropped + " earlier notes dropped";
478
-
479
- // A modal whose title changes is not announced, so walking with j/k says where it
480
- // landed: the position is counted over the cards as they are drawn, not as stored, so
481
- // a filter or a folded lane cannot make the number disagree with the screen.
482
- var where = document.getElementById("card-dialog-where");
483
- if (where) {
484
- var drawn = drawnCards();
485
- var at = 0;
486
- for (var k = 0; k < drawn.length; k++) {
487
- if (drawn[k].getAttribute("data-card") === String(card.id)) at = k + 1;
488
- }
489
- // Opened from the Team tab there is no drawn board to count along, so the line
490
- // names the card instead of claiming a position it cannot know.
491
- where.textContent = drawn.length === 0 ? card.title : "card " + at + " of " + drawn.length + ": " + card.title;
492
- }
414
+ /**
415
+ * Put focus back on the control a reader just moved.
416
+ *
417
+ * A fold repaints the panel it lives in, so the element that was clicked is gone by the
418
+ * time the frame lands and focus would fall to the document — a reader who folds with the
419
+ * keyboard would then start again from the top of the page. toggleFold records the key
420
+ * and each control names itself with it, so this is the whole lookup.
421
+ */
422
+ function restoreFoldFocus() {
423
+ var key = board.foldFocus;
424
+ if (key === undefined) return;
425
+ board.foldFocus = undefined;
426
+ var control = document.querySelector('[data-fold="' + key + '"]');
427
+ if (control) control.focus();
493
428
  }
494
429
 
495
430
  /** Paint a whole snapshot. */
@@ -498,22 +433,29 @@ __RUNTIME__
498
433
  renderChrome(view);
499
434
  renderLanes(view);
500
435
  board.renderTeam(view);
501
- if (board.openId !== undefined) {
502
- var card = findCard(view, board.openId);
503
- if (card) fillDialog(card, view);
504
- else board.closeCard();
505
- }
436
+ // The dialog module keeps an open record current — or closes it when its card left the
437
+ // frame — so the panel and the board cannot disagree about one card.
438
+ board.syncDialog(view);
439
+ // The marks are painted after the frame, not with each card: a lane is rebuilt whole
440
+ // when its cards move, and a reader's place must survive that.
441
+ board.markCards();
442
+ // Last, because the control it looks for is one this frame has just built.
443
+ restoreFoldFocus();
506
444
  };
507
445
 
508
446
  board.el = el;
509
447
  board.typing = typing;
510
448
  board.chip = chip;
511
- board.actionButton = actionButton;
512
449
  board.ago = ago;
513
450
  board.findCard = findCard;
514
451
  board.drawnCards = drawnCards;
515
452
  board.matches = matches;
516
- board.fillDialog = fillDialog;
453
+ board.reasonFor = reasonFor;
454
+ board.rankOf = rankOf;
455
+ board.decisionTone = decisionTone;
456
+ board.reasonTone = reasonTone;
457
+ board.laneCount = LANE_COUNT;
458
+ board.laneOf = laneOf;
517
459
  board.refreshAges = refreshAges;
518
460
  board.plural = plural;
519
461
  })();`;