@gavin-hu/my-pi-agent 0.21.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 (254) hide show
  1. package/README.md +46 -31
  2. package/README.zh-CN.md +40 -26
  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 -24
  9. package/docs/extensions/board.md +361 -147
  10. package/docs/extensions/bytefly.md +152 -111
  11. package/docs/extensions/check.md +15 -17
  12. package/docs/extensions/context.md +35 -15
  13. package/docs/extensions/file-browse.md +30 -31
  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 +166 -126
  20. package/docs/extensions/question.md +16 -18
  21. package/docs/extensions/rewind.md +17 -19
  22. package/docs/extensions/serve.md +34 -18
  23. package/docs/extensions/session-bus.md +38 -16
  24. package/docs/extensions/subagent.md +23 -25
  25. package/docs/extensions/team.md +72 -33
  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 +56 -15
  32. package/extensions/board/format.ts +43 -24
  33. package/extensions/board/index.ts +5 -4
  34. package/extensions/board/input.ts +4 -9
  35. package/extensions/board/join.ts +4 -40
  36. package/extensions/board/lazy-page.ts +40 -0
  37. package/extensions/board/page.ts +13 -11
  38. package/extensions/board/result.ts +30 -16
  39. package/extensions/board/rows.ts +62 -25
  40. package/extensions/board/schema.ts +42 -10
  41. package/extensions/board/snapshot.ts +2 -1
  42. package/extensions/board/tone.ts +22 -0
  43. package/extensions/board/tools.ts +131 -44
  44. package/extensions/board/types.ts +36 -32
  45. package/extensions/board/view/card.ts +229 -0
  46. package/extensions/board/view/client.ts +33 -15
  47. package/extensions/board/view/dialog.ts +169 -6
  48. package/extensions/board/view/empty.ts +47 -0
  49. package/extensions/board/view/keys.ts +13 -2
  50. package/extensions/board/view/live.ts +89 -21
  51. package/extensions/board/view/names.ts +13 -4
  52. package/extensions/board/view/page.ts +23 -9
  53. package/extensions/board/view/render.ts +208 -271
  54. package/extensions/board/view/styles.ts +123 -31
  55. package/extensions/board/view/team.ts +297 -76
  56. package/extensions/bytefly/block/fallback.ts +1 -1
  57. package/extensions/bytefly/doctor/runtime.ts +5 -2
  58. package/extensions/bytefly/footer/collect/label.ts +60 -0
  59. package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
  60. package/extensions/bytefly/footer/config.ts +4 -2
  61. package/extensions/bytefly/footer/register.ts +33 -4
  62. package/extensions/bytefly/footer/render/lines.ts +7 -4
  63. package/extensions/bytefly/footer/render/segments.ts +37 -10
  64. package/extensions/bytefly/footer/render/status.ts +0 -11
  65. package/extensions/bytefly/footer/types.ts +7 -0
  66. package/extensions/bytefly/header/config.ts +1 -1
  67. package/extensions/bytefly/index.ts +32 -6
  68. package/extensions/bytefly/mode/editor.ts +115 -0
  69. package/extensions/bytefly/panel/commands.ts +210 -120
  70. package/extensions/bytefly/panel/format.ts +4 -4
  71. package/extensions/bytefly/panel/settings.ts +73 -6
  72. package/extensions/bytefly/panel/tui/keys.ts +2 -6
  73. package/extensions/bytefly/update/cache.ts +1 -1
  74. package/extensions/bytefly/update/register.ts +4 -4
  75. package/extensions/bytefly/working/config.ts +5 -7
  76. package/extensions/bytefly/working/format.ts +10 -13
  77. package/extensions/bytefly/working/register.ts +22 -27
  78. package/extensions/check/commands.ts +3 -2
  79. package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
  80. package/extensions/context/commands.ts +2 -2
  81. package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
  82. package/extensions/file-browse/index.ts +67 -36
  83. package/extensions/file-read/render.ts +4 -4
  84. package/extensions/goal/plan-route.ts +2 -2
  85. package/extensions/job/child.ts +527 -0
  86. package/extensions/job/clock.ts +142 -0
  87. package/extensions/job/commands.ts +4 -2
  88. package/extensions/job/config.ts +8 -1
  89. package/extensions/job/format.ts +22 -1
  90. package/extensions/job/log-writer.ts +13 -2
  91. package/extensions/job/registry.ts +63 -7
  92. package/extensions/job/render.ts +34 -0
  93. package/extensions/job/runtime.ts +296 -655
  94. package/extensions/job/session.ts +132 -19
  95. package/extensions/job/store.ts +7 -7
  96. package/extensions/job/tools.ts +3 -3
  97. package/extensions/job/types.ts +47 -0
  98. package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
  99. package/extensions/job/waiters.ts +44 -1
  100. package/extensions/memory/commands.ts +4 -2
  101. package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
  102. package/extensions/plan/commands.ts +12 -9
  103. package/extensions/plan/format.ts +8 -2
  104. package/extensions/plan/git-bash.ts +115 -16
  105. package/extensions/plan/index.ts +53 -52
  106. package/extensions/plan/plan-index.ts +4 -2
  107. package/extensions/plan/plans.ts +5 -54
  108. package/extensions/plan/policy.ts +8 -10
  109. package/extensions/plan/review-lazy.ts +85 -0
  110. package/extensions/plan/review.ts +2 -2
  111. package/extensions/plan/runtime.ts +49 -39
  112. package/extensions/plan/sections.ts +1 -1
  113. package/extensions/plan/steps.ts +3 -3
  114. package/extensions/plan/todos.ts +1 -1
  115. package/extensions/plan/tools/cancel.ts +27 -62
  116. package/extensions/plan/tools/enter.ts +55 -81
  117. package/extensions/plan/tools/exit.ts +221 -238
  118. package/extensions/plan/tools/refusal.ts +18 -0
  119. package/extensions/plan/tools/steps.ts +75 -92
  120. package/extensions/plan/tools/write.ts +77 -120
  121. package/extensions/plan/tools.ts +188 -30
  122. package/extensions/plan/tui.ts +5 -5
  123. package/extensions/plan/types.ts +48 -5
  124. package/extensions/plan/web-client.ts +3 -0
  125. package/extensions/plan/web-lazy.ts +53 -0
  126. package/extensions/plan/web-markdown.ts +9 -67
  127. package/extensions/plan/web-payload.ts +3 -3
  128. package/extensions/plan/web-router.ts +18 -9
  129. package/extensions/plan/web.ts +1 -12
  130. package/extensions/question/tools.ts +6 -2
  131. package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
  132. package/extensions/rewind/commands/commands.ts +6 -3
  133. package/extensions/rewind/state/runtime.ts +1 -1
  134. package/extensions/rewind/{view/status.ts → status.ts} +4 -4
  135. package/extensions/rewind/view/review-tui.ts +4 -4
  136. package/extensions/serve/config.ts +10 -8
  137. package/extensions/serve/index.ts +17 -3
  138. package/extensions/serve/registry.ts +26 -17
  139. package/extensions/session-bus/commands.ts +11 -6
  140. package/extensions/session-bus/join-request.ts +2 -1
  141. package/extensions/session-bus/mail-watch.ts +26 -17
  142. package/extensions/session-bus/protocol.ts +15 -3
  143. package/extensions/session-bus/resolve.ts +5 -7
  144. package/extensions/session-bus/runtime.ts +40 -12
  145. package/extensions/session-bus/store.ts +101 -19
  146. package/extensions/session-bus/types.ts +19 -0
  147. package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
  148. package/extensions/subagent/agents.ts +1 -1
  149. package/extensions/subagent/commands.ts +4 -2
  150. package/extensions/subagent/format.ts +15 -15
  151. package/extensions/subagent/index.ts +1 -1
  152. package/extensions/subagent/render.ts +3 -3
  153. package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
  154. package/extensions/team/commands.ts +24 -7
  155. package/extensions/team/dispatch.ts +18 -4
  156. package/extensions/team/format.ts +17 -2
  157. package/extensions/team/index.ts +9 -8
  158. package/extensions/team/schema.ts +6 -5
  159. package/extensions/team/tools.ts +107 -45
  160. package/extensions/team/types.ts +3 -2
  161. package/extensions/todo/commands.ts +4 -2
  162. package/extensions/todo/format.ts +11 -0
  163. package/extensions/todo/render.ts +57 -0
  164. package/extensions/todo/runtime.ts +1 -1
  165. package/extensions/todo/tools.ts +1 -1
  166. package/extensions/todo/view/todo-screen.ts +122 -0
  167. package/extensions/todo/widget.ts +47 -0
  168. package/extensions/watch/commands.ts +4 -2
  169. package/extensions/watch/format.ts +36 -7
  170. package/extensions/watch/render.ts +57 -0
  171. package/extensions/watch/runtime.ts +27 -22
  172. package/extensions/watch/tools.ts +4 -4
  173. package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
  174. package/extensions/web-access/fetch/tool.ts +12 -4
  175. package/extensions/wechat/commands/commands.ts +18 -14
  176. package/extensions/wechat/index.ts +72 -37
  177. package/extensions/wechat/state/credentials.ts +1 -1
  178. package/extensions/wechat/state/state.ts +1 -1
  179. package/extensions/wechat/text/status.ts +112 -0
  180. package/extensions/wechat/types.ts +2 -2
  181. package/extensions/wechat/view/status-screen.ts +4 -103
  182. package/extensions/worktree/commands.ts +11 -9
  183. package/extensions/worktree/enter.ts +2 -2
  184. package/extensions/worktree/exit.ts +2 -2
  185. package/extensions/worktree/index.ts +5 -5
  186. package/extensions/worktree/screen.ts +3 -3
  187. package/extensions/worktree/status.ts +2 -2
  188. package/extensions/worktree/tools.ts +113 -116
  189. package/lib/README.md +27 -13
  190. package/lib/board/actions.ts +111 -0
  191. package/lib/board/artifact.ts +149 -0
  192. package/lib/board/card.ts +163 -52
  193. package/lib/board/config.ts +58 -0
  194. package/lib/board/contract.ts +113 -0
  195. package/lib/board/join.ts +38 -0
  196. package/lib/board/lanes.ts +182 -0
  197. package/lib/board/lead.ts +130 -0
  198. package/lib/board/limits.ts +78 -0
  199. package/lib/board/links.ts +31 -10
  200. package/lib/board/page-pointer.ts +1 -1
  201. package/lib/board/read.ts +83 -0
  202. package/lib/board/reason.ts +102 -0
  203. package/lib/board/round-text.ts +68 -6
  204. package/lib/board/round.ts +31 -15
  205. package/lib/board/rules.ts +151 -307
  206. package/lib/board/seat.ts +35 -0
  207. package/lib/board/session.ts +136 -54
  208. package/lib/board/skipped.ts +22 -0
  209. package/lib/board/steps.ts +487 -0
  210. package/lib/board/store.ts +87 -30
  211. package/lib/board/tone.ts +92 -0
  212. package/lib/board/view.ts +70 -58
  213. package/{extensions/watch → lib}/child.ts +40 -21
  214. package/lib/env.ts +65 -7
  215. package/lib/format.ts +13 -0
  216. package/lib/http/index.ts +14 -12
  217. package/lib/http/port.ts +15 -10
  218. package/lib/lazy.ts +45 -0
  219. package/lib/plan-entry.ts +39 -10
  220. package/lib/plans/paths.ts +82 -0
  221. package/lib/policy.ts +9 -6
  222. package/lib/report.ts +3 -2
  223. package/lib/requirements.ts +8 -8
  224. package/lib/session-label.ts +111 -5
  225. package/lib/session-roster.ts +43 -1
  226. package/lib/session-tool.ts +5 -4
  227. package/lib/stray-fields.ts +7 -2
  228. package/lib/tool-names.ts +5 -10
  229. package/lib/tui/README.md +23 -8
  230. package/lib/tui/chrome.ts +16 -15
  231. package/lib/tui/cursor.ts +18 -3
  232. package/lib/tui/glyphs.ts +9 -7
  233. package/lib/tui/tokens.ts +33 -2
  234. package/lib/tui/tool.ts +3 -3
  235. package/lib/web/README.md +21 -0
  236. package/lib/web/components.ts +5 -1
  237. package/lib/web/index.ts +1 -0
  238. package/lib/web/markdown.ts +83 -0
  239. package/lib/web/markup.ts +2 -1
  240. package/lib/web/runtime.ts +9 -10
  241. package/package.json +6 -1
  242. package/skills/init/SKILL.md +5 -5
  243. package/skills/next/SKILL.md +3 -5
  244. package/extensions/board/seat.ts +0 -28
  245. package/extensions/todo/tui.ts +0 -212
  246. package/lib/tui/index.ts +0 -34
  247. package/skills/project-docs/SKILL.md +0 -133
  248. package/skills/project-docs/references/audit.md +0 -131
  249. package/skills/project-docs/references/changelog.md +0 -105
  250. package/skills/project-docs/references/discovery.md +0 -127
  251. package/skills/project-docs/references/readme.md +0 -80
  252. package/skills/project-docs/references/release.md +0 -84
  253. package/skills/project-docs/references/roadmap.md +0 -93
  254. 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,24 +66,55 @@ 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
  *
