@gavin-hu/my-pi-agent 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +17 -11
  2. package/README.zh-CN.md +13 -8
  3. package/docs/extensions/README.md +1 -1
  4. package/docs/extensions/board.md +286 -123
  5. package/docs/extensions/bytefly.md +381 -223
  6. package/docs/extensions/file-browse.md +14 -7
  7. package/docs/extensions/job.md +1 -2
  8. package/docs/extensions/plan.md +56 -11
  9. package/docs/extensions/session-bus.md +23 -10
  10. package/docs/extensions/team.md +45 -17
  11. package/docs/extensions/wechat.md +2 -1
  12. package/docs/extensions/worktree.md +30 -15
  13. package/extensions/board/commands.ts +23 -5
  14. package/extensions/board/format.ts +107 -9
  15. package/extensions/board/join.ts +1 -1
  16. package/extensions/board/page.ts +170 -94
  17. package/extensions/board/result.ts +9 -0
  18. package/extensions/board/schema.ts +22 -7
  19. package/extensions/board/seat.ts +9 -10
  20. package/extensions/board/snapshot.ts +1 -0
  21. package/extensions/board/tools.ts +186 -21
  22. package/extensions/board/transport/router.ts +27 -108
  23. package/extensions/board/transport/server.ts +18 -29
  24. package/extensions/board/types.ts +67 -0
  25. package/extensions/board/view/client.ts +23 -13
  26. package/extensions/board/view/dialog.ts +43 -0
  27. package/extensions/board/view/keys.ts +28 -24
  28. package/extensions/board/view/live.ts +5 -171
  29. package/extensions/board/view/names.ts +91 -0
  30. package/extensions/board/view/page.ts +48 -88
  31. package/extensions/board/view/render.ts +213 -190
  32. package/extensions/board/view/styles.ts +65 -48
  33. package/extensions/board/view/team.ts +63 -41
  34. package/extensions/bytefly/footer/budget.ts +6 -5
  35. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  36. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  37. package/extensions/bytefly/footer/config.ts +19 -8
  38. package/extensions/bytefly/footer/price/price.ts +30 -8
  39. package/extensions/bytefly/footer/register.ts +5 -5
  40. package/extensions/bytefly/footer/render/format.ts +18 -0
  41. package/extensions/bytefly/footer/render/lines.ts +23 -7
  42. package/extensions/bytefly/footer/render/segments.ts +117 -31
  43. package/extensions/bytefly/footer/types.ts +29 -5
  44. package/extensions/bytefly/footer/unpriced.ts +42 -17
  45. package/extensions/bytefly/panel/commands.ts +28 -1
  46. package/extensions/bytefly/panel/filter.ts +68 -0
  47. package/extensions/bytefly/panel/format.ts +16 -15
  48. package/extensions/bytefly/panel/join.ts +56 -13
  49. package/extensions/bytefly/panel/prices.ts +43 -0
  50. package/extensions/bytefly/panel/settings.ts +72 -23
  51. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  52. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  53. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  54. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  55. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  56. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  57. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  58. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  59. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  60. package/extensions/bytefly/panel/tui.ts +306 -259
  61. package/extensions/check/commands.ts +2 -1
  62. package/extensions/file-browse/files/classify.ts +136 -0
  63. package/extensions/file-browse/files/files.ts +8 -547
  64. package/extensions/file-browse/files/index.ts +90 -0
  65. package/extensions/file-browse/files/list.ts +261 -0
  66. package/extensions/file-browse/files/tree.ts +101 -0
  67. package/extensions/file-browse/transport/router.ts +0 -2
  68. package/extensions/file-browse/view/emit.ts +51 -0
  69. package/extensions/file-browse/view/html.ts +6 -7
  70. package/extensions/file-browse/view/markdown.ts +11 -13
  71. package/extensions/file-browse/view/raw-html.ts +18 -13
  72. package/extensions/job/runtime.ts +4 -4
  73. package/extensions/plan/feedback.ts +40 -16
  74. package/extensions/plan/format.ts +10 -3
  75. package/extensions/plan/index.ts +17 -1
  76. package/extensions/plan/plan-files.ts +88 -0
  77. package/extensions/plan/plans.ts +19 -23
  78. package/extensions/plan/policy.ts +10 -5
  79. package/extensions/plan/runtime.ts +31 -2
  80. package/extensions/plan/todos.ts +51 -3
  81. package/extensions/plan/tools/exit.ts +10 -1
  82. package/extensions/plan/tools/todo-seed.ts +4 -2
  83. package/extensions/plan/tools/write.ts +12 -1
  84. package/extensions/plan/types.ts +15 -0
  85. package/extensions/plan/web-blocks.ts +89 -0
  86. package/extensions/plan/web-chrome.ts +3 -5
  87. package/extensions/plan/web-client.ts +179 -330
  88. package/extensions/plan/web-collab.ts +3 -3
  89. package/extensions/plan/web-comments.ts +18 -8
  90. package/extensions/plan/web-composer.ts +80 -0
  91. package/extensions/plan/web-draft.ts +40 -0
  92. package/extensions/plan/web-keys.ts +134 -0
  93. package/extensions/plan/web-outline.ts +192 -0
  94. package/extensions/plan/web-page.ts +4 -1
  95. package/extensions/plan/web-plan.ts +150 -0
  96. package/extensions/plan/web-router.ts +0 -1
  97. package/extensions/plan/web-settle.ts +194 -0
  98. package/extensions/rewind/commands/review.ts +2 -1
  99. package/extensions/session-bus/rows.ts +7 -3
  100. package/extensions/session-bus/runtime.ts +100 -38
  101. package/extensions/session-bus/store.ts +14 -2
  102. package/extensions/session-bus/tui.ts +1 -1
  103. package/extensions/session-bus/types.ts +10 -1
  104. package/extensions/subagent/commands.ts +2 -1
  105. package/extensions/team/commands.ts +41 -8
  106. package/extensions/team/format.ts +14 -0
  107. package/extensions/team/index.ts +2 -1
  108. package/extensions/team/tools.ts +30 -9
  109. package/extensions/team/types.ts +17 -0
  110. package/extensions/todo/index.ts +4 -1
  111. package/extensions/todo/nudge.ts +8 -2
  112. package/extensions/wechat/bridge.ts +43 -172
  113. package/extensions/wechat/intake.ts +245 -0
  114. package/extensions/worktree/commands.ts +24 -17
  115. package/extensions/worktree/exit.ts +12 -6
  116. package/extensions/worktree/git.ts +22 -14
  117. package/extensions/worktree/guard.ts +15 -285
  118. package/extensions/worktree/handoff.ts +10 -2
  119. package/extensions/worktree/index.ts +2 -1
  120. package/extensions/worktree/lifecycle.ts +2 -0
  121. package/extensions/worktree/registry.ts +20 -11
  122. package/extensions/worktree/root-tools.ts +13 -3
  123. package/extensions/worktree/runtime.ts +8 -2
  124. package/extensions/worktree/screen.ts +15 -8
  125. package/extensions/worktree/shell.ts +303 -0
  126. package/extensions/worktree/state.ts +1 -1
  127. package/extensions/worktree/status.ts +42 -9
  128. package/extensions/worktree/tools.ts +19 -13
  129. package/lib/README.md +6 -5
  130. package/lib/board/card.ts +192 -3
  131. package/lib/board/links.ts +26 -2
  132. package/lib/board/page-pointer.ts +86 -32
  133. package/lib/board/round-text.ts +11 -2
  134. package/lib/board/round.ts +25 -20
  135. package/lib/board/rules.ts +144 -11
  136. package/lib/board/store.ts +239 -52
  137. package/lib/board/view.ts +94 -10
  138. package/lib/format.ts +22 -0
  139. package/lib/http/serve.ts +3 -0
  140. package/lib/http/sink.ts +34 -9
  141. package/lib/plan-entry.ts +11 -0
  142. package/lib/policy.ts +14 -1
  143. package/lib/session-roster.ts +74 -3
  144. package/lib/spawn.ts +37 -3
  145. package/lib/stray-fields.ts +45 -0
  146. package/lib/tool-names.ts +13 -0
  147. package/lib/tui/glyphs.ts +2 -3
  148. package/lib/web/README.md +52 -2
  149. package/lib/web/client-runtime.ts +123 -0
  150. package/lib/web/index.ts +4 -2
  151. package/lib/web/markup.ts +88 -1
  152. package/lib/web/names.ts +42 -0
  153. package/package.json +1 -1
  154. package/extensions/board/view/actions.ts +0 -306
  155. package/extensions/board/writes.ts +0 -99
  156. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  157. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  158. package/extensions/job/process.ts +0 -35
