@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
@@ -3,9 +3,20 @@
3
3
  *
4
4
  * Two panels and one strip. **Needs a decision** is the queue — one row per card,
5
5
  * carrying why it is there and the action that fixes it — and **Teammates** is the
6
- * people: the sessions this repository's bus knows, plus every owner a card names,
7
- * so a session holding nothing and a label that stopped answering are both visible.
8
- * Done is a quiet strip of ids that open their cards.
6
+ * people: the sessions this repository's bus knows, plus every owner a card names, so a
7
+ * session holding nothing and a label that stopped answering are both visible. Done is a
8
+ * quiet strip of ids that open their cards.
9
+ *
10
+ * Two of those lists only grow: an owner a card names stays one, and a finished card is
11
+ * finished for good. Both therefore fold — the owners the bus has no record of into one row
12
+ * that states what they finished, and the strip past its sixth id behind its own count — and
13
+ * the three slots are refilled only when what they print has moved, so a peer's write does
14
+ * not take the reader's scroll or their focus with it.
15
+ *
16
+ * Which owners are history, and the sentence that states them, are not decided here: the
17
+ * terminal's round prints the same fold, so both read `lib/board/round-text.ts`'s two bodies
18
+ * through {@link teamJs}. A second copy in this fragment is how the page and `/team` came to
19
+ * describe one board two ways.
9
20
  *
10
21
  * The roster is optional throughout: `view.teammates` is absent when `session-bus`
11
22
  * has no records here, and every state then reads `—` rather than claiming the
@@ -16,30 +27,90 @@
16
27
  * Same rule as the rest of the client: every value arrives as `textContent`.
17
28
  */
18
29
 