@@ -114,102 +164,6 @@ __RUNTIME__
114
164
  return undefined;
115
165
  }
116
166
 
117
- /** The one line under a card's meta: why it sits where it does. */
118
- function reasonOf(card, view) {
119
- var last = card.notes.length > 0 ? card.notes[card.notes.length - 1] : undefined;
120
- // An order is the strongest reason a card is not being worked on, and the one a
121
- // reader cannot see from the lane: it comes before the note that says the same.
122
- // The sentence is the snapshot's, so this line and the tool's refusal name one list.
123
- if (card.waits !== undefined) return { text: "waits on " + card.waits, kind: "waiting" };
124
- if (card.status === "blocked") {
125
- // The lane already says blocked and the tone already says danger; the line says why.
126
- // The recorded reason comes first: the note is what somebody happened to write last,
127
- // and it is the reason a move to blocked asks for. The slot carries the reason alone —
128
- // the lane head already says blocked, and a status prefix in it would be a second voice.
129
- if (card.blockedBy) return { text: card.blockedBy, kind: "blocked" };
130
- return { text: last ? last.text : "blocked, with nothing recorded", kind: "blocked" };
131
- }
132
- // An offer is a decision the lead already made once: the lane says nobody holds it, and
133
- // this says how long the broadcast has gone unanswered, which is the difference between
134
- // nobody having looked and nobody taking it.
135
- if (card.status === "todo" && card.offeredAt !== undefined) {
136
- return { text: "offered " + ago(card.offeredAt) + " ago · nobody took it", kind: "waiting" };
137
- }
138
- if (card.status === "doing" && !card.plan) {
139
- return { text: "no plan recorded", kind: "unplanned" };
140
- }
141
- if (card.status === "doing") return { text: "planned", kind: "plan" };
142
- if (card.status === "done") {
143
- var landed = card.landed ? " · landed " + card.landed.commit : "";
144
- return { text: "✓ done" + (card.owner ? " by " + card.owner.label : "") + landed, kind: "done" };
145
- }
146
- return last ? { text: last.text, kind: "note" } : undefined;
147
- }
148
-
149
- /** A path hint, or the +N chip that points at the rest. */
150
- function paths(card, limit) {
151
- var list = el("ul", N.cardPaths);
152
- var shown = card.paths.slice(0, limit);
153
- for (var i = 0; i < shown.length; i++) {
154
- var item = el("li", "");
155
- var hint = chip(shown[i]);
156
- // A path longer than the card elides, and the whole of it is on the hover title: the
157
- // card is often the only place its hints are read before opening the record.
158
- hint.title = shown[i];
159
- item.appendChild(hint);
160
- list.appendChild(item);
161
- }
162
- if (card.paths.length > limit) {
163
- var rest = el("li", "");
164
- rest.appendChild(chip("+" + (card.paths.length - limit)));
165
- list.appendChild(rest);
166
- }
167
- return list;
168
- }
169
-
170
- /** One card in its lane. */
171
- function cardNode(card, view) {
172
- var item = el("li", N.card);
173
- item.setAttribute("data-card", card.id);
174
- item.setAttribute("data-status", card.status);
175
- item.setAttribute("data-rev", String(card.rev));
176
-
177
- var head = el("p", N.cardHead);
178
- head.appendChild(el("span", N.cardId, "#" + card.id));
179
- var title = actionButton(N.cardTitle, card.title, function () {
180
- board.openCard(card.id);
181
- });
182
- head.appendChild(title);
183
- item.appendChild(head);
184
-
185
- var meta = el("p", N.cardMeta);
186
- meta.appendChild(card.owner ? chip(card.owner.label, "accent") : el("span", "", "unassigned"));
187
- // The order inside a lane is the priority, so an urgent card says so: an order
188
- // nobody can see reads as an accident. The lane's colour stays the card's own.
189
- if (card.priority !== "normal") meta.appendChild(chip(card.priority));
190
- // The stamp, so the clock can keep the age honest between frames: a board can be
191
- // quiet for ten minutes while the page still says "live".
192
- var age = el("span", "", ago(card.updatedAt));
193
- age.setAttribute("data-at", String(card.updatedAt));
194
- meta.appendChild(age);
195
- item.appendChild(meta);
196
-
197
- if (card.paths.length > 0) item.appendChild(paths(card, PATH_CHIPS));
198
-
199
- var reason = reasonOf(card, view);
200
- if (reason) {
201
- // The tone is a lookup, not a suffix: a name assembled from a prefix is a name no
202
- // test can compare with the stylesheet.
203
- var tone = REASON_TONE[reason.kind];
204
- var line = el("p", tone === undefined ? N.cardReason : N.cardReason + " " + tone, reason.text);
205
- // It clamps to one line on the card, so the full sentence is the tooltip.
206
- line.title = reason.text;
207
- item.appendChild(line);
208
- }
209
-
210
- return item;
211
- }
212
-
213
167
  /**
214
168
  * The cards as the page draws them.
215
169
  *
@@ -228,45 +182,55 @@ __RUNTIME__
228
182
  return seen;
229
183
  }
230
184
 
231
- /** 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. */
232
186
  function renderChrome(view) {
233
- var me = document.getElementById("board-me");
234
- if (me) {
235
- me.textContent = view.me.label;
236
- 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;
237
195
  }
238
196
  var progress = document.getElementById("board-progress");
197
+ var done = laneOf(view, "done");
198
+ var finished = done === undefined ? 0 : done.cards.length;
239
199
  if (progress) {
240
- var total = view.counts.total;
200
+ var total = view.total;
241
201
  // One or two cards is not a ratio: an empty bar beside "0/1 done" is noise.
242
202
  progress.hidden = total < 3;
243
203
  var fill = document.getElementById("board-progress-fill");
244
- 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) + "%";
245
205
  var text = document.getElementById("board-progress-text");
246
- if (text) text.textContent = view.counts.done + "/" + total + " done";
206
+ if (text) text.textContent = finished + "/" + total + " done";
247
207
  }
