@gavin-hu/my-pi-agent 0.18.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 (324) hide show
  1. package/README.md +66 -52
  2. package/README.zh-CN.md +56 -46
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +507 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +561 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +298 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +254 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
  25. package/extensions/board/commands.ts +105 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +170 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +388 -0
  32. package/extensions/board/result.ts +108 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +90 -0
  35. package/extensions/board/seat.ts +28 -0
  36. package/extensions/board/snapshot.ts +42 -0
  37. package/extensions/board/tools.ts +444 -0
  38. package/extensions/board/transport/router.ts +91 -0
  39. package/extensions/board/transport/server.ts +138 -0
  40. package/extensions/board/types.ts +121 -0
  41. package/extensions/board/view/client.ts +51 -0
  42. package/extensions/board/view/dialog.ts +43 -0
  43. package/extensions/board/view/keys.ts +125 -0
  44. package/extensions/board/view/live.ts +231 -0
  45. package/extensions/board/view/names.ts +91 -0
  46. package/extensions/board/view/page.ts +118 -0
  47. package/extensions/board/view/render.ts +519 -0
  48. package/extensions/board/view/styles.ts +228 -0
  49. package/extensions/board/view/team.ts +327 -0
  50. package/extensions/bytefly/footer/budget.ts +6 -5
  51. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  52. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  53. package/extensions/bytefly/footer/config.ts +19 -8
  54. package/extensions/bytefly/footer/price/price.ts +30 -8
  55. package/extensions/bytefly/footer/register.ts +5 -5
  56. package/extensions/bytefly/footer/render/format.ts +18 -0
  57. package/extensions/bytefly/footer/render/lines.ts +23 -7
  58. package/extensions/bytefly/footer/render/segments.ts +117 -31
  59. package/extensions/bytefly/footer/types.ts +29 -5
  60. package/extensions/bytefly/footer/unpriced.ts +42 -17
  61. package/extensions/bytefly/panel/commands.ts +67 -7
  62. package/extensions/bytefly/panel/filter.ts +68 -0
  63. package/extensions/bytefly/panel/format.ts +16 -15
  64. package/extensions/bytefly/panel/join.ts +56 -13
  65. package/extensions/bytefly/panel/prices.ts +43 -0
  66. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  67. package/extensions/bytefly/panel/settings.ts +75 -43
  68. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  69. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  70. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  71. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  72. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  73. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  74. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  75. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  76. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  77. package/extensions/bytefly/panel/tui.ts +306 -259
  78. package/extensions/bytefly/picker/discovery.ts +0 -2
  79. package/extensions/bytefly/picker/types.ts +0 -2
  80. package/extensions/bytefly/update/check.ts +39 -0
  81. package/extensions/bytefly/update/format.ts +15 -0
  82. package/extensions/bytefly/update/register.ts +73 -34
  83. package/extensions/bytefly/update/state.ts +6 -3
  84. package/extensions/check/commands.ts +2 -1
  85. package/extensions/context/tui.ts +1 -1
  86. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  87. package/extensions/file-browse/files/classify.ts +136 -0
  88. package/extensions/file-browse/files/files.ts +12 -0
  89. package/extensions/file-browse/files/index.ts +90 -0
  90. package/extensions/file-browse/files/list.ts +261 -0
  91. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  92. package/extensions/file-browse/files/tree.ts +101 -0
  93. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  94. package/extensions/file-browse/index.ts +4 -4
  95. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  96. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
  97. package/extensions/file-browse/transport/server.ts +110 -0
  98. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  99. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  100. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  101. package/extensions/file-browse/view/emit.ts +51 -0
  102. package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
  103. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  104. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
  105. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
  106. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  107. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  108. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  109. package/extensions/file-read/schema.ts +2 -1
  110. package/extensions/job/runtime.ts +4 -4
  111. package/extensions/memory/store.ts +12 -5
  112. package/extensions/plan/blocks.ts +23 -2
  113. package/extensions/plan/commands.ts +21 -16
  114. package/extensions/plan/feedback.ts +185 -52
  115. package/extensions/plan/format.ts +10 -3
  116. package/extensions/plan/index.ts +23 -5
  117. package/extensions/plan/plan-files.ts +88 -0
  118. package/extensions/plan/plan-index.ts +43 -9
  119. package/extensions/plan/plan-text.ts +33 -23
  120. package/extensions/plan/plans.ts +27 -34
  121. package/extensions/plan/policy.ts +30 -17
  122. package/extensions/plan/review.ts +3 -27
  123. package/extensions/plan/runtime.ts +52 -11
  124. package/extensions/plan/sections.ts +112 -0
  125. package/extensions/plan/steps.ts +3 -22
  126. package/extensions/plan/todos.ts +51 -3
  127. package/extensions/plan/tools/exit.ts +20 -95
  128. package/extensions/plan/tools/todo-seed.ts +4 -2
  129. package/extensions/plan/tools/write.ts +12 -1
  130. package/extensions/plan/tui.ts +5 -34
  131. package/extensions/plan/types.ts +15 -8
  132. package/extensions/plan/web-blocks.ts +89 -0
  133. package/extensions/plan/web-chrome.ts +3 -5
  134. package/extensions/plan/web-client.ts +179 -330
  135. package/extensions/plan/web-collab.ts +3 -3
  136. package/extensions/plan/web-comments.ts +18 -8
  137. package/extensions/plan/web-composer.ts +80 -0
  138. package/extensions/plan/web-draft.ts +40 -0
  139. package/extensions/plan/web-keys.ts +134 -0
  140. package/extensions/plan/web-outline.ts +192 -0
  141. package/extensions/plan/web-page.ts +4 -8
  142. package/extensions/plan/web-plan.ts +150 -0
  143. package/extensions/plan/web-router.ts +3 -58
  144. package/extensions/plan/web-settle.ts +194 -0
  145. package/extensions/plan/web-share-session.ts +24 -30
  146. package/extensions/plan/web-streams.ts +14 -14
  147. package/extensions/plan/web-tokens.ts +2 -2
  148. package/extensions/plan/web.ts +66 -120
  149. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  150. package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
  151. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  152. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  153. package/extensions/rewind/index.ts +3 -3
  154. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  155. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  156. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  157. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  158. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  159. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  160. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  161. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  162. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  163. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  164. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  165. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  166. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  167. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  168. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  169. package/extensions/session-bus/commands.ts +17 -69
  170. package/extensions/session-bus/format.ts +22 -45
  171. package/extensions/session-bus/index.ts +13 -7
  172. package/extensions/session-bus/join-request.ts +40 -0
  173. package/extensions/session-bus/mail-watch.ts +134 -0
  174. package/extensions/session-bus/notice.ts +32 -4
  175. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  176. package/extensions/session-bus/protocol.ts +19 -49
  177. package/extensions/session-bus/resolve.ts +67 -0
  178. package/extensions/session-bus/result.ts +16 -51
  179. package/extensions/session-bus/rows.ts +11 -5
  180. package/extensions/session-bus/runtime.ts +220 -267
  181. package/extensions/session-bus/store.ts +55 -7
  182. package/extensions/session-bus/tools.ts +31 -66
  183. package/extensions/session-bus/tui.ts +5 -5
  184. package/extensions/session-bus/types.ts +23 -14
  185. package/extensions/subagent/agents.ts +9 -8
  186. package/extensions/subagent/commands.ts +2 -1
  187. package/extensions/subagent/invocation.ts +4 -2
  188. package/extensions/team/commands.ts +145 -0
  189. package/extensions/team/dispatch.ts +133 -0
  190. package/extensions/team/format.ts +119 -0
  191. package/extensions/team/index.ts +34 -0
  192. package/extensions/team/schema.ts +29 -0
  193. package/extensions/team/tools.ts +267 -0
  194. package/extensions/team/types.ts +65 -0
  195. package/extensions/todo/index.ts +4 -1
  196. package/extensions/todo/nudge.ts +8 -2
  197. package/extensions/web-access/fetch/page.ts +4 -1
  198. package/extensions/web-access/fetch/structured.ts +447 -0
  199. package/extensions/web-access/fetch/tool.ts +2 -1
  200. package/extensions/web-access/search/tool.ts +2 -1
  201. package/extensions/wechat/bridge.ts +106 -251
  202. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  203. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  204. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  205. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  206. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  207. package/extensions/wechat/dialog/channels.ts +136 -0
  208. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  209. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  210. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  211. package/extensions/wechat/index.ts +10 -10
  212. package/extensions/wechat/intake.ts +245 -0
  213. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  214. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  215. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  216. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  217. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  218. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  219. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  220. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  221. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  222. package/extensions/wechat/tools.ts +29 -12
  223. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  224. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  225. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  226. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  227. package/extensions/wechat/transport/outbound.ts +142 -0
  228. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  229. package/extensions/wechat/types.ts +10 -1
  230. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  231. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  232. package/extensions/worktree/commands.ts +24 -17
  233. package/extensions/worktree/exit.ts +12 -6
  234. package/extensions/worktree/git.ts +22 -14
  235. package/extensions/worktree/guard.ts +18 -290
  236. package/extensions/worktree/handoff.ts +10 -2
  237. package/extensions/worktree/index.ts +2 -1
  238. package/extensions/worktree/lifecycle.ts +2 -0
  239. package/extensions/worktree/registry.ts +20 -11
  240. package/extensions/worktree/root-tools.ts +13 -3
  241. package/extensions/worktree/runtime.ts +8 -2
  242. package/extensions/worktree/screen.ts +15 -8
  243. package/extensions/worktree/shell.ts +303 -0
  244. package/extensions/worktree/state.ts +1 -1
  245. package/extensions/worktree/status.ts +42 -9
  246. package/extensions/worktree/tools.ts +19 -13
  247. package/lib/README.md +11 -8
  248. package/lib/board/card.ts +479 -0
  249. package/lib/board/links.ts +141 -0
  250. package/lib/board/page-pointer.ts +310 -0
  251. package/lib/board/round-text.ts +81 -0
  252. package/lib/board/round.ts +94 -0
  253. package/lib/board/rules.ts +437 -0
  254. package/lib/board/session.ts +105 -0
  255. package/lib/board/store.ts +497 -0
  256. package/lib/board/view.ts +234 -0
  257. package/lib/errors.ts +12 -4
  258. package/lib/format.ts +35 -0
  259. package/lib/http/guard.ts +112 -0
  260. package/lib/http/index.ts +16 -5
  261. package/lib/http/serve.ts +237 -0
  262. package/lib/http/server.ts +12 -10
  263. package/lib/http/sink.ts +135 -0
  264. package/lib/http/token.ts +38 -0
  265. package/lib/plan-entry.ts +11 -0
  266. package/lib/policy.ts +25 -2
  267. package/lib/requirements.ts +6 -7
  268. package/lib/session-label.ts +64 -0
  269. package/lib/session-roster.ts +201 -0
  270. package/lib/session-tool.ts +184 -0
  271. package/lib/spawn.ts +37 -3
  272. package/lib/stray-fields.ts +45 -0
  273. package/lib/tool-names.ts +38 -2
  274. package/lib/tui/glyphs.ts +3 -2
  275. package/lib/web/README.md +57 -4
  276. package/lib/web/client-runtime.ts +123 -0
  277. package/lib/web/components.ts +5 -0
  278. package/lib/web/index.ts +4 -2
  279. package/lib/web/markup.ts +88 -1
  280. package/lib/web/names.ts +42 -0
  281. package/lib/web/runtime.ts +7 -1
  282. package/lib/web/tokens.ts +25 -2
  283. package/package.json +26 -14
  284. package/skills/next/SKILL.md +4 -3
  285. package/skills/project-docs/SKILL.md +133 -0
  286. package/skills/project-docs/references/audit.md +131 -0
  287. package/skills/project-docs/references/changelog.md +105 -0
  288. package/skills/project-docs/references/discovery.md +127 -0
  289. package/skills/project-docs/references/readme.md +80 -0
  290. package/skills/project-docs/references/release.md +84 -0
  291. package/skills/project-docs/references/roadmap.md +93 -0
  292. package/skills/project-docs/scripts/state.ts +316 -0
  293. package/skills/refactor/SKILL.md +100 -125
  294. package/skills/refactor/references/handoff.md +35 -0
  295. package/skills/refactor/references/signals.md +54 -0
  296. package/skills/refactor/scripts/inventory.ts +587 -0
  297. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  298. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  299. package/extensions/file-browse/README.md +0 -506
  300. package/extensions/file-browse/files.ts +0 -564
  301. package/extensions/file-browse/server.ts +0 -141
  302. package/extensions/job/process.ts +0 -35
  303. package/extensions/plan/split.ts +0 -87
  304. package/extensions/session-bus/README.md +0 -247
  305. package/extensions/session-bus/team.ts +0 -102
  306. package/extensions/wechat/outbound.ts +0 -100
  307. package/lib/plans/sections.ts +0 -172
  308. package/lib/plans/setting.ts +0 -52
  309. package/lib/plans/split.ts +0 -215
  310. package/lib/plans/store.ts +0 -19
  311. package/lib/plans/team.ts +0 -151
  312. package/skills/changelog/SKILL.md +0 -148
  313. package/skills/parallel-dev/SKILL.md +0 -150
  314. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  315. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  316. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  317. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  318. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  319. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  320. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  321. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  322. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  323. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  324. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -0,0 +1,352 @@