@@ -13,11 +13,37 @@
13
13
  * offset, and an open dialog all survive a peer's write.
14
14
  *
15
15
  * The client modules share one namespace, `window.bfBoard`: this one paints,
16
- * `team.ts` draws the round, `actions.ts` posts, `keys.ts` binds, and `live.ts` owns
17
- * the frame stream and the wiring.
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.
18
+ *
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.
18
24
  */
19
25
 
20
- export const BOARD_RENDER_JS = String.raw`(function () {
26
+ /**
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.
30
+ */
31
+ export function renderJs(rules: {
32
+ plural: string;
33
+ tabTitle: string;
34
+ matches: string;
35
+ age: string;
36
+ runtime: string;
37
+ }): string {
38
+ return renderTemplate
39
+ .replace("__PLURAL__", rules.plural)
40
+ .replace("__TAB_TITLE__", rules.tabTitle)
41
+ .replace("__MATCHES__", rules.matches)
42
+ .replace("__AGE__", rules.age)
43
+ .replace("__RUNTIME__", rules.runtime);
44
+ }
45
+
46
+ const renderTemplate = String.raw`(function () {
21
47
  "use strict";
22
48
  var board = window.bfBoard || (window.bfBoard = {});
23
49
 
@@ -26,14 +52,18 @@ export const BOARD_RENDER_JS = String.raw`(function () {
26
52
  var LANE_EMPTY = { todo: "nothing waiting", doing: "nothing in flight", blocked: "nothing blocked", done: "nothing finished yet" };
27
53
  /** Path chips a lane card shows before it counts the rest. */
28
54
  var PATH_CHIPS = 3;
55
+ /**
56
+ * Finished cards a lane may hold before it starts folded.
57
+ *
58
+ * 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.
61
+ */
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 };
29
65
 
30
- /** One element with optional class and text. */
31
- function el(tag, className, text) {
32
- var node = document.createElement(tag);
33
- if (className) node.className = className;
34
- if (text !== undefined && text !== null) node.textContent = String(text);
35
- return node;
36
- }
66
+ __RUNTIME__
37
67
 
38
68
  /**
39
69
  * One button that calls back, so nothing on the page is a click handler in markup.
@@ -55,16 +85,14 @@ export const BOARD_RENDER_JS = String.raw`(function () {
55
85
  return el("span", "bf-chip" + (tone ? " bf-chip--" + tone : ""), text);
56
86
  }
57
87
 
58
- /** "12m", "3h", "2d" — the age of a card's last write, compact enough for a meta line. */
59
- function ago(at) {
60
- var seconds = Math.max(0, Math.round((Date.now() - at) / 1000));
61
- if (seconds < 45) return "now";
62
- var minutes = Math.round(seconds / 60);
63
- if (minutes < 60) return minutes + "m";
64
- var hours = Math.round(minutes / 60);
65
- if (hours < 24) return hours + "h";
66
- return Math.round(hours / 24) + "d";
67
- }
88
+ /**
89
+ * "12m", "3h", "2d" — an age, compact enough for a meta line.
90
+ *
91
+ * The transcript prints the same rule for the same fact, so the body is spliced in from
92
+ * lib/format.ts rather than written twice. No backticks in here: this is a template.
93
+ */
94
+ var ageText = __AGE__;
95
+ function ago(at) { return ageText(Date.now(), at); }
68
96
 
69
97
  /** Bytes, the way the footer says them. */
70
98
  function size(bytes) {
@@ -87,106 +115,41 @@ export const BOARD_RENDER_JS = String.raw`(function () {
87
115
  return undefined;
88
116
  }
89
117
 
90
- /** Whether this session holds the card. */
91
- function mine(card, me) {
92
- if (!card.owner) return false;
93
- if (card.owner.id !== undefined && me.id !== undefined) return card.owner.id === me.id;
94
- return String(card.owner.label).toLowerCase() === String(me.label).toLowerCase();
95
- }
96
-
97
- /** A card's priority; an older card that never recorded one is ordinary. */
98
- function priorityOf(card) {
99
- return card.priority || "normal";
100
- }
101
-
102
- /**
103
- * The cards this one waits for that are not finished yet.
104
- *
105
- * The board is the index: a link is one id in the waiting card's file, so the other
106
- * card's lane can only be read from the frame. A finished dependency has stopped
107
- * explaining anything, so it drops out of the answer.
108
- */
109
- function blockersOf(card, view) {
110
- var ids = card.dependsOn || [];
111
- if (ids.length === 0 || !view) return [];
112
- var all = cards(view);
113
- var open = [];
114
- for (var i = 0; i < ids.length; i++) {
115
- for (var j = 0; j < all.length; j++) {
116
- if (all[j].id === ids[i] && all[j].status !== "done") open.push(all[j]);
117
- }
118
- }
119
- return open;
120
- }
121
-
122
- /** How the board says what a card waits for: the id and the lane that explains it. */
123
- function waitingText(card, view) {
124
- return blockersOf(card, view)
125
- .map(function (blocker) { return "#" + blocker.id + " (" + blocker.status + ")"; })
126
- .join(", ");
127
- }
128
-
129
- /**
130
- * Whether this reader may do one verb to one card.
131
- *
132
- * The one table behind three surfaces: a card face, the dialog's footer, and a queue
133
- * row each draw a different subset — a face offers two actions, the dialog five — but
134
- * a verb is allowed for the same reader on all of them, and it is the same rule the
135
- * server applies in lib/board/rules.ts. Open is always allowed: reading is not a write.
136
- */
137
- function may(card, view, verb) {
138
- // Reading is not a write, so it survives a viewer's page; everything else is what
139
- // the document's authority says it may be.
140
- if (verb === "open") return true;
141
- if (!canWrite()) return false;
142
- var isMine = mine(card, view.me);
143
- var waiting = blockersOf(card, view).length > 0;
144
- var finished = card.status === "done";
145
- if (verb === "claim") return card.status === "todo" && !waiting;
146
- if (verb === "move") return isMine && !finished;
147
- if (verb === "release") return isMine && !finished;
148
- if (verb === "edit") return card.owner === undefined || isMine;
149
- if (verb === "drop") return card.status === "todo" || finished || isMine;
150
- if (verb === "reclaim") return card.owner !== undefined && !finished;
151
- // Recording an owner is what a page can do where it cannot message: an unowned card
152
- // that is not waiting for another, which is the same card a claim is offered.
153
- if (verb === "assign") return card.owner === undefined && !finished && !waiting;
154
- return false;
155
- }
156
-
157
- /**
158
- * Whether this page may change a card at all.
159
- *
160
- * The document is rendered per request, so a viewer's shell says data-writes="0":
161
- * one attribute decides every affordance, and the server refuses anyway — a page
162
- * that offered what it would be turned down for teaches a reader the wrong thing.
163
- */
164
- function canWrite() {
165
- return document.documentElement.getAttribute("data-writes") !== "0";
166
- }
167
-
168
118
  /** The one line under a card's meta: why it sits where it does. */
169
119
  function reasonOf(card, view) {
170
120
  var last = card.notes.length > 0 ? card.notes[card.notes.length - 1] : undefined;
171
121
  // An order is the strongest reason a card is not being worked on, and the one a
172
122
  // reader cannot see from the lane: it comes before the note that says the same.
173
- var waiting = waitingText(card, view);
174
- if (waiting !== "") return { text: "waits on " + waiting, kind: "waiting" };
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" };
175
125
  if (card.status === "blocked") {
176
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" };
177
131
  return { text: last ? last.text : "blocked, with nothing recorded", kind: "blocked" };
178
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
+ }
179
139
  if (card.status === "doing" && !card.plan) {
180
140
  return { text: "no plan recorded", kind: "unplanned" };
181
141
  }
182
142
  if (card.status === "doing") return { text: "planned", kind: "plan" };
183
- if (card.status === "done") return { text: "✓ done" + (card.owner ? " by " + card.owner.label : ""), kind: "done" };
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
+ }
184
147
  return last ? { text: last.text, kind: "note" } : undefined;
185
148
  }
186
149
 
187
150
  /** A path hint, or the +N chip that points at the rest. */
188
151
  function paths(card, limit) {
189
- var list = el("ul", "board-card__paths");
152
+ var list = el("ul", N.cardPaths);
190
153
  var shown = card.paths.slice(0, limit);
191
154
  for (var i = 0; i < shown.length; i++) {
192
155
  var item = el("li", "");
@@ -203,25 +166,25 @@ export const BOARD_RENDER_JS = String.raw`(function () {
203
166
 
204
167
  /** One card in its lane. */
205
168
  function cardNode(card, view) {
206
- var item = el("li", "board-card");
169
+ var item = el("li", N.card);
207
170
  item.setAttribute("data-card", card.id);
208
171
  item.setAttribute("data-status", card.status);
209
172
  item.setAttribute("data-rev", String(card.rev));
210
173
 
211
- var head = el("p", "board-card__head");
212
- head.appendChild(el("span", "board-card__id", "#" + card.id));
174
+ var head = el("p", N.cardHead);
175
+ head.appendChild(el("span", N.cardId, "#" + card.id));
213
176
  var title = actionButton(card.title, function () {
214
177
  board.openCard(card.id);
215
178
  });
216
- title.className = "board-card__title";
179
+ title.className = N.cardTitle;
217
180
  head.appendChild(title);
218
181
  item.appendChild(head);
219
182
 
220
- var meta = el("p", "board-card__meta");
183
+ var meta = el("p", N.cardMeta);
221
184
  meta.appendChild(card.owner ? chip(card.owner.label, "accent") : el("span", "", "unassigned"));
222
185
  // The order inside a lane is the priority, so an urgent card says so: an order
223
186
  // nobody can see reads as an accident. The lane's colour stays the card's own.
224
- if (priorityOf(card) !== "normal") meta.appendChild(chip(priorityOf(card)));
187
+ if (card.priority !== "normal") meta.appendChild(chip(card.priority));
225
188
  // The stamp, so the clock can keep the age honest between frames: a board can be
226
189
  // quiet for ten minutes while the page still says "live".
227
190
  var age = el("span", "", ago(card.updatedAt));
@@ -233,37 +196,42 @@ export const BOARD_RENDER_JS = String.raw`(function () {
233
196
 
234
197
  var reason = reasonOf(card, view);
235
198
  if (reason) {
236
- var kind = reason.kind === "blocked" || reason.kind === "unplanned" ? " board-card__reason--" + reason.kind : "";
237
- var line = el("p", "board-card__reason" + kind, reason.text);
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);
238
203
  // It clamps to one line on the card, so the full sentence is the tooltip.
239
204
  line.title = reason.text;
240
205
  item.appendChild(line);
241
206
  }
242
207
 
243
- var actions = el("div", "board-card__actions");
244
- // A card that waits cannot be claimed, so the card face does not offer it: the reason
245
- // line above is what it says instead. Move is not a permission but the way into the
246
- // dialog, so it is drawn for any card still in flight, whoever holds it.
247
- if (may(card, view, "claim")) {
248
- actions.appendChild(actionButton("Claim", function () { board.act("claim", card); }, "plain"));
249
- } else if (canWrite() && card.status !== "todo" && card.status !== "done") {
250
- // Move is the way into the dialog, not a permission, but a dialog with no actions
251
- // in it is not worth opening.
252
- actions.appendChild(actionButton("Move", function () { board.openCard(card.id, "move"); }, "plain"));
253
- }
254
- // A note is a write, and a viewer's dialog has no field for one.
255
- if (canWrite()) actions.appendChild(actionButton("Note", function () { board.openCard(card.id, "note"); }));
256
- actions.appendChild(actionButton("Open", function () { board.openCard(card.id); }));
257
- item.appendChild(actions);
258
208
  return item;
259
209
  }
260
210
 
261
- /** The toolbar's identity, progress, and footer. */
211
+ /**
212
+ * The cards as the page draws them.
213
+ *
214
+ * A folded lane keeps its cards in the document, and so does a panel the other tab
215
+ * hides: the box is the only honest test of whether a reader can see one, and it is
216
+ * what j/k walks and what the dialog's position counts.
217
+ */
218
+ function drawnCards() {
219
+ var nodes = document.querySelectorAll(".board-card");
220
+ var seen = [];
221
+ for (var i = 0; i < nodes.length; i++) {
222
+ if (nodes[i].hidden) continue;
223
+ if (!nodes[i].getClientRects || nodes[i].getClientRects().length === 0) continue;
224
+ seen.push(nodes[i]);
225
+ }
226
+ return seen;
227
+ }
228
+
229
+ /** The toolbar's identity, progress, footer, and the tab's own name. */
262
230
  function renderChrome(view) {
263
231
  var me = document.getElementById("board-me");
264
232
  if (me) {
265
233
  me.textContent = view.me.label;
266
- me.title = "Writes from this page are made as " + view.me.label;
234
+ me.title = "This page is served as " + view.me.label + ", and it cannot change a card";
267
235
  }
268
236
  var progress = document.getElementById("board-progress");
269
237
  if (progress) {
@@ -275,22 +243,25 @@ export const BOARD_RENDER_JS = String.raw`(function () {
275
243
  var text = document.getElementById("board-progress-text");
276
244
  if (text) text.textContent = view.counts.done + "/" + total + " done";
277
245
  }
278
- // A viewer gets the board, not the chance to make one: the composer and the empty
279
- // state's way into it belong to whoever serves the page.
280
- var create = document.getElementById("board-new");
281
- if (create) create.hidden = !canWrite();
282
- var first = document.getElementById("board-first-card");
283
- if (first) first.hidden = !canWrite();
284
246
  var where = document.getElementById("board-where");
285
247
  if (where) {
286
- where.textContent = "store " + view.where + " · " + plural(view.counts.total, "card") + " · " + size(view.bytes);
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.
250
+ where.textContent =
251
+ "store " + view.where + " · " + plural(view.counts.total, "card") + " · " + size(view.bytes) +
252
+ (view.skipped > 0 ? " · " + plural(view.skipped, "unreadable") : "");
287
253
  }
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);
288
259
  }
289
260
 
290
- /** 1 card, 2 cards. */
291
- function plural(count, noun) {
292
- return count + " " + noun + (count === 1 ? "" : "s");
293
- }
261
+ /** 1 card, 2 cards — the one body, handed in by the bundle's assembler. */
262
+ var plural = __PLURAL__;
263
+ /** board · my-pi-agent · 3 open, the tab's one shape, handed in the same way. */
264
+ var tabTitle = __TAB_TITLE__;
294
265
 
295
266
  /**
296
267
  * Rewrite every age already on the page from its own timestamp.
@@ -308,38 +279,31 @@ export const BOARD_RENDER_JS = String.raw`(function () {
308
279
  }
309
280
  }
310
281
 
311
- /** Whether the filter keeps a card: title, a path hint, or the holder. */
312
- function matches(card, query) {
313
- if (!query) return true;
314
- var needle = query.toLowerCase();
315
- if (card.title.toLowerCase().indexOf(needle) !== -1) return true;
316
- if (card.owner && card.owner.label.toLowerCase().indexOf(needle) !== -1) return true;
317
- for (var i = 0; i < card.paths.length; i++) if (card.paths[i].toLowerCase().indexOf(needle) !== -1) return true;
318
- return false;
319
- }
282
+ /** Whether the filter keeps a card: the module's one predicate, spliced in as source. */
283
+ var matches = __MATCHES__;
320
284
 
321
285
  /** The lane section for one status, created once and then reused. */
322
286
  function laneNode(lane) {
323
287
  var section = document.querySelector('.board-lane[data-status="' + lane.status + '"]');
324
288
  if (section) return section;
325
- section = el("section", "board-lane");
289
+ section = el("section", N.lane);
326
290
  section.setAttribute("data-status", lane.status);
327
- var head = el("h2", "board-lane__head");
291
+ var head = el("h2", N.laneHead);
328
292
  var toggle = actionButton(LANE_LABEL[lane.status] || lane.status, function () {
329
293
  board.toggleLane(lane.status);
330
294
  });
331
- toggle.className = "board-lane__toggle";
295
+ toggle.className = N.laneToggle;
332
296
  toggle.setAttribute("aria-expanded", "true");
333
297
  toggle.setAttribute("aria-controls", "lane-" + lane.status);
334
298
  toggle.id = "lane-head-" + lane.status;
335
- head.appendChild(el("span", "board-lane__dot"));
299
+ head.appendChild(el("span", N.laneDot));
336
300
  head.appendChild(toggle);
337
- head.appendChild(el("span", "board-lane__count"));
301
+ head.appendChild(el("span", N.laneCount));
338
302
  section.appendChild(head);
339
- var list = el("ol", "board-lane__list");
303
+ var list = el("ol", N.laneList);
340
304
  list.id = "lane-" + lane.status;
341
305
  section.appendChild(list);
342
- section.appendChild(el("p", "board-lane__empty", LANE_EMPTY[lane.status] || "nothing"));
306
+ section.appendChild(el("p", N.laneEmpty, LANE_EMPTY[lane.status] || "nothing"));
343
307
  return section;
344
308
  }
345
309
 
@@ -360,6 +324,21 @@ export const BOARD_RENDER_JS = String.raw`(function () {
360
324
  var lane = view.lanes[i];
361
325
  var section = laneNode(lane);
362
326
  if (section.parentNode !== lanes) lanes.appendChild(section);
327
+ // Finished work starts folded, once: the default is what the page opens with, and
328
+ // toggleLane writes a boolean from then on, so a reader who unfolds Done keeps it
329
+ // unfolded through every frame a peer's write causes.
330
+ if (lane.status === "done" && lane.cards.length >= DONE_FOLD_AT) {
331
+ board.collapsed = board.collapsed || {};
332
+ if (board.collapsed[lane.status] === undefined) board.collapsed[lane.status] = true;
333
+ }
334
+ // A lane holding nothing is a rail: its dot, its word, and its count, in the width
335
+ // the fact deserves. The filter never does this — filtering is a view of the cards,
336
+ // not a statement that the lane has none.
337
+ section.setAttribute("data-empty", lane.cards.length === 0 ? "true" : "false");
338
+ // The lane's share of the row follows the work it holds: the sheet turns this number
339
+ // into a flex-grow, so two cards earn more width than one and a title is not wrapped
340
+ // for want of a column the lane is not using. A count, never text.
341
+ section.style.setProperty("--lane-count", String(lane.cards.length));
363
342
  var shown = lane.cards.filter(function (card) { return matches(card, query); });
364
343
  var sig = lane.cards.map(function (card) { return card.id + ":" + card.rev; }).join(",") + "|" + query;
365
344
  if (section.getAttribute("data-sig") !== sig) {
@@ -382,6 +361,10 @@ export const BOARD_RENDER_JS = String.raw`(function () {
382
361
  var collapsed = board.collapsed && board.collapsed[lane.status] === true;
383
362
  var list = section.querySelector(".board-lane__list");
384
363
  if (list) list.hidden = collapsed;
364
+ // A folded lane shows nothing, so it takes the rail's width the way an empty one
365
+ // does: the count in its head is the whole fact, and the lanes with cards keep the
366
+ // width that would otherwise be a tray of nothing.
367
+ section.setAttribute("data-folded", collapsed ? "true" : "false");
385
368
  var toggle = section.querySelector(".board-lane__toggle");
386
369
  if (toggle) toggle.setAttribute("aria-expanded", String(!collapsed));
387
370
  }
@@ -409,60 +392,104 @@ export const BOARD_RENDER_JS = String.raw`(function () {
409
392
  /** Fill the card dialog's contents from a card, without opening or closing it. */
410
393
  function fillDialog(card, view) {
411
394
  document.getElementById("card-dialog-title").textContent = card.title;
395
+ document.getElementById("card-dialog-id").textContent = "#" + card.id;
412
396
  // The lane is a status of its own, not a word inside a sentence: the same dot and
413
397
  // label a lane head wears, so the dialog and the board speak the same vocabulary.
414
398
  var state = document.getElementById("card-dialog-state");
415
399
  state.setAttribute("data-status", card.status);
416
400
  state.textContent = card.status;
417
- document.getElementById("card-dialog-meta").textContent =
418
- "#" + card.id + " · updated " + ago(card.updatedAt) + " · last edited by " + card.by;
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
419
  var owner = document.getElementById("card-dialog-owner");
420
420
  owner.textContent = "";
421
421
  owner.appendChild(card.owner ? chip(card.owner.label, "accent") : document.createTextNode("unassigned"));
422
- if (priorityOf(card) !== "normal") owner.appendChild(chip(priorityOf(card)));
423
-
424
- var pathList = document.getElementById("card-dialog-paths");
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");
425
449
  pathList.textContent = "";
426
450
  for (var i = 0; i < card.paths.length; i++) {
427
451
  var item = el("li", "");
428
- item.appendChild(chip(card.paths[i]));
452
+ var hint = chip(card.paths[i]);
453
+ hint.className += " " + N.dialogPath;
454
+ hint.title = card.paths[i];
455
+ item.appendChild(hint);
429
456
  pathList.appendChild(item);
430
457
  }
431
458
 
432
- var waits = document.getElementById("card-dialog-depends");
433
- if (waits) {
434
- var linked = (card.dependsOn || []).map(function (id) { return "#" + id; }).join(", ");
435
- waits.hidden = linked === "";
436
- waits.textContent = linked === "" ? "" : "waits on " + linked;
437
- }
438
-
439
- var plan = document.getElementById("card-dialog-plan");
440
- plan.hidden = !card.plan;
441
- plan.textContent = card.plan ? "plan " + card.plan : "";
442
-
443
459
  var notes = document.getElementById("card-dialog-notes");
444
460
  notes.textContent = "";
445
461
  for (var j = 0; j < card.notes.length; j++) {
446
462
  var note = card.notes[j];
447
- // Who and when on their own line, the sentence under it, a rule between notes:
448
- // wrapped prose used to run one note into the next with nothing to separate them.
449
- var row = el("li", "board-dialog__note");
450
- var head = el("p", "board-dialog__note-head");
451
- head.appendChild(el("span", "board-dialog__note-by", note.by));
452
- head.appendChild(el("span", "board-dialog__note-at", ago(note.at)));
453
- row.appendChild(head);
454
- row.appendChild(el("p", "board-dialog__note-text", note.text));
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);
455
471
  notes.appendChild(row);
456
472
  }
473
+ var count = document.getElementById("card-dialog-note-count");
474
+ if (count) count.textContent = card.notes.length === 0 ? " · none" : " · " + card.notes.length;
457
475
  var earlier = document.getElementById("card-dialog-earlier");
458
476
  earlier.hidden = card.notesDropped === 0;
459
477
  earlier.textContent = card.notesDropped === 0 ? "" : card.notesDropped + " earlier notes dropped";
460
478
 
461
- // A note is a write, so a viewer's dialog carries none — and its actions are what
462
- // board.may leaves, which for a viewer is Close alone.
463
- var noteComposer = document.getElementById("card-dialog-note-composer");
464
- if (noteComposer) noteComposer.hidden = !canWrite();
465
- board.fillDialogActions(card, view);
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
+ }
466
493
  }
467
494
 
468
495
  /** Paint a whole snapshot. */
@@ -479,17 +506,13 @@ export const BOARD_RENDER_JS = String.raw`(function () {
479
506
  };
480
507
 
481
508
  board.el = el;
509
+ board.typing = typing;
482
510
  board.chip = chip;
483
511
  board.actionButton = actionButton;
484
512
  board.ago = ago;
485
513
  board.findCard = findCard;
486
- board.mine = mine;
514
+ board.drawnCards = drawnCards;
487
515
  board.matches = matches;
488
- board.canWrite = canWrite;
489
- board.priorityOf = priorityOf;
490
- board.blockersOf = blockersOf;
491
- board.waitingText = waitingText;
492
- board.may = may;
493
516
  board.fillDialog = fillDialog;
494
517
  board.refreshAges = refreshAges;
495
518
  board.plural = plural;