248
208
  var where = document.getElementById("board-where");
249
209
  if (where) {
250
- // A footer that only counts read cards would say "2 cards" over a board holding
251
- // 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.
252
215
  where.textContent =
253
- view.repo + " · store " + view.where + " · " + plural(view.counts.total, "card") + " · " + size(view.bytes) +
254
- (view.skipped > 0 ? " · " + plural(view.skipped, "unreadable") : "");
255
- // The name is what the line has room for; the path is what tells two clones of one
256
- // repository apart when the name is the same.
257
- where.title = view.root;
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");
258
220
  }
259
221
  // The tab has to tell two boards apart, and a board's subject is its repository: one
260
222
  // board per repository, named the way the directory holding it is. The store key is a
261
223
  // hash, which no reader can say out loud, so it is not what a tab leads with. The count
262
224
  // is what a browser full of tabs is read for, and an empty board says nothing rather
263
225
  // than 0.
264
- var open = view.counts.total - view.counts.done;
226
+ var open = view.total - finished;
265
227
  document.title = tabTitle("board", view.repo, open > 0 ? open + " open" : undefined);
266
228
  }
267
229
 
268
230
  /** 1 card, 2 cards — the one body, handed in by the bundle's assembler. */
269
231
  var plural = __PLURAL__;
232
+ /** The refused files, named with their reasons: the tool's own sentence, handed in. */
233
+ var skippedText = __SKIPPED__;
270
234
  /** board · my-pi-agent · 3 open, the tab's one shape, handed in the same way. */