1
+ /**
2
+ * The control center's key table: the binding, the hint row it appears in, and
3
+ * the help block `?` prints.
4
+ *
5
+ * One table, so a help line cannot drift from a binding: the component dispatches
6
+ * through {@link keyFor}, the hint row is {@link hintsFor}, and the help block is
7
+ * {@link helpRows}. Movement is not here — `lib/tui/cursor.ts` owns the arrows,
8
+ * `j`/`k`, PgUp/PgDn and Home/End for every screen in the package — so the help
9
+ * block names movement in its own row and the table stays the actions this screen
10
+ * alone defines.
11
+ *
12
+ * Value-only: pure functions, the theme passed in.
13
+ */
14
+
15
+ import type { Theme } from "@earendil-works/pi-coding-agent";
16
+ import { Key, matchesKey } from "@earendil-works/pi-tui";
17
+ import type { ScreenHint } from "../../../../lib/tui/chrome.ts";
18
+ import { clip, padTo } from "../../../../lib/tui/columns.ts";
19
+ import { isCloseKey } from "../../../../lib/tui/cursor.ts";
20
+ import { BODY_INDENT } from "../../../../lib/tui/indent.ts";
21
+ import { paint } from "../../../../lib/tui/tokens.ts";
22
+ import type { PanelSection } from "../types.ts";
23
+ import { hintLine, noticeLine } from "./frame.ts";
24
+ import { editHints } from "./settings-view.ts";
25
+
26
+ /** One action the screen binds. */
27
+ export type PanelKeyId =
28
+ | "close"
29
+ | "previousSection"
30
+ | "nextSection"
31
+ | "toggle"
32
+ | "change"
33
+ | "detail"
34
+ | "scope"
35
+ | "filter"
36
+ | "attention"
37
+ | "refresh"
38
+ | "reload"
39
+ | "reset"
40
+ | "json"
41
+ | "inherit"
42
+ | "help";
43
+
44
+ /** One binding, its hint label, and what the help block says about it. */
45
+ export interface PanelKey {
46
+ id: PanelKeyId;
47
+ /** The keys as the hint row and the help block print them. */
48
+ label: string;
49
+ /** The sections that offer it. A key absent from a section is not hinted there. */
50
+ sections: readonly PanelSection[];
51
+ /** One plain sentence for the help block. */
52
+ help: string;
53
+ /** Whether `data` is this key. */
54
+ match(data: string): boolean;
55
+ }
56
+
57
+ /** Every section, and the two resource lists that share a set of keys. */
58
+ const ALL: readonly PanelSection[] = ["extensions", "skills", "settings"];
59
+ const LISTS: readonly PanelSection[] = ["extensions", "skills"];
60
+
61
+ /** Shift+Tab arrives as this sequence on terminals without the Kitty protocol. */
62
+ const SHIFT_TAB = "\u001b[Z";
63
+
64
+ /** The screen's keys, in help order. */
65
+ export const PANEL_KEYS: readonly PanelKey[] = [
66
+ {
67
+ id: "close",
68
+ label: "Esc",
69
+ sections: ALL,
70
+ help: "Close the screen",
71
+ match: (data) => isCloseKey(data),
72
+ },
73
+ {
74
+ id: "previousSection",
75
+ label: "←",
76
+ sections: ALL,
77
+ help: "Previous section",
78
+ match: (data) =>
79
+ matchesKey(data, Key.left) || data === "h" || matchesKey(data, Key.shift("tab")) || data === SHIFT_TAB,
80
+ },
81
+ {
82
+ id: "nextSection",
83
+ label: "→",
84
+ sections: ALL,
85
+ help: "Next section",
86
+ match: (data) => matchesKey(data, Key.right) || data === "l" || matchesKey(data, Key.tab),
87
+ },
88
+ {
89
+ id: "toggle",
90
+ label: "Space",
91
+ sections: LISTS,
92
+ help: "Enable or disable, and re-probe when it changes",
93
+ match: (data) => matchesKey(data, Key.space),
94
+ },
95
+ {
96
+ id: "detail",
97
+ label: "Enter",
98
+ sections: LISTS,
99
+ help: "Show or hide the focused row's findings",
100
+ match: (data) => matchesKey(data, Key.enter),
101
+ },
102
+ {
103
+ id: "change",
104
+ label: "Space",
105
+ sections: ["settings"],
106
+ help: "Change the focused value",
107
+ match: (data) => matchesKey(data, Key.space) || matchesKey(data, Key.enter),
108
+ },
109
+ {
110
+ id: "reset",
111
+ label: "u",
112
+ sections: ["settings"],
113
+ help: "Reset the focused value to its default",
114
+ match: (data) => data === "u",
115
+ },
116
+ {
117
+ id: "json",
118
+ label: "J",
119
+ sections: ["settings"],
120
+ help: "Show the whole `bytefly.json` section the value lives in",
121
+ match: (data) => data === "J",
122
+ },
123
+ {
124
+ id: "scope",
125
+ label: "s",
126
+ sections: ALL,
127
+ help: "Switch where a change is written: Global or Project",
128
+ match: (data) => data === "s",
129
+ },
130
+ {
131
+ id: "filter",
132
+ label: "/",
133
+ sections: ALL,
134
+ help: "Filter the rows by text; Esc clears it",
135
+ match: (data) => matchesKey(data, Key.slash),
136
+ },
137
+ {
138
+ id: "attention",
139
+ label: "!",
140
+ sections: ALL,
141
+ help: "Show only the rows that need attention",
142
+ match: (data) => matchesKey(data, Key.exclamation),
143
+ },
144
+ {
145
+ id: "refresh",
146
+ label: "r",
147
+ sections: ["extensions"],
148
+ help: "Re-probe every extension's requirements",
149
+ match: (data) => data === "r",
150
+ },
151
+ {
152
+ id: "inherit",
153
+ label: "x",
154
+ sections: LISTS,
155
+ help: "Drop the Project override, so Global applies again",
156
+ match: (data) => data === "x",
157
+ },
158
+ {
159
+ id: "reload",
160
+ label: "R",
161
+ sections: ALL,
162
+ help: "Reload now, to apply what you changed",
163
+ match: (data) => data === "R",
164
+ },
165
+ {
166
+ id: "help",
167
+ label: "?",
168
+ sections: ALL,
169
+ help: "Show every key, in place of the focused row's facts",
170
+ match: (data) => matchesKey(data, Key.question),
171
+ },
172
+ ];
173
+
174
+ /** The binding for `id`; a table with no row would be a bug, not a key state. */
175
+ export function keyOf(id: PanelKeyId): PanelKey {
176
+ const key = PANEL_KEYS.find((entry) => entry.id === id);
177
+ if (key === undefined) throw new Error(`No panel key "${id}"`);
178
+ return key;
179
+ }
180
+
181
+ /** The action `data` asks for in `section`, or `undefined` for anything else. */
182
+ export function keyFor(data: string, section: PanelSection): PanelKeyId | undefined {
183
+ for (const key of PANEL_KEYS) {
184
+ if (key.sections.includes(section) && key.match(data)) return key.id;
185
+ }
186
+ return undefined;
187
+ }
188
+
189
+ /** What the hint row needs to know about the screen's state. */
190
+ export interface HintState {
191
+ /** The focused row's findings are expanded (lists only). */
192
+ expanded: boolean;
193
+ /** A probe is in flight, so the refresh key is not offered. */
194
+ refreshing: boolean;
195
+ /** Project scope can hold an override, Global cannot (lists only). */
196
+ projectScope: boolean;
197
+ /** Something was changed this session, so the reload key applies. */
198
+ dirty: boolean;
199
+ /** The help block is open. */
200
+ help: boolean;
201
+ }
202
+
203
+ /** One pale, dim hint, or an accented primary one. */
204
+ function hint(text: string, primary = false): ScreenHint {
205
+ return primary ? { text, color: "accent" } : text;
206
+ }
207
+
208
+ /**
209
+ * The hint row for a section, most useful first: whole hints drop from the right
210
+ * as the width runs out, so the close and section keys survive on a narrow
211
+ * terminal and `?` is the one that leaves first.
212
+ */
213
+ export function hintsFor(section: PanelSection, state: HintState): ScreenHint[] {
214
+ const hints: ScreenHint[] = [
215
+ hint("Esc close"),
216
+ hint("←/→ section"),
217
+ section === "settings" ? hint("Space change", true) : hint("Space toggle", true),
218
+ ...(section === "settings" ? [] : [hint(state.expanded ? "Enter collapse" : "Enter detail", true)]),
219
+ hint("/ filter"),
220
+ ];
221
+ if (section === "settings") {
222
+ hints.push(hint("u reset"), hint(state.help ? "? hide" : "? keys"));
223
+ return hints;
224
+ }
225
+ hints.push(hint(state.help ? "? hide" : "? keys"));
226
+ if (section === "extensions") hints.push(hint(state.refreshing ? "refreshing…" : "r refresh"));
227
+ if (state.dirty) hints.push(hint("R reload", true));
228
+ hints.push(hint("s scope"));
229
+ if (state.projectScope) hints.push(hint("x inherit"));
230
+ hints.push(hint("↑/↓ select"));
231
+ return hints;
232
+ }
233
+
234
+ /**
235
+ * The hint row a section shows: an editor's own keys while one is open, else the
236
+ * transient notice, else the section's hints. The three cases are exclusive, so a
237
+ * notice never shares the row with a hint that could not work.
238
+ */
239
+ export function hintRow(options: {
240
+ theme: Theme;
241
+ width: number;
242
+ section: PanelSection;
243
+ state: HintState;
244
+ /** A settings row or the filter is being typed into. */
245
+ editing: boolean;
246
+ /** The last refusal or failure, shown until the next key. */
247
+ notice?: string;
248
+ }): string {
249
+ const { theme, width, section, state, editing, notice } = options;
250
+ if (editing) return hintLine(theme, width, editHints());
251
+ if (notice !== undefined) return noticeLine(theme, width, notice);
252
+ return hintLine(theme, width, hintsFor(section, state));
253
+ }
254
+
255
+ /**
256
+ * One help row: the keys it names, and what they do.
257
+ *
258
+ * A row naming one binding takes that binding's own sentence, so the two cannot
259
+ * disagree; a row that explains a verb for this section — `!` narrows settings to
260
+ * what changed, not to what is broken — states its own.
261
+ */
262
+ interface HelpRow {
263
+ /** Table bindings this row documents; empty for a row over shared movement keys. */
264
+ ids: readonly PanelKeyId[];
265
+ /** The keys as printed; joined from the bindings unless the row sets its own. */
266
+ label?: string;
267
+ /** What the keys do here; the single binding's own sentence when omitted. */
268
+ help?: string;
269
+ }
270
+
271
+ /** Rows over the movement keys, which `lib/tui/cursor.ts` owns for every screen. */
272
+ const MOVEMENT: readonly HelpRow[] = [
273
+ { ids: [], label: "↑/↓ j/k", help: "Move to the next or previous row" },
274
+ { ids: [], label: "g G", help: "Jump to the first or last row" },
275
+ { ids: [], label: "[ ]", help: "Jump to the next or previous group" },
276
+ ];
277
+
278
+ /** The help block per section: the table's own rows, plus movement and the close pair. */
279
+ const HELP: Record<PanelSection, readonly HelpRow[]> = {
280
+ extensions: [
281
+ { ids: ["close", "previousSection", "nextSection"], help: "Close, or change section" },
282
+ ...MOVEMENT,
283
+ { ids: ["toggle"], help: "Enable or disable the focused extension" },
284
+ { ids: ["detail"], help: "Show or hide its findings" },
285
+ { ids: ["attention"], help: "Show only the rows that need attention" },
286
+ { ids: ["filter"], help: "Filter the rows by text; Esc clears it" },
287
+ { ids: ["scope"], help: "Switch where a change is written" },
288
+ { ids: ["refresh"], help: "Re-probe every extension's requirements" },
289
+ { ids: ["inherit"], help: "Drop the Project override" },
290
+ { ids: ["reload"], help: "Reload to apply what you changed" },
291
+ { ids: ["help"], help: "Show or hide this key list" },
292
+ ],
293
+ skills: [
294
+ { ids: ["close", "previousSection", "nextSection"], help: "Close, or change section" },
295
+ ...MOVEMENT,
296
+ { ids: ["toggle"], help: "Enable or disable the focused skill" },
297
+ { ids: ["detail"], help: "Show or hide what its `SKILL.md` declares" },
298
+ { ids: ["attention"], help: "Show only the rows that need attention" },
299
+ { ids: ["filter"], help: "Filter the rows by text; Esc clears it" },
300
+ { ids: ["scope"], help: "Switch where a change is written" },
301
+ { ids: ["inherit"], help: "Drop the Project override" },
302
+ { ids: ["reload"], help: "Reload to apply what you changed" },
303
+ { ids: ["help"], help: "Show or hide this key list" },
304
+ ],
305
+ settings: [
306
+ { ids: ["close", "previousSection", "nextSection"], help: "Close, or change section" },
307
+ ...MOVEMENT,
308
+ { ids: ["change"], help: "Change the focused value" },
309
+ { ids: ["reset"], help: "Reset it to its default" },
310
+ { ids: ["json"], help: "Show the section it lives in" },
311
+ { ids: ["attention"], help: "Show only the values you changed" },
312
+ { ids: ["filter"], help: "Filter the rows by text; Esc clears it" },
313
+ { ids: ["scope"], help: "Switch where a change is written" },
314
+ { ids: ["reload"], help: "Reload to apply what you changed" },
315
+ { ids: ["help"], help: "Show or hide this key list" },
316
+ ],
317
+ };
318
+
319
+ /** The bindings a section's help block documents, for the drift test. */
320
+ export function helpKeyIds(section: PanelSection): PanelKeyId[] {
321
+ return HELP[section].flatMap((row) => [...row.ids]);
322
+ }
323
+
324
+ /** The keys a help row prints, from the bindings it names. */
325
+ function helpLabel(row: HelpRow): string {
326
+ if (row.label !== undefined) return row.label;
327
+ return row.ids.map((id) => keyOf(id).label).join(" ");
328
+ }
329
+
330
+ /** What a help row says: its own sentence, or the single binding's. */
331
+ function helpText(row: HelpRow): string {
332
+ if (row.help !== undefined) return row.help;
333
+ return row.ids.map((id) => keyOf(id).help).join("; ");
334
+ }
335
+
336
+ /**
337
+ * The help block for a section: one `keys action` line per row, the key column
338
+ * padded to the widest label so the sentences line up. Every binding the section
339
+ * offers has a row, and every row names a binding the section offers —
340
+ * `keys.test.ts` holds both directions.
341
+ */
342
+ export function helpRows(theme: Theme, width: number, section: PanelSection): string[] {
343
+ const rows = HELP[section].map((row) => ({ label: helpLabel(row), help: helpText(row) }));
344
+ const column = Math.max(...rows.map((row) => row.label.length)) + 2;
345
+ const indent = " ".repeat(BODY_INDENT);
346
+ return rows.map((row) =>
347
+ clip(
348
+ `${indent}${paint(theme, "faint", padTo(row.label, column))}${paint(theme, "meta", row.help)}`,
349
+ Math.max(1, width),
350
+ ),
351
+ );
352
+ }
@@ -1,35 +1,38 @@
1
1
  /**
2
- * The grouped list sections' shared pieces: the headline counts, one row, and
3
- * the key hints.
2
+ * The resource row: the one row grammar Extensions and Skills share.
4
3
  *
5
- * Extensions and Skills draw the same row grammar — a checkbox, a verdict, and
6
- * the resource's name — and offer the same keys, so the row, the summary, and
7
- * the hint row live here and each section supplies only what it can say about
8
- * the focused row and whether it has anything to refresh.
4
+ * A row is a checkbox, a status glyph, the resource's name, and one verdict word.
5
+ * A clean row carries no glyph and no word, so the column reads as one shape
6
+ * instead of a checkmark that comes and goes, and the eye finds the rows that
7
+ * need it.
8
+ *
9
+ * Pure: the theme, the width and the model row come in, one clipped line comes out.
9
10
  */