19
- export const BOARD_TEAM_JS = String.raw`(function () {
30
+ /**
31
+ * The tab's fragment, with the fold's rule and its sentence spliced in.
32
+ *
33
+ * The source crosses as text, because a page's bundle has no import graph, and the two
34
+ * `__NAME__` placeholders are rules a served script must never carry — `pages.test` fails an
35
+ * unfilled one.
36
+ */
37
+ export function teamJs(rules: { historyOwner: string; historyLine: string }): string {
38
+ return teamTemplate.replace("__HISTORY_OWNER__", rules.historyOwner).replace("__HISTORY_LINE__", rules.historyLine);
39
+ }
40
+
41
+ const teamTemplate = String.raw`(function () {
20
42
  "use strict";
21
43
  var board = window.bfBoard || (window.bfBoard = {});
22
44
  var el = board.el;
23
45
  var chip = board.chip;
24
46
  var ago = board.ago;
25
47
  var plural = board.plural;
48
+ var rankOf = board.rankOf;
49
+ var decisionTone = board.decisionTone;
50
+
51
+ __HISTORY_OWNER__
52
+
53
+ __HISTORY_LINE__
26
54
 
27
55
  /** The word a teammate's state is read as; never a colour alone. */
28
56
  var STATE_WORD = { live: "live", quiet: "quiet", gone: "gone", absent: "no record", unknown: "\u2014" };
29
57
 
30
58
  /**
31
- * A panel: its heading row, then its body.
59
+ * A slot's heading row: the panel's name and the count a reader came for.
32
60
  *
33
- * With a filter on, the count reads shown/total the way a lane head does, so a
34
- * reader can tell "nothing here" from "nothing matches".
61
+ * With a filter on, the count reads shown/total the way a lane head does, so a reader can
62
+ * tell "nothing here" from "nothing matches".
35
63
  */
36
- function panel(text, count, total) {
37
- var node = el("section", N.teamGroup);
38
- node.setAttribute("aria-label", text);
39
- var caption = el("h2", N.teamCaption, text);
40
- caption.appendChild(chip(total !== undefined && total !== count ? count + "/" + total : String(count)));
41
- node.appendChild(caption);
42
- return node;
64
+ function caption(slot, text, count, total) {
65
+ var filtered = total !== undefined && total !== count;
66
+ var head = el("h2", N.teamCaption, text);
67
+ head.appendChild(chip(filtered ? count + "/" + total : String(count)));
68
+ // The count is the reason the panel is read, but glued to its name a heading announces
69
+ // "Needs a decision 4" as one word. The label says the same fact in a sentence and the
70
+ // pill keeps the shape a reader scans.
71
+ head.setAttribute("aria-label", text + ", " + (filtered ? count + " of " + total : String(count)));
72
+ slot.setAttribute("aria-label", text);
73
+ slot.appendChild(head);
74
+ }
75
+
76
+ /**
77
+ * The tab's three slots, built once and then refilled in place.
78
+ *
79
+ * A lane has always been rebuilt only when its cards moved; the Team tab used to clear
80
+ * itself and start again on every frame, which threw away the scroll offset of a capped
81
+ * roster and put the reader's focus back on the document. A slot is a fixed element, and
82
+ * what a signature guards is its contents.
83
+ */
84
+ function slots() {
85
+ if (board.teamPanels !== undefined) return board.teamPanels;
86
+ var root = document.getElementById("board-team");
87
+ if (!root) return undefined;
88
+ root.textContent = "";
89
+ var grid = el("div", N.teamGrid);
90
+ var queue = el("section", N.teamGroup);
91
+ var people = el("section", N.teamGroup);
92
+ grid.appendChild(queue);
93
+ grid.appendChild(people);
94
+ root.appendChild(grid);
95
+ var strip = el("p", N.teamDone);
96
+ root.appendChild(strip);
97
+ board.teamPanels = { queue: queue, people: people, strip: strip };
98
+ return board.teamPanels;
99
+ }
100
+
101
+ /**
102
+ * Refill one slot, but only when what it draws has moved.
103
+ *
104
+ * The signature is the facts the slot prints — ids, revisions, states, counts — plus the
105
+ * filter and the slot's own folded state, so a frame that changed nothing here leaves the
106
+ * rows, the scroll offset, and whatever the reader has focused alone. The data-sig hook is
107
+ * the lanes' own, not a second one.
108
+ */
109
+ function fill(slot, sig, build) {
110
+ if (slot.getAttribute("data-sig") === sig) return;
111
+ slot.setAttribute("data-sig", sig);
112
+ slot.textContent = "";
113
+ build(slot);
43
114
  }
44
115
 
45
116
  /**
@@ -48,13 +119,11 @@ export const BOARD_TEAM_JS = String.raw`(function () {
48
119
  * The renderer filters this list, so it has to be the data: filtering the built
49
120
  * rows would test the element instead of the card.
50
121
  *
51
- * The round's groups overlap on purpose — a doing card untouched for half an hour
122
+ * The round's groups overlap on purpose — an executing card untouched for half an hour
52
123
  * is both idle and unplanned — so a card can arrive twice. On the page one card is
53
124
  * one row, and the strongest reason is the one it keeps: a blocker outranks silence,
54
125
  * which outranks a missing plan, which outranks nobody holding it.
55
126
  */
56
- var KIND_RANK = { blocked: 0, gone: 1, waiting: 2, idle: 3, unplanned: 4, unassigned: 5 };
57
-
58
127
  /** The labels of the sessions whose process is gone, in the roster's own spelling. */
59
128
  function goneLabels(view) {
60
129
  var roster = view.teammates;
@@ -78,19 +147,22 @@ export const BOARD_TEAM_JS = String.raw`(function () {
78
147
  if (seen === undefined) {
79
148
  byCard[card.id] = entry;
80
149
  entries.push(entry);
81
- } else if (KIND_RANK[kind] < KIND_RANK[seen.kind]) {
150
+ } else if (rankOf(kind) < rankOf(seen.kind)) {
82
151
  seen.why = entry.why;
83
152
  seen.kind = kind;
84
153
  }
85
154
  }
86
155
  }
87
- // A blocked card in this queue is a person's decision: the reason the move recorded
88
- // comes first, and the last note is only the fallback for a block nobody explained.
89
- add(round.blocked, function (card) {
90
- if (card.blockedBy) return card.blockedBy;
91
- var last = card.notes.length > 0 ? card.notes[card.notes.length - 1] : undefined;
92
- return last ? last.text : "blocked";
93
- }, "blocked");
156
+ // The card's own sentence, from the one rule: the page's faces and the transcript read
157
+ // the same body, so a block and a contract cannot be described two ways.
158
+ function why(card, fallback) {
159
+ var reason = board.reasonFor(card, ago);
160
+ return reason === undefined ? fallback : reason.text;
161
+ }
162
+ add(round.blocked, function (card) { return why(card, "blocked"); }, "blocked");
163
+ // A complete contract nobody has read is the lead's decision, and the one the flow
164
+ // stops on: the card cannot be executed until it is approved.
165
+ add(round.unapproved, function (card) { return why(card, "contract ready — needs approval"); }, "approval");
94
166
  // A dead owner is a fact, not an inference from silence: the card is anyone's now.
95
167
  var gone = goneLabels(view);
96
168
  add(
@@ -102,7 +174,7 @@ export const BOARD_TEAM_JS = String.raw`(function () {
102
174
  );
103
175
  add(round.idle, function (card) { return "no touch for " + ago(card.updatedAt); }, "idle");
104
176
  // The board card says the same words for the same fact: one voice for one state.
105
- add(round.unplanned, function () { return "no plan recorded"; }, "unplanned");
177
+ add(round.unplanned, function (card) { return why(card, "no contract yet"); }, "unplanned");
106
178
  add(round.unassigned, function () { return "nobody holds it"; }, "unassigned");
107
179
  // An order outranks nobody holding it: the claim would be refused, so the queue says
108
180
  // what it waits for instead of offering a button the rules would turn down.
@@ -126,7 +198,11 @@ export const BOARD_TEAM_JS = String.raw`(function () {
126
198
  function decision(entry, view) {
127
199
  var card = entry.card;
128
200
  var node = el("article", N.teamDecision);
201
+ // What the row is, and how it wears: data-kind names it for a test and a hook, and
202
+ // data-tone is the colour its edge is drawn in, from the table the page's stylesheet
203
+ // and the terminal share.
129
204
  node.setAttribute("data-kind", entry.kind);
205
+ node.setAttribute("data-tone", decisionTone(entry.kind));
130
206
 
131
207
  var open = el("button", N.teamOpen);
132
208
  open.type = "button";
@@ -150,16 +226,20 @@ export const BOARD_TEAM_JS = String.raw`(function () {
150
226
  // rest, because this sentence is the lead's whole reason to look at the row.
151
227
  var why = el("p", N.teamWhy, entry.why);
152
228
  why.title = entry.why;
153
- if (entry.kind === "blocked") why.className = N.teamWhy + " " + N.teamWhyBlocked;
154
229
  node.appendChild(why);
155
230
  return node;
156
231
  }
157
232
 
158
233
  /**
159
- * Every teammate: the bus's sessions and the owners a card names, as one list.
234
+ * Every teammate: the bus's sessions and the owners a card names, split in two.
160
235
  *
161
- * An owner with no live session is the interesting row — work is held by a label
162
- * nothing answers to — so it stays, with "no record" where a heartbeat would be.
236
+ * The head is who is here to read: the bus's sessions, and any owner a card names that the
237
+ * bus has no record of **and** that holds no card — that second kind is the one the queue
238
+ * reports, and folding it would hide the row the tab exists for.
239
+ *
240
+ * The tail is history: an owner the bus has no record of, holding nothing. The interesting
241
+ * case is why the row stays at all, but the set grows with every label that ever claimed a
242
+ * card, so the page states it once and unfolds it on a click.
163
243
  */
164
244
  function teammates(view, roster) {
165
245
  var round = view.round;
@@ -202,7 +282,21 @@ export const BOARD_TEAM_JS = String.raw`(function () {
202
282
  return 4;
203
283
  }
204
284
  rows.sort(function (a, b) { return rank(a) - rank(b) || a.label.localeCompare(b.label); });
205
- return rows;
285
+
286
+ var head = [];
287
+ var tail = [];
288
+ for (var r = 0; r < rows.length; r++) {
289
+ // The rule is the round text's own (isHistoryOwner), so the tab and /team cannot answer
290
+ // "which owners are only history" two ways. A null roster is a machine with no bus: the
291
+ // rule folds nobody there, because nobody can be called absent.
292
+ if (isHistoryOwner(rows[r].label, rows[r].cards.live.length, roster === null ? undefined : roster)) {
293
+ tail.push(rows[r]);
294
+ } else head.push(rows[r]);
295
+ }
296
+ // Unfolded, history is read by what it finished and then by name, so the list says which
297
+ // labels did the work rather than which sorts first.
298
+ tail.sort(function (a, b) { return b.cards.done.length - a.cards.done.length || a.label.localeCompare(b.label); });
299
+ return { head: head, tail: tail };
206
300
  }
207
301
 
208
302
  /** A text node carrying its stamp, so refreshAges rewrites it with every other age. */
@@ -254,10 +348,106 @@ export const BOARD_TEAM_JS = String.raw`(function () {
254
348
  return node;
255
349
  }
256
350
 
351
+ /**
352
+ * The history row: how many owners nothing answers to, and what they finished.
353
+ *
354
+ * One row instead of eighteen, because a label that ever claimed a card stays an owner for
355
+ * good: eighteen rows of "no record · no cards · 1 done" are one fact printed eighteen
356
+ * times. The row wears the same absence its rows do, so its dot is the hollow ring.
357
+ */
358
+ function tailRow(tail, folded) {
359
+ var finished = 0;
360
+ for (var i = 0; i < tail.length; i++) finished += tail[i].cards.done.length;
361
+ var row = el("li", N.teamMate);
362
+ row.setAttribute("data-state", "absent");
363
+ var button = el("button", N.teamTail);
364
+ button.type = "button";
365
+ button.setAttribute("data-fold", "team-roster");
366
+ button.setAttribute("aria-expanded", String(!folded));
367
+ var dot = el("span", N.teamDot);
368
+ dot.setAttribute("aria-hidden", "true");
369
+ button.appendChild(dot);
370
+ // The row counts in the rows' own words — "no record", "11 done" — so the summary is not
371
+ // a second way of saying what a teammate row says.
372
+ button.appendChild(document.createTextNode(historyLine(tail.length, finished)));
373
+ button.addEventListener("click", function () { board.toggleFold("team-roster"); });
374
+ row.appendChild(button);
375
+ return row;
376
+ }
377
+
378
+ /** Whether the history row starts folded: once, like the Done lane, and the reader decides after. */
379
+ function historyFolded(tail) {
380
+ if (tail.length < board.foldAt.roster) return false;
381
+ board.collapsed = board.collapsed || {};
382
+ if (board.collapsed["team-roster"] === undefined) board.collapsed["team-roster"] = true;
383
+ return board.collapsed["team-roster"] === true;
384
+ }
385
+
386
+ /**
387
+ * Whether the strip starts folded, and stays as the reader left it.
388
+ *
389
+ * The lane folds at six because a tray of finished cards is history the moment it reaches
390
+ * one more than a reader checks; the strip shows six ids and folds the seventh on, because
391
+ * it is one line and six is what fits before it becomes a wall of underlined numbers.
392
+ */
393
+ function doneFolded(count) {
394
+ if (count <= board.foldAt.done) return false;
395
+ board.collapsed = board.collapsed || {};
396
+ if (board.collapsed["team-done"] === undefined) board.collapsed["team-done"] = true;
397
+ return board.collapsed["team-done"] === true;
398
+ }
399
+
400
+ /** One finished id in the strip: a button, because it opens its card. */
401
+ function doneId(card) {
402
+ var id = el("button", N.teamId, "#" + card.id);
403
+ id.type = "button";
404
+ id.setAttribute("aria-haspopup", "dialog");
405
+ id.addEventListener("click", function () { board.openCard(card.id); });
406
+ return id;
407
+ }
408
+
409
+ /**
410
+ * What a slot prints, in one string — the facts its rows are drawn from, plus the filter and
411
+ * the slot's own fold. A signature that leaves a fact out is a row that goes stale, so each
412
+ * one is built from the same expressions the rows read.
413
+ */
414
+ function queueSig(decisions, total, query) {
415
+ var parts = [query, String(total)];
416
+ for (var i = 0; i < decisions.length; i++) {
417
+ var card = decisions[i].card;
418
+ parts.push(card.id + ":" + card.rev + ":" + decisions[i].kind + ":" + decisions[i].why);
419
+ }
420
+ return parts.join("|");
421
+ }
422
+
423
+ function peopleSig(head, tail, query, folded, noBus) {
424
+ function row(mate) {
425
+ var ids = [];
426
+ for (var i = 0; i < mate.cards.live.length; i++) ids.push(mate.cards.live[i].id + ":" + mate.cards.live[i].rev);
427
+ // No seen: the bus moves it every few seconds, and it is churn without a fact. A live row
428
+ // prints no age — only a quiet one does, and a quiet session that pings is live again on the
429
+ // next snapshot, which moves its state and refills the row anyway. Putting the stamp here
430
+ // rebuilt the roster on the first frame after every heartbeat, taking the reader's scroll
431
+ // offset and focus with it, which is the one thing this signature exists to prevent.
432
+ return mate.label + "," + mate.state + "," + mate.mail + "," + mate.listening +
433
+ "," + (mate.you ? 1 : 0) + "," + ids.join("+") + "," + mate.cards.done.length;
434
+ }
435
+ var parts = [query, folded ? "folded" : "open", noBus ? "no bus" : "bus"];
436
+ for (var i = 0; i < head.length; i++) parts.push(row(head[i]));
437
+ parts.push("--");
438
+ for (var j = 0; j < tail.length; j++) parts.push(row(tail[j]));
439
+ return parts.join("|");
440
+ }
441
+
442
+ function stripSig(done, filtering, folded, total) {
443
+ var ids = [];
444
+ for (var i = 0; i < done.length; i++) ids.push(done[i].id);
445
+ return ids.join(",") + "|" + (filtering ? "filtered" : "all") + "|" + (folded ? "folded" : "open") + "|" + total;
446
+ }
447
+
257
448
  board.renderTeam = function (view) {
258
- var root = document.getElementById("board-team");
259
- if (!root) return;
260
- root.textContent = "";
449
+ var shell = slots();
450
+ if (shell === undefined) return;
261
451
  var round = view.round;
262
452
  // Absent, not empty: null means no bus here, so every state reads unknown.
263
453
  var roster = view.teammates === undefined ? null : view.teammates;
@@ -268,60 +458,91 @@ export const BOARD_TEAM_JS = String.raw`(function () {
268
458
  var filtering = query !== "";
269
459
  var noMatch = "no card here matches \"" + query + "\"";
270
460
 
271
- var grid = el("div", N.teamGrid);
272
-
273
461
  var all = needs(view);
274
462
  var decisions = all.filter(function (entry) { return keep(entry.card); });
275
- var queue = panel("Needs a decision", decisions.length, all.length);
276
- if (decisions.length === 0) {
277
- queue.appendChild(el("p", N.teamEmpty, filtering ? noMatch : "nothing needs you: no blockers, nothing idle, every card planned and held"));
278
- } else {
463
+ fill(shell.queue, queueSig(decisions, all.length, query), function (slot) {
464
+ caption(slot, "Needs a decision", decisions.length, all.length);
465
+ if (decisions.length === 0) {
466
+ slot.appendChild(el("p", N.teamEmpty, filtering ? noMatch : "nothing needs you: no blockers, no contract waiting to be read, nothing idle"));
467
+ return;
468
+ }
279
469
  var list = el("div", N.teamQueue);
280
470
  for (var i = 0; i < decisions.length; i++) list.appendChild(decision(decisions[i], view));
281
- queue.appendChild(list);
282
- }
283
- grid.appendChild(queue);
471
+ slot.appendChild(list);
472
+ });
284
473
 
285
474
  var everyone = teammates(view, roster);
286
- var shown = everyone.filter(function (m) {
475
+ var keepRow = function (mate) {
287
476
  if (!filtering) return true;
288
- if (matchesLabel(m.label)) return true;
289
- var held = m.cards.live.concat(m.cards.done);
477
+ if (matchesLabel(mate.label)) return true;
478
+ var held = mate.cards.live.concat(mate.cards.done);
290
479
  for (var k = 0; k < held.length; k++) if (keep(held[k])) return true;
291
480
  return false;
481
+ };
482
+ var head = everyone.head.filter(keepRow);
483
+ var history = everyone.tail.filter(keepRow);
484
+ var people = everyone.head.length + everyone.tail.length;
485
+ // A filter is a view of the rows, so it unfolds the history: a matching owner hidden
486
+ // inside a fold would make the panel say "nothing matches" about somebody who is here.
487
+ var folded = filtering ? false : historyFolded(everyone.tail);
488
+ fill(shell.people, peopleSig(head, history, query, folded, roster === null), function (slot) {
489
+ caption(slot, "Teammates", head.length + history.length, people);
490
+ if (head.length + history.length === 0) {
491
+ slot.appendChild(el("p", N.teamEmpty, filtering ? noMatch : roster === null ? "session-bus is not loaded here" : "nobody is here yet"));
492
+ return;
493
+ }
494
+ var list = el("ul", N.teamRoster);
495
+ for (var i = 0; i < head.length; i++) list.appendChild(mate(head[i]));
496
+ if (history.length > 0) {
497
+ // While a filter asks for people by name, the matches are rows like any other; unfolded,
498
+ // they follow the one row that states them.
499
+ if (!filtering) {
500
+ list.appendChild(tailRow(history, folded));
501
+ if (folded) {
502
+ slot.appendChild(list);
503
+ return;
504
+ }
505
+ }
506
+ for (var j = 0; j < history.length; j++) list.appendChild(mate(history[j]));
507
+ }
508
+ slot.appendChild(list);
292
509
  });
293
- var people = panel("Teammates", shown.length, everyone.length);
294
- if (shown.length === 0) {
295
- people.appendChild(el("p", N.teamEmpty, filtering ? noMatch : roster === null ? "session-bus is not loaded here" : "nobody is here yet"));
296
- } else {
297
- var rosterList = el("ul", N.teamRoster);
298
- for (var j = 0; j < shown.length; j++) rosterList.appendChild(mate(shown[j]));
299
- people.appendChild(rosterList);
300
- }
301
- grid.appendChild(people);
302
- root.appendChild(grid);
303
510
 
304
511
  var done = round.done.filter(keep);
305
- var strip = el("p", N.teamDone);
306
- if (done.length === 0) {
307
- // While filtering, a third "no card here matches" line adds nothing; the two
308
- // panels above already said it.
309
- if (filtering) return root;
310
- strip.textContent = "nothing finished yet";
311
- } else {
312
- strip.appendChild(document.createTextNode("Done \u00B7 "));
313
- for (var m = 0; m < done.length; m++) {
314
- (function (card) {
315
- var id = el("button", N.teamId, "#" + card.id);
316
- id.type = "button";
317
- id.setAttribute("aria-haspopup", "dialog");
318
- id.addEventListener("click", function () { board.openCard(card.id); });
319
- strip.appendChild(id);
320
- })(done[m]);
512
+ var stripFolded = doneFolded(done.length);
513
+ fill(shell.strip, stripSig(done, filtering, stripFolded, round.done.length), function (slot) {
514
+ if (done.length === 0) {
515
+ // While filtering, a third "no card here matches" line adds nothing; the two panels
516
+ // above already said it.
517
+ if (!filtering) slot.textContent = "nothing finished yet";
518
+ return;
321
519
  }
322
- strip.appendChild(document.createTextNode(" \u00B7 " + plural(done.length, "card") +
323
- (filtering && done.length !== round.done.length ? " of " + round.done.length : "")));
324
- }
325
- root.appendChild(strip);
520
+ slot.appendChild(document.createTextNode("Done \u00B7 "));
521
+ var shown = Math.min(done.length, board.foldAt.done);
522
+ for (var i = 0; i < shown; i++) slot.appendChild(doneId(done[i]));
523
+ if (done.length > board.foldAt.done) {
524
+ var rest = el("span", N.teamDoneIds);
525
+ rest.id = "team-done-ids";
526
+ rest.hidden = stripFolded;
527
+ for (var j = board.foldAt.done; j < done.length; j++) rest.appendChild(doneId(done[j]));
528
+ slot.appendChild(rest);
529
+ }
530
+ slot.appendChild(document.createTextNode(" \u00B7 "));
531
+ var count = plural(done.length, "card") +
532
+ (filtering && done.length !== round.done.length ? " of " + round.done.length : "");
533
+ if (done.length > board.foldAt.done) {
534
+ // The count is the fold's control here, the way a lane's head is its own: the strip has
535
+ // no room for a second button, and the number is what says how much is out of sight.
536
+ var toggle = el("button", N.teamFold, count);
537
+ toggle.type = "button";
538
+ toggle.setAttribute("data-fold", "team-done");
539
+ toggle.setAttribute("aria-expanded", String(!stripFolded));
540
+ toggle.setAttribute("aria-controls", "team-done-ids");
541
+ toggle.addEventListener("click", function () { board.toggleFold("team-done"); });
542
+ slot.appendChild(toggle);
543
+ } else {
544
+ slot.appendChild(document.createTextNode(count));
545
+ }
546
+ });
326
547
  };
327
548
  })();`;
@@ -114,7 +114,7 @@ export function resultFallback(
114
114
  if (!text) return "";
115
115
  const lines = text.split("\n");
116
116
  const shown = expanded ? lines : lines.slice(0, FALLBACK_PREVIEW_LINES);
117
- let output = theme.fg("toolOutput", shown.join("\n"));
117
+ let output = paint(theme, "toolOutput", shown.join("\n"));
118
118
  const remaining = lines.length - shown.length;
119
119
  if (remaining > 0) output += `\n${paint(theme, "meta", `... (${remaining} more lines)`)}`;
120
120
  return output;
@@ -12,7 +12,7 @@ import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works
12
12
  import type { WebSearchProviderEvent } from "../../../lib/events.ts";
13
13
  import { PRICE_VERIFIED_AT } from "../footer/price/config.ts";
14
14
  import { readJsonResult } from "../../../lib/config.ts";
15
- import { isExtensionEnabled } from "../../../lib/env.ts";
15
+ import { extensionDisabledReason } from "../../../lib/env.ts";
16
16
  import {
17
17
  hardRequirementExtensions,
18
18
  probeAll as probeDeclared,
@@ -91,7 +91,10 @@ export function createDoctorRuntime(
91
91
  readJson: readJsonResult,
92
92
  dirs: { agentDir: getAgentDir(), cwd, configDirName: CONFIG_DIR_NAME },
93
93
  platform: process.platform,
94
- isEnabled: (extension) => isExtensionEnabled(extension) && !config.skip.includes(extension),
94
+ // The doctor's own skip list is this report's decision, not the environment's,
95
+ // so it answers for the extension it holds back.
96
+ disabledReason: (extension) =>
97
+ config.skip.includes(extension) ? "skipped by `doctor.skip`" : extensionDisabledReason(extension),
95
98
  notify: notifySeam(cwd),
96
99
  isTTY: process.stdout.isTTY === true,
97
100
  searchProvider: () => searchProvider,
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Following this session's roster label, for the bar.
3
+ *
4
+ * The footer renders on a frame timer, so the label cannot be a bus read per frame. It is
5
+ * one read of the record the bus wrote — a resumed pane can already have one — and a
6
+ * subscription to `SESSION_LABEL_CHANNEL` for every label the bus settles afterwards. Both
7
+ * report through the sink the footer handed in, so the renderer reads one field and the I/O
8
+ * happens once.
9
+ *
10
+ * No record is the ordinary answer, not a failure: bytefly is on by default and the bus is
11
+ * opt-in, so a session that never joined keeps Pi's own name.
12
+ *
13
+ * Its caller loads this module on the first footer install rather than at registration,
14
+ * because the read needs the bus's layout on disk and the registration graph is a budget in
15
+ * `test/dependencies.test.ts` that may only fall.
16
+ */
17
+
18
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
+ import { SESSION_LABEL_CHANNEL, type SessionLabelEvent, sessionIdOf } from "../../../../lib/session-label.ts";
20
+ import { readSessionLabel } from "../../../../lib/session-roster.ts";
21
+
22
+ /** What to draw: the label, or `null` for "no record, keep Pi's session name". */
23
+ export type RosterLabelSink = (label: string | null) => void;
24
+
25
+ /**
26
+ * Read this session's label once, then report every label the bus publishes for it.
27
+ *
28
+ * The subscription is here rather than beside the footer's other listeners because the
29
+ * channel and the record walk are one contract, and only this module is deferred. It is
30
+ * never unsubscribed: Pi drops an extension's listeners when it unloads, and a footer that
31
+ * re-installs (a session tree change) is the same session under the same label.
32
+ *
33
+ * Answers when the record has been read, so a caller can wait for the first frame that
34
+ * draws a label rather than guess at a timer.
35
+ */
36
+ export function followRosterLabel(input: {
37
+ pi: Pick<ExtensionAPI, "events">;
38
+ ctx: Pick<ExtensionContext, "sessionManager">;
39
+ agentDir: string;
40
+ sink: RosterLabelSink;
41
+ }): Promise<void> {
42
+ const { pi, ctx, agentDir, sink } = input;
43
+ const own = sessionIdOf(ctx);
44
+ if (own === undefined) return Promise.resolve();
45
+ pi.events.on(SESSION_LABEL_CHANNEL, (event) => {
46
+ // The channel reaches every session in this process, and only one of them is this one:
47
+ // a peer's label would put its name on this session's bar.
48
+ const published = event as SessionLabelEvent | undefined;
49
+ if (published === undefined || published.id !== own) return;
50
+ sink(published.label);
51
+ });
52
+ // The record the bus wrote before this process started, if any. A failure is the same
53
+ // answer as no record: the bar keeps Pi's own name.
54
+ return readSessionLabel(agentDir, own).then(
55
+ (label) => {
56
+ if (label !== undefined) sink(label);
57
+ },
58
+ () => {},
59
+ );
60
+ }
@@ -42,7 +42,11 @@ export interface SnapshotReader {
42
42
  peek(): PricedUsage | undefined;
43
43
  }
44
44
 
45
- export function createSnapshotReader(config: ByteflyConfig, delegation: DelegationLedger): SnapshotReader {
45
+ export function createSnapshotReader(
46
+ config: ByteflyConfig,
47
+ delegation: DelegationLedger,
48
+ rosterLabel: () => string | null = () => null,
49
+ ): SnapshotReader {
46
50
  const price = createPricer(config);
47
51
  let key: string | null = null;
48
52
  let usage: PricedUsage = { ...zeroUsage(), unpriced: 0, unpricedModels: [] };
@@ -90,6 +94,9 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
90
94
  return {
91
95
  cwd: ctx.cwd,
92
96
  sessionName,
97
+ // Read every frame, because the bus settles a label without the session moving: a
98
+ // cached field, not a read — nothing here touches the bus.
99
+ rosterLabel: rosterLabel(),
93
100
  branch: footerData.getGitBranch(),
94
101
  statuses: footerData.getExtensionStatuses(),
95
102
  model,
@@ -28,17 +28,16 @@ export const CONFIG = {
28
28
  branch: GLYPHS.branch,
29
29
  detached: GLYPHS.detached,
30
30
  worktree: GLYPHS.worktree,
31
- board: GLYPHS.board,
32
- browse: GLYPHS.browse,
31
+ http: GLYPHS.http,
33
32
  delegation: GLYPHS.delegation,
33
+ autoMode: GLYPHS.autoMode,
34
+ planMode: GLYPHS.plan,
34
35
  },
35
36
  /** Status key the worktree extension uses; routed to line 1's right zone. */
36
37
  worktreeStatusKey: STATUS_KEYS.worktree,
37
- /** Status key the board extension publishes its page port under. */
38
- boardStatusKey: STATUS_KEYS.board,
39
- /** Status key the file-browse extension uses; routed to line 1's right zone. */
40
- browseStatusKey: STATUS_KEYS.browse,
41
- /** Status key whose ` · detail` suffix (the plan file name) is dropped. */
38
+ /** Status key `serve` publishes the session's one page port under. */
39
+ httpStatusKey: STATUS_KEYS.http,
40
+ /** Status key whose chip is the plan state; the mode segment renders it. */
42
41
  planStatusKey: STATUS_KEYS.planMode,
43
42
  /**
44
43
  * Status keys that are alerts rather than modes, and so are never dropped.
@@ -46,7 +45,7 @@ export const CONFIG = {
46
45
  * once its chip is off the bar.
47
46
  */
48
47
  alertStatusKeys: ALERT_STATUS_KEYS,
49
- labels: { noModel: "no model", detached: "detached" },
48
+ labels: { noModel: "no model", detached: "detached", autoMode: "auto", planMode: "plan" },
50
49
  /** Minimum gap kept between the left and right zones. */
51
50
  minGap: 2,
52
51
  /** Worktree label length before the compact form truncates it. */