271
235
  var tabTitle = __TAB_TITLE__;
272
236
 
@@ -277,43 +241,90 @@ __RUNTIME__
277
241
  * writing to the cards would slowly lie about how long they have sat there. The
278
242
  * ticker calls this; it touches nothing else, so a filter, a folded lane, or an
279
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.
280
247
  */
281
248
  function refreshAges() {
282
249
  var nodes = document.querySelectorAll("[data-at]");
283
250
  for (var i = 0; i < nodes.length; i++) {
284
251
  var next = ago(Number(nodes[i].getAttribute("data-at")));
285
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);
286
255
  }
287
256
  }
288
257
 
289
258
  /** Whether the filter keeps a card: the module's one predicate, spliced in as source. */
290
259
  var matches = __MATCHES__;
291
260
 
292
- /** 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. */
293
289
  function laneNode(lane) {
294
- var section = document.querySelector('.board-lane[data-status="' + lane.status + '"]');
290
+ var section = document.querySelector('.board-lane[data-lane="' + lane.key + '"]');
295
291
  if (section) return section;
296
292
  section = el("section", N.lane);
297
- section.setAttribute("data-status", lane.status);
293
+ section.setAttribute("data-lane", lane.key);
298
294
  var head = el("h2", N.laneHead);
299
- var toggle = actionButton(N.laneToggle, LANE_LABEL[lane.status] || lane.status, function () {
300
- board.toggleLane(lane.status);
295
+ var toggle = actionButton(N.laneToggle, lane.label, function () {
296
+ board.toggleFold(lane.key);
301
297
  });
298
+ toggle.setAttribute("data-fold", lane.key);
302
299
  toggle.setAttribute("aria-expanded", "true");
303
- toggle.setAttribute("aria-controls", "lane-" + lane.status);
304
- toggle.id = "lane-head-" + lane.status;
300
+ toggle.setAttribute("aria-controls", "lane-" + lane.key);
301
+ toggle.id = "lane-head-" + lane.key;
305
302
  head.appendChild(el("span", N.laneDot));
306
303
  head.appendChild(toggle);
307
304
  head.appendChild(el("span", N.laneCount));
308
305
  section.appendChild(head);
309
306
  var list = el("ol", N.laneList);
310
- list.id = "lane-" + lane.status;
307
+ list.id = "lane-" + lane.key;
311
308
  section.appendChild(list);
312
- section.appendChild(el("p", N.laneEmpty, LANE_EMPTY[lane.status] || "nothing"));
309
+ section.appendChild(el("p", N.laneEmpty, lane.empty));
313
310
  return section;
314
311
  }
315
312
 
316
- /** 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. */
317
328
  function renderLanes(view) {
318
329
  var lanes = document.getElementById("board-lanes");
319
330
  if (!lanes) return;
@@ -323,9 +334,7 @@ __RUNTIME__
323
334
  lanes.removeAttribute("data-skeleton");
324
335
  }
325
336
  var query = board.query || "";
326
- var empty = document.getElementById("board-no-cards");
327
- if (empty) empty.hidden = view.counts.total !== 0;
328
- lanes.hidden = view.counts.total === 0;
337
+ var shownTotal = 0;
329
338
  for (var i = 0; i < view.lanes.length; i++) {
330
339
  var lane = view.lanes[i];
331
340
  var section = laneNode(lane);
@@ -333,9 +342,9 @@ __RUNTIME__
333
342
  // Finished work starts folded, once: the default is what the page opens with, and
334
343
  // toggleLane writes a boolean from then on, so a reader who unfolds Done keeps it
335
344
  // unfolded through every frame a peer's write causes.
336
- if (lane.status === "done" && lane.cards.length >= DONE_FOLD_AT) {
345
+ if (lane.key === "done" && lane.cards.length >= FOLD_AT.done) {
337
346
  board.collapsed = board.collapsed || {};
338
- if (board.collapsed[lane.status] === undefined) board.collapsed[lane.status] = true;
347
+ if (board.collapsed[lane.key] === undefined) board.collapsed[lane.key] = true;
339
348
  }
340
349
  // A lane holding nothing is a rail: its dot, its word, and its count, in the width
341
350
  // the fact deserves. The filter never does this — filtering is a view of the cards,
@@ -346,25 +355,29 @@ __RUNTIME__
346
355
  // for want of a column the lane is not using. A count, never text.
347
356
  section.style.setProperty("--lane-count", String(lane.cards.length));
348
357
  var shown = lane.cards.filter(function (card) { return matches(card, query); });
349
- 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;
350
360
  if (section.getAttribute("data-sig") !== sig) {
351
361
  section.setAttribute("data-sig", sig);
352
362
  var list = section.querySelector(".board-lane__list");
353
363
  list.textContent = "";
354
- 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]));
355
365
  }
356
366
  var count = section.querySelector(".board-lane__count");
367
+ var head = section.querySelector(".board-lane__head");
357
368
  if (count) {
358
- count.textContent = query === "" || shown.length === lane.cards.length
359
- ? String(lane.cards.length)
360
- : 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));
361
374
  }