10
11
 
11
12
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
13
  import { sanitizeLine } from "../../../../lib/format.ts";
13
- import type { ScreenHint } from "../../../../lib/tui/chrome.ts";
14
+ import { clip, padTo } from "../../../../lib/tui/columns.ts";
14
15
  import { checkbox, listRow } from "../../../../lib/tui/rows.ts";
15
- import { rowBadge, type PanelScope, summarySegments, type SummaryOptions } from "../format.ts";
16
- import type { PanelModel, PanelRow } from "../types.ts";
17
- import { countsLine } from "./frame.ts";
16
+ import { rowBadge } from "../format.ts";
17
+ import type { PanelRow } from "../types.ts";
18
+
19
+ /** Columns the verdict keeps before the name is clipped instead. */
20
+ const MIN_VERDICT_WIDTH = 9;
18
21
 
19
- /** The section's headline: the scope, the row count, and what is off. */
20
- export function resourceSummary(
21
- theme: Theme,
22
- width: number,
23
- model: PanelModel,
24
- scope: PanelScope,
25
- options: SummaryOptions = {},
26
- ): string {
27
- return countsLine(theme, width, summarySegments(model, scope, { ...options, compact: width < 72 }));
22
+ /** The widest verdict the visible rows carry, so the column lines up. */
23
+ export function verdictColumn(rows: readonly PanelRow[]): number {
24
+ return rows.reduce((columns, row) => Math.max(columns, (rowBadge(row).text ?? "").length), 0);
28
25
  }
29
26
 
30
- /** One resource row: its checkbox, verdict, and name. */
31
- export function resourceRow(theme: Theme, width: number, row: PanelRow, selected: boolean): string {
27
+ /** One resource row: its checkbox, verdict glyph, name, and verdict word. */
28
+ export function resourceRow(theme: Theme, width: number, row: PanelRow, selected: boolean, verdicts = 0): string {
32
29
  const badge = rowBadge(row);
30
+ const name = sanitizeLine(row.label);
31
+ // The verdict sits in its own column instead of against the name, so a column
32
+ // of rows can be read down rather than across; a long name loses its tail
33
+ // before the verdict loses its word.
34
+ const room = Math.max(MIN_VERDICT_WIDTH, Math.min(verdicts, Math.max(0, width - 22)));
35
+ const nameCell = verdicts > 0 ? padTo(clip(name, Math.max(1, width - 22)), room) : name;
33
36
  return listRow(theme, width, {
34
37
  selected,
35
38
  // A reported-only row has no enable pattern, but still reserves the box
@@ -37,36 +40,12 @@ export function resourceRow(theme: Theme, width: number, row: PanelRow, selected
37
40
  leading: row.item ? checkbox(theme, row.item.enabled) : " ",
38
41
  glyph: badge.glyph ?? " ",
39
42
  glyphRole: badge.role,
40
- label: sanitizeLine(row.label),
43
+ label: nameCell,
41
44
  labelRole: selected ? "accent" : row.item && !row.item.enabled ? "faint" : "label",
42
45
  trailing: badge.text,
43
46
  // The glyph carries the status colour; the word stays quiet unless it is a
44
47
  // problem, so a clean row does not shout.
45
48
  trailingRole: badge.role === "bad" || badge.role === "warn" ? badge.role : "faint",
46
- // The verdict sits beside the name instead of at the far edge of a wide
47
- // terminal, where a two-word verdict would be unreadable.
48
49
  align: "inline",
49
50
  });
50
51
  }
51
-
52
- export interface SectionHintOptions {
53
- expanded: boolean;
54
- projectScope: boolean;
55
- /** The refresh key's state; omitted by a section with nothing to refresh. */
56
- refreshing?: boolean;
57
- }
58
-
59
- /** The section's key hints, in the order they drop as the width runs out. */
60
- export function sectionHints({ expanded, projectScope, refreshing }: SectionHintOptions): ScreenHint[] {
61
- return [
62
- "Esc close",
63
- "←/→",
64
- { text: "Space toggle", color: "accent" },
65
- { text: expanded ? "Enter collapse" : "Enter detail", color: "accent" },
66
- "Tab scope",
67
- // Only Project scope can hold an override, so the key is not offered elsewhere.
68
- ...(projectScope ? ["x inherit"] : []),
69
- ...(refreshing === undefined ? [] : [refreshing ? "refreshing…" : "r refresh"]),
70
- "↑/↓ select",
71
- ];
72
- }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The two-pane split: where the side pane starts, how wide each side is, and how
3
+ * one row of each is laid on one terminal line.
4
+ *
5
+ * Above the breakpoint the focused row's facts sit beside the list; below it the
6
+ * frame keeps the single column and the facts move under the list. The side lines
7
+ * are *composed onto* the list's lines rather than appended, so the two panes cost
8
+ * exactly the rows the list already spends — the frame's row budget is the same at
9
+ * every width.
10
+ *
11
+ * Value-only: pure functions, the theme passed in.
12
+ */
13
+
14
+ import type { Theme } from "@earendil-works/pi-coding-agent";
15
+ import { clip, padTo } from "../../../../lib/tui/columns.ts";
16
+ import { GLYPHS } from "../../../../lib/tui/glyphs.ts";
17
+ import { paint } from "../../../../lib/tui/tokens.ts";
18
+
19
+ /** The narrowest terminal that gets a side pane. */
20
+ export const WIDE_MIN = 96;
21
+
22
+ /** The side pane's width bounds, in columns. */
23
+ export const SIDE_MIN = 36;
24
+ export const SIDE_MAX = 56;
25
+
26
+ /** Columns between the list and the side pane, on each side of the divider. */
27
+ const GAP = 1;
28
+
29
+ /** Whether this width gets a side pane. */
30
+ export function isWide(width: number): boolean {
31
+ return width >= WIDE_MIN;
32
+ }
33
+
34
+ /**
35
+ * The side pane's width: two fifths of the terminal, clamped. Reading the list
36
+ * and its facts at the same height is the point, so the side never grows past
37
+ * what keeps a row and its verdict readable on the left.
38
+ */
39
+ export function sideWidth(width: number): number {
40
+ return Math.max(SIDE_MIN, Math.min(SIDE_MAX, Math.floor(width * 0.42)));
41
+ }
42
+
43
+ /** The columns the list keeps, divider and gaps included in the accounting. */
44
+ export function listWidth(width: number): number {
45
+ return Math.max(1, width - sideWidth(width) - GAP * 2 - 1);
46
+ }
47
+
48
+ /**
49
+ * Compose one line pair: the list row padded to its column, the divider, and the
50
+ * side row. A pair with no side content still draws the divider, so the pane keeps
51
+ * one edge down the body instead of appearing and disappearing row by row.
52
+ */
53
+ export function composeRow(theme: Theme, width: number, list: string, side: string): string {
54
+ const left = padTo(clip(list, listWidth(width)), listWidth(width));
55
+ const divider = paint(theme, "rule", GLYPHS.divider);
56
+ const room = Math.max(0, width - listWidth(width) - GAP * 2 - 1);
57
+ return clip(`${left}${" ".repeat(GAP)}${divider}${" ".repeat(GAP)}${clip(side, room)}`, width);
58
+ }
59
+
60
+ /** Compose a body of list rows with a side pane, one line pair per row. */
61
+ export function composeRows(theme: Theme, width: number, list: readonly string[], side: readonly string[]): string[] {
62
+ return list.map((line, index) => composeRow(theme, width, line, side[index] ?? ""));
63
+ }
64
+
65
+ /**
66
+ * Cut the side block to the rows it has, keeping the reader informed: the last row
67
+ * says how many lines are left out, which is the same convention the JSON pane uses.
68
+ */
69
+ export function clipRows(rows: readonly string[], budget: number, width: number): string[] {
70
+ const room = Math.max(0, Math.floor(budget));
71
+ if (rows.length <= room) return [...rows];
72
+ if (room === 0) return [];
73
+ const kept = rows.slice(0, room - 1);
74
+ const hidden = rows.length - kept.length;
75
+ return [...kept, clip(`… ${hidden} more line${hidden === 1 ? "" : "s"}`, width)];
76
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * What the sections show, derived from the session's sources: the counts on the
3
+ * tabs, the one body height they share, the attention in the header, the filter
4
+ * chips, and the merged model the rows are drawn from.
5
+ *
6
+ * One module because these are one question asked five ways — "what is on this
7
+ * screen, and what does it add up to" — and because the component should be left
8
+ * with the state it mutates and the keys it reads, not the arithmetic over it.
9
+ *
10
+ * Pure: the source is handed in, no host is touched, and nothing is cached.
11
+ */
12
+
13
+ import type { ExtensionReport } from "../../../../lib/requirements.ts";
14
+ import { filterChips, isFiltering, rowMatches, type PanelFilter } from "../filter.ts";
15
+ import { attentionSegments, panelCounts, type SummarySegment } from "../format.ts";
16
+ import { buildPanelModel, buildSkillModel, groupedEntryCount } from "../join.ts";
17
+ import type { ResourceItem, ResourceKind, WriteScope } from "../../picker/types.ts";
18
+ import type { SettingsSurface } from "../settings-runtime.ts";
19
+ import type { PanelModel, PanelRow, PanelSection, SkillInfo } from "../types.ts";
20
+ import { settingEntries } from "./entries.ts";
21
+
22
+ /** Everything the screen derives its content from. */
23
+ export interface ViewSource {
24
+ /** The write scope the settings rows are read in. */
25
+ scope: WriteScope;
26
+ /** The resolved resources of one kind. */
27
+ items(kind: ResourceKind): readonly ResourceItem[];
28
+ /** The current doctor reports. */
29
+ reports(): readonly ExtensionReport[];
30
+ /** The settings surface, for its rows. */
31
+ settings: SettingsSurface;
32
+ /** The agent directory the resource groups are labelled from. */
33
+ agentDir: string;
34
+ /** What the reader narrowed the rows to. */
35
+ filter: PanelFilter;
36
+ /** The extension the command narrowed to, if it named one. */
37
+ commandFilter?: string;
38
+ /** Something was changed this session. */
39
+ dirty: boolean;
40
+ }
41
+
42
+ /**
43
+ * The merged model for a list section. `filtered` off skips the reader's filter,
44
+ * which is what the header's attention counts need: narrowing the list must not
45
+ * hide a problem.
46
+ */
47
+ export function modelFor(
48
+ source: ViewSource,
49
+ kind: ResourceKind,
50
+ skills: () => ReadonlyMap<string, SkillInfo>,
51
+ filtered = true,
52
+ ): PanelModel {
53
+ const where = filtered && isFiltering(source.filter) ? (row: PanelRow) => rowMatches(row, source.filter) : undefined;
54
+ if (kind === "skills") {
55
+ return buildSkillModel({ items: source.items("skills"), index: skills(), agentDir: source.agentDir, where });
56
+ }
57
+ return buildPanelModel({
58
+ items: source.items("extensions"),
59
+ reports: source.reports(),
60
+ agentDir: source.agentDir,
61
+ filter: source.commandFilter,
62
+ where,
63
+ });
64
+ }
65
+
66
+ /**
67
+ * The section sizes the tabs print: the resources each section holds. They count
68
+ * what the section has, not what a filter left, so a tab does not shrink while
69
+ * another section is being narrowed.
70
+ */
71
+ export function sectionCounts(source: ViewSource): Record<PanelSection, number> {
72
+ return {
73
+ extensions: source.items("extensions").length,
74
+ skills: source.items("skills").length,
75
+ settings: source.settings.views(source.scope).length,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * The tallest section's entry count, group headings included. All three sections
81
+ * pad their body to it, which is what keeps the dock from resizing when the tabs
82
+ * change. Counted through `groupedEntryCount`, so measuring Skills never reads a
83
+ * `SKILL.md`.
84
+ */
85
+ export function tallestBody(source: ViewSource): number {
86
+ const lists = (["extensions", "skills"] as const).map((kind) =>
87
+ groupedEntryCount(source.items(kind), source.agentDir, kind),
88
+ );
89
+ const settings = settingEntries(source.settings.views(source.scope)).length;
90
+ return Math.max(1, ...lists, settings);
91
+ }
92
+
93
+ /** The header's attention, over every extension rather than the filter. */
94
+ export function attentionFor(source: ViewSource): SummarySegment[] {
95
+ return attentionSegments(panelCounts(modelFor(source, "extensions", () => new Map(), false)), source.dirty);
96
+ }
97
+
98
+ /** The chips the tab row shows: the filter editor while open, else what applies. */
99
+ export function chipsFor(
100
+ source: ViewSource,
101
+ section: PanelSection,
102
+ editing: { buffer: string } | undefined,
103
+ ): SummarySegment[] {
104
+ if (editing !== undefined) return [{ text: `/${editing.buffer}▏`, color: "accent" }];
105
+ const chips = filterChips(source.filter, section === "settings" ? "settings" : "rows");
106
+ // The command's own narrowing (`/bytefly plan`) is a filter the reader did not
107
+ // ask for this time, so the tab row says why the list is short.
108
+ if (source.commandFilter !== undefined && section === "extensions") {
109
+ chips.unshift({ text: `${source.commandFilter} only`, color: "meta" });
110
+ }
111
+ return chips;
112
+ }
113
+
114
+ /** The line a section shows in place of its body. */
115
+ export function emptyLabelFor(filter: PanelFilter, section: PanelSection): string {
116
+ if (isFiltering(filter)) return "Nothing matches the filter.";
117
+ if (section === "skills") return "No skills found.";
118
+ if (section === "settings") return "No settings found.";
119
+ return "No extensions found.";
120
+ }
121
+
122
+ /** The group heading a row sits under, for the block's `Source` fact. */
123
+ export function groupLabelFor(model: PanelModel | undefined, row: PanelRow | undefined): string | undefined {
124
+ if (model === undefined || row === undefined) return undefined;
125
+ return model.groups.find((group) => group.key === row.group)?.label;
126
+ }