362
375
  var none = section.querySelector(".board-lane__empty");
363
376
  if (none) {
364
377
  none.hidden = shown.length !== 0;
365
- 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;
366
379
  }
367
- var collapsed = board.collapsed && board.collapsed[lane.status] === true;
380
+ var collapsed = board.collapsed && board.collapsed[lane.key] === true;
368
381
  var list = section.querySelector(".board-lane__list");
369
382
  if (list) list.hidden = collapsed;
370
383
  // A folded lane shows nothing, so it takes the rail's width the way an empty one
@@ -374,6 +387,7 @@ __RUNTIME__
374
387
  var toggle = section.querySelector(".board-lane__toggle");
375
388
  if (toggle) toggle.setAttribute("aria-expanded", String(!collapsed));
376
389
  }
390
+ board.renderEmpty(view, query, shownTotal);
377
391
  renderPicker(view);
378
392
  }
379
393
 
@@ -384,118 +398,33 @@ __RUNTIME__
384
398
  picker.textContent = "";
385
399
  for (var i = 0; i < view.lanes.length; i++) {
386
400
  (function (lane) {
387
- 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));
388
405
  item.type = "button";
389
- item.setAttribute("role", "tab");
390
- item.setAttribute("data-lane", lane.status);
391
- 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); });
392
408
  picker.appendChild(item);
393
409
  })(view.lanes[i]);
394
410
  }
395
411
  board.applyLane();
396
412
  }
397
413
 
398
- /** Fill the card dialog's contents from a card, without opening or closing it. */
399
- function fillDialog(card, view) {
400
- document.getElementById("card-dialog-title").textContent = card.title;
401
- document.getElementById("card-dialog-id").textContent = "#" + card.id;
402
- // The lane is a status of its own, not a word inside a sentence: the same dot and
403
- // label a lane head wears, so the dialog and the board speak the same vocabulary.
404
- var state = document.getElementById("card-dialog-state");
405
- state.setAttribute("data-status", card.status);
406
- state.textContent = card.status;
407
-
408
- // One fact per row of the ladder, so a reader can pick out a value instead of
409
- // hunting for it in a sentence. A row with nothing to say takes its label with it:
410
- // an empty cell under a heading reads as a missing value, not as an absence.
411
- var pair = function (name, shown) {
412
- var dt = document.getElementById("card-dialog-" + name + "-label");
413
- var dd = document.getElementById("card-dialog-" + name);
414
- if (dt) dt.hidden = !shown;
415
- if (dd) dd.hidden = !shown;
416
- return dd;
417
- };
418
- var setRow = function (name, shown, value, full) {
419
- var dd = pair(name, shown);
420
- dd.textContent = shown ? value : "";
421
- if (full === undefined) dd.removeAttribute("title");
422
- else dd.setAttribute("title", full);
423
- };
424
-
425
- var owner = document.getElementById("card-dialog-owner");
426
- owner.textContent = "";
427
- owner.appendChild(card.owner ? chip(card.owner.label, "accent") : document.createTextNode("unassigned"));
428
- pair("owner", true);
429
-
430
- var priority = pair("priority", card.priority !== "normal");
431
- priority.textContent = "";
432
- if (card.priority !== "normal") priority.appendChild(chip(card.priority));
433
-
434
- setRow("updated", true, ago(card.updatedAt));
435
- // How long the card has been in the lane it is in now, which updatedAt cannot say:
436
- // a note filed against a blocked card moves that stamp without moving the card. Absent
437
- // for a card written before the log existed, so the row takes its label with it.
438
- setRow("lane", card.since !== undefined, card.status + " · " + ago(card.since));
439
- setRow("by", true, card.by);
440
- setRow("landed", card.landed !== undefined, card.landed ? card.landed.commit : "", card.landed ? card.landed.commit : undefined);
441
-
442
- // The dialog and the card face say one thing, from one field: the ids it was linked
443
- // to are the storage, and a reader is owed the lanes that make them a decision.
444
- setRow("depends", card.waits !== undefined, card.waits || "", card.waits);
445
- // The other side of the same link: what leaving this card undone is holding up, which
446
- // is the one thing a card's own row cannot say.
447
- setRow("dependents", card.dependents !== undefined, card.dependents || "", card.dependents);
448
-
449
- // A long plan path elides on one line, and the whole of it is on the hover title and
450
- // in the text, so it stays copyable: the reason to open a card is often its plan.
451
- setRow("plan", Boolean(card.plan), card.plan || "", card.plan || undefined);
452
-
453
- pair("paths", card.paths.length > 0);
454
- var pathList = document.getElementById("card-dialog-path-list");
455
- pathList.textContent = "";
456
- for (var i = 0; i < card.paths.length; i++) {
457
- var item = el("li", "");
458
- var hint = chip(card.paths[i]);
459
- hint.className += " " + N.dialogPath;
460
- hint.title = card.paths[i];
461
- item.appendChild(hint);
462
- pathList.appendChild(item);
463
- }
464
-
465
- var notes = document.getElementById("card-dialog-notes");
466
- notes.textContent = "";
467
- for (var j = 0; j < card.notes.length; j++) {
468
- var note = card.notes[j];
469
- // The prose is the record and reads at body size; who wrote it and when is its
470
- // footnote, so it follows the sentence instead of standing over it.
471
- var row = el("li", N.dialogNote);
472
- row.appendChild(el("p", N.dialogNoteText, note.text));
473
- var foot = el("p", N.dialogNoteFoot);
474
- foot.appendChild(el("span", N.dialogNoteBy, note.by));
475
- foot.appendChild(el("span", N.dialogNoteAt, ago(note.at)));
476
- row.appendChild(foot);
477
- notes.appendChild(row);
478
- }
479
- var count = document.getElementById("card-dialog-note-count");
480
- if (count) count.textContent = card.notes.length === 0 ? " · none" : " · " + card.notes.length;
481
- var earlier = document.getElementById("card-dialog-earlier");
482
- earlier.hidden = card.notesDropped === 0;
483
- earlier.textContent = card.notesDropped === 0 ? "" : card.notesDropped + " earlier notes dropped";
484
-
485
- // A modal whose title changes is not announced, so walking with j/k says where it
486
- // landed: the position is counted over the cards as they are drawn, not as stored, so
487
- // a filter or a folded lane cannot make the number disagree with the screen.
488
- var where = document.getElementById("card-dialog-where");
489
- if (where) {
490
- var drawn = drawnCards();
491
- var at = 0;
492
- for (var k = 0; k < drawn.length; k++) {
493
- if (drawn[k].getAttribute("data-card") === String(card.id)) at = k + 1;
494
- }
495
- // Opened from the Team tab there is no drawn board to count along, so the line
496
- // names the card instead of claiming a position it cannot know.
497
- where.textContent = drawn.length === 0 ? card.title : "card " + at + " of " + drawn.length + ": " + card.title;
498
- }
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();
499
428
  }
500
429
 
501
430
  /** Paint a whole snapshot. */
@@ -504,11 +433,14 @@ __RUNTIME__
504
433
  renderChrome(view);
505
434
  renderLanes(view);
506
435
  board.renderTeam(view);
507
- if (board.openId !== undefined) {
508
- var card = findCard(view, board.openId);
509
- if (card) fillDialog(card, view);
510
- else board.closeCard();
511
- }
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();
512
444
  };
513
445
 
514
446
  board.el = el;
@@ -518,7 +450,12 @@ __RUNTIME__
518
450
  board.findCard = findCard;
519
451
  board.drawnCards = drawnCards;
520
452
  board.matches = matches;
521
- 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;
522
459
  board.refreshAges = refreshAges;
523
460
  board.plural = plural;
524
461
  })();`;