@gavin-hu/my-pi-agent 0.17.0 → 0.19.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 (274) hide show
  1. package/README.md +61 -45
  2. package/README.zh-CN.md +54 -42
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +344 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +465 -416
  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 +554 -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} +66 -53
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +74 -63
  14. package/docs/extensions/plan.md +976 -0
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -28
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +285 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +166 -119
  19. package/docs/extensions/team.md +226 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +60 -38
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +275 -192
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +115 -72
  25. package/extensions/board/commands.ts +87 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +72 -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 +312 -0
  32. package/extensions/board/result.ts +99 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +75 -0
  35. package/extensions/board/seat.ts +29 -0
  36. package/extensions/board/snapshot.ts +41 -0
  37. package/extensions/board/tools.ts +279 -0
  38. package/extensions/board/transport/router.ts +172 -0
  39. package/extensions/board/transport/server.ts +149 -0
  40. package/extensions/board/types.ts +54 -0
  41. package/extensions/board/view/actions.ts +306 -0
  42. package/extensions/board/view/client.ts +41 -0
  43. package/extensions/board/view/keys.ts +121 -0
  44. package/extensions/board/view/live.ts +397 -0
  45. package/extensions/board/view/page.ts +158 -0
  46. package/extensions/board/view/render.ts +496 -0
  47. package/extensions/board/view/styles.ts +211 -0
  48. package/extensions/board/view/team.ts +305 -0
  49. package/extensions/board/writes.ts +99 -0
  50. package/extensions/bytefly/notify/types.ts +1 -1
  51. package/extensions/bytefly/panel/commands.ts +40 -7
  52. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  53. package/extensions/bytefly/panel/settings.ts +11 -21
  54. package/extensions/bytefly/panel/tui/settings-view.ts +1 -1
  55. package/extensions/bytefly/picker/discovery.ts +0 -2
  56. package/extensions/bytefly/picker/types.ts +0 -2
  57. package/extensions/bytefly/update/check.ts +39 -0
  58. package/extensions/bytefly/update/format.ts +15 -0
  59. package/extensions/bytefly/update/register.ts +73 -34
  60. package/extensions/bytefly/update/state.ts +6 -3
  61. package/extensions/context/tui.ts +1 -1
  62. package/extensions/file-browse/{config.ts → config/config.ts} +13 -4
  63. package/extensions/file-browse/{files.ts → files/files.ts} +1 -14
  64. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  65. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  66. package/extensions/file-browse/index.ts +4 -4
  67. package/extensions/file-browse/{live.ts → live/live.ts} +54 -82
  68. package/extensions/file-browse/live/rev.ts +42 -0
  69. package/extensions/file-browse/{router.ts → transport/router.ts} +44 -17
  70. package/extensions/file-browse/transport/server.ts +110 -0
  71. package/extensions/file-browse/view/chrome.ts +83 -0
  72. package/extensions/file-browse/{client.ts → view/client.ts} +63 -1
  73. package/extensions/file-browse/view/diff-html.ts +94 -0
  74. package/extensions/file-browse/{html.ts → view/html.ts} +32 -158
  75. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  76. package/extensions/file-browse/{live-client.ts → view/live-client.ts} +47 -1
  77. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +7 -7
  78. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +1 -1
  79. package/extensions/file-browse/{styles.ts → view/styles.ts} +31 -5
  80. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  81. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  82. package/extensions/file-read/schema.ts +2 -1
  83. package/extensions/goal/plan-route.ts +8 -2
  84. package/extensions/job/index.ts +15 -7
  85. package/extensions/memory/store.ts +12 -5
  86. package/extensions/plan/blocks.ts +23 -2
  87. package/extensions/plan/commands.ts +77 -21
  88. package/extensions/plan/feedback.ts +409 -63
  89. package/extensions/plan/index.ts +7 -5
  90. package/extensions/plan/plan-index.ts +44 -10
  91. package/extensions/plan/plan-text.ts +51 -24
  92. package/extensions/plan/plans.ts +162 -57
  93. package/extensions/plan/policy.ts +20 -12
  94. package/extensions/plan/review-prompt.ts +93 -0
  95. package/extensions/plan/review.ts +51 -95
  96. package/extensions/plan/runtime.ts +22 -10
  97. package/extensions/plan/sections.ts +112 -0
  98. package/extensions/plan/steps.ts +63 -95
  99. package/extensions/plan/tools/enter.ts +8 -3
  100. package/extensions/plan/tools/exit.ts +13 -19
  101. package/extensions/plan/tools/write.ts +8 -3
  102. package/extensions/plan/tools.ts +1 -1
  103. package/extensions/plan/tui.ts +29 -15
  104. package/extensions/plan/types.ts +29 -3
  105. package/extensions/plan/web-chrome.ts +2 -1
  106. package/extensions/plan/web-client.ts +122 -178
  107. package/extensions/plan/web-collab.ts +207 -0
  108. package/extensions/plan/web-comments.ts +269 -0
  109. package/extensions/plan/web-page.ts +96 -17
  110. package/extensions/plan/web-router.ts +109 -47
  111. package/extensions/plan/web-share-session.ts +191 -0
  112. package/extensions/plan/web-share.ts +169 -0
  113. package/extensions/plan/web-streams.ts +124 -0
  114. package/extensions/plan/web-tokens.ts +63 -0
  115. package/extensions/plan/web.ts +245 -151
  116. package/extensions/question/dialogs.ts +91 -1
  117. package/extensions/question/tools.ts +2 -1
  118. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  119. package/extensions/rewind/{review.ts → commands/review.ts} +13 -13
  120. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  121. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  122. package/extensions/rewind/index.ts +3 -3
  123. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  124. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  125. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  126. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  127. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  128. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  129. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  130. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  131. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  132. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  133. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  134. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  135. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  136. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  137. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  138. package/extensions/session-bus/commands.ts +113 -0
  139. package/extensions/session-bus/format.ts +101 -0
  140. package/extensions/session-bus/index.ts +75 -0
  141. package/extensions/session-bus/join-request.ts +40 -0
  142. package/extensions/session-bus/mail-watch.ts +134 -0
  143. package/extensions/session-bus/notice.ts +86 -0
  144. package/extensions/session-bus/overlap.ts +36 -0
  145. package/extensions/session-bus/protocol.ts +104 -0
  146. package/extensions/session-bus/resolve.ts +67 -0
  147. package/extensions/session-bus/result.ts +208 -0
  148. package/extensions/session-bus/rows.ts +171 -0
  149. package/extensions/session-bus/runtime.ts +466 -0
  150. package/extensions/session-bus/store.ts +365 -0
  151. package/extensions/session-bus/tools.ts +192 -0
  152. package/extensions/session-bus/tui.ts +394 -0
  153. package/extensions/session-bus/types.ts +98 -0
  154. package/extensions/subagent/agents.ts +9 -8
  155. package/extensions/subagent/invocation.ts +4 -2
  156. package/extensions/team/commands.ts +112 -0
  157. package/extensions/team/dispatch.ts +133 -0
  158. package/extensions/team/format.ts +105 -0
  159. package/extensions/team/index.ts +33 -0
  160. package/extensions/team/schema.ts +29 -0
  161. package/extensions/team/tools.ts +246 -0
  162. package/extensions/team/types.ts +48 -0
  163. package/extensions/watch/index.ts +15 -9
  164. package/extensions/web-access/fetch/page.ts +4 -1
  165. package/extensions/web-access/fetch/structured.ts +447 -0
  166. package/extensions/web-access/fetch/tool.ts +2 -1
  167. package/extensions/web-access/search/tool.ts +2 -1
  168. package/extensions/wechat/bridge.ts +76 -92
  169. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  170. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  171. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  172. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  173. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  174. package/extensions/wechat/dialog/channels.ts +136 -0
  175. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +66 -51
  176. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +40 -25
  177. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  178. package/extensions/wechat/index.ts +10 -10
  179. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  180. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  181. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +119 -56
  182. package/extensions/wechat/{presence.ts → peer/presence.ts} +11 -11
  183. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +13 -5
  184. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  185. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  186. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  187. package/extensions/wechat/{notices.ts → text/notices.ts} +15 -7
  188. package/extensions/wechat/tools.ts +29 -12
  189. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  190. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +104 -40
  191. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  192. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  193. package/extensions/wechat/transport/outbound.ts +142 -0
  194. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  195. package/extensions/wechat/types.ts +10 -1
  196. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  197. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  198. package/extensions/worktree/git.ts +5 -0
  199. package/extensions/worktree/guard.ts +3 -5
  200. package/extensions/worktree/registry.ts +11 -5
  201. package/extensions/worktree/status.ts +1 -3
  202. package/lib/README.md +12 -6
  203. package/lib/board/card.ts +290 -0
  204. package/lib/board/links.ts +117 -0
  205. package/lib/board/page-pointer.ts +256 -0
  206. package/lib/board/round-text.ts +72 -0
  207. package/lib/board/round.ts +89 -0
  208. package/lib/board/rules.ts +304 -0
  209. package/lib/board/session.ts +105 -0
  210. package/lib/board/store.ts +310 -0
  211. package/lib/board/view.ts +150 -0
  212. package/lib/errors.ts +12 -4
  213. package/lib/format.ts +13 -0
  214. package/lib/http/guard.ts +112 -0
  215. package/lib/http/index.ts +16 -5
  216. package/lib/http/router.ts +40 -13
  217. package/lib/http/serve.ts +234 -0
  218. package/lib/http/server.ts +29 -13
  219. package/lib/http/sink.ts +110 -0
  220. package/lib/http/token.ts +38 -0
  221. package/lib/interaction.ts +10 -0
  222. package/lib/notice.ts +29 -2
  223. package/lib/plan-entry.ts +7 -2
  224. package/lib/policy.ts +12 -2
  225. package/lib/qr.ts +64 -0
  226. package/lib/repo-key.ts +74 -0
  227. package/lib/report.ts +52 -33
  228. package/lib/requirements.ts +7 -9
  229. package/lib/session-label.ts +64 -0
  230. package/lib/session-roster.ts +130 -0
  231. package/lib/session-tool.ts +184 -0
  232. package/lib/tool-names.ts +25 -2
  233. package/lib/tui/README.md +16 -2
  234. package/lib/tui/dock-screen.ts +85 -17
  235. package/lib/tui/glyphs.ts +2 -0
  236. package/lib/tui/line-pane.ts +15 -4
  237. package/lib/tui/tool.ts +23 -4
  238. package/lib/web/README.md +5 -2
  239. package/lib/web/components.ts +6 -1
  240. package/lib/web/runtime.ts +7 -1
  241. package/lib/web/tokens.ts +25 -2
  242. package/package.json +27 -14
  243. package/skills/init/SKILL.md +80 -45
  244. package/skills/init/references/agents-md-quality.md +26 -2
  245. package/skills/init/references/interview-questions.md +36 -14
  246. package/skills/next/SKILL.md +77 -51
  247. package/skills/project-docs/SKILL.md +133 -0
  248. package/skills/project-docs/references/audit.md +131 -0
  249. package/skills/project-docs/references/changelog.md +105 -0
  250. package/skills/project-docs/references/discovery.md +127 -0
  251. package/skills/project-docs/references/readme.md +80 -0
  252. package/skills/project-docs/references/release.md +84 -0
  253. package/skills/project-docs/references/roadmap.md +93 -0
  254. package/skills/project-docs/scripts/state.ts +316 -0
  255. package/skills/refactor/SKILL.md +100 -125
  256. package/skills/refactor/references/handoff.md +35 -0
  257. package/skills/refactor/references/signals.md +54 -0
  258. package/skills/refactor/scripts/inventory.ts +587 -0
  259. package/extensions/file-browse/README.md +0 -467
  260. package/extensions/file-browse/server.ts +0 -134
  261. package/extensions/plan/README.md +0 -709
  262. package/extensions/plan/config.ts +0 -27
  263. package/extensions/wechat/outbound.ts +0 -100
  264. package/extensions/wechat/qrcode.ts +0 -46
  265. package/skills/changelog/SKILL.md +0 -148
  266. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  267. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  268. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  269. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  270. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  271. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  272. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  273. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  274. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * The `session` tool's transcript block: the expanded rows one section per entity
3
+ * group, and the one collapsed line a reader sees without expanding.
4
+ *
5
+ * The row grammar and the sanitization live in `rows.ts`; this module only decides
6
+ * which rows an action shows and how the failed ones read.
7
+ */
8
+
9
+ import type { Theme } from "@earendil-works/pi-coding-agent";
10
+ import type { Component } from "@earendil-works/pi-tui";
11
+ import { sanitizeLine } from "../../lib/format.ts";
12
+ import { SEPARATORS } from "../../lib/tui/glyphs.ts";
13
+ import { TailResult, type TailResultInput, type TailSection } from "../../lib/tui/tool.ts";
14
+ import { paint } from "../../lib/tui/tokens.ts";
15
+ import { type BusRow, busRowLine, count, sessionRows } from "./rows.ts";
16
+ import type { SessionDetails } from "./types.ts";
17
+
18
+ /**
19
+ * The transcript block for an expanded `session` result: the shared tail block
20
+ * over the board's rows, one section per entity group.
21
+ */
22
+ export class SessionResult implements Component {
23
+ private readonly view: TailResult<BusRow>;
24
+
25
+ constructor(input: TailResultInput<BusRow>, theme: Theme) {
26
+ this.view = new TailResult(input, theme);
27
+ }
28
+
29
+ /** Update in place so the transcript can reuse this component across renders. */
30
+ setInput(input: TailResultInput<BusRow>, theme: Theme): void {
31
+ this.view.setInput(input, theme);
32
+ }
33
+
34
+ invalidate(): void {
35
+ this.view.invalidate();
36
+ }
37
+
38
+ render(width: number): string[] {
39
+ return this.view.render(width);
40
+ }
41
+ }
42
+
43
+ /** One section of the expanded result. */
44
+ function section(rows: BusRow[], label?: string): TailSection<BusRow> {
45
+ return { label, rows, more: 0, rowLine: busRowLine };
46
+ }
47
+
48
+ /**
49
+ * The expanded result rows for one action, in the order a reader expects: the
50
+ * board, the consumed mail, a conflict before the claim, released before
51
+ * not-owned. Returns `undefined` when the action has nothing to list.
52
+ */
53
+ export function resultSections(details: SessionDetails): TailResultInput<BusRow> | undefined {
54
+ switch (details.action) {
55
+ case "status": {
56
+ const board = sessionRows(details.status);
57
+ const claims = board.flatMap((row) => row.claims);
58
+ const sections: TailSection<BusRow>[] = [];
59
+ if (board.length > 0) sections.push(section(board.map((row) => ({ kind: "session", row }))));
60
+ if (claims.length > 0) {
61
+ sections.push(
62
+ section(
63
+ claims.map((claim) => ({ kind: "claim", claim })),
64
+ "Claims",
65
+ ),
66
+ );
67
+ }
68
+ return sections.length > 0 ? { kind: "sections", sections } : undefined;
69
+ }
70
+ case "inbox": {
71
+ const messages = details.messages ?? [];
72
+ if (messages.length === 0) return undefined;
73
+ return { kind: "sections", sections: [section(messages.map((message) => ({ kind: "message", message })))] };
74
+ }
75
+ case "claim": {
76
+ const conflicts = details.conflicts ?? [];
77
+ if (conflicts.length > 0) {
78
+ return {
79
+ kind: "sections",
80
+ sections: [
81
+ section(
82
+ conflicts.map((claim) => ({ kind: "claim", claim })),
83
+ "Conflicts",
84
+ ),
85
+ ],
86
+ };
87
+ }
88
+ const claimed = details.claimed ?? [];
89
+ if (claimed.length === 0) return undefined;
90
+ return {
91
+ kind: "sections",
92
+ sections: [
93
+ section(
94
+ claimed.map((label) => ({ kind: "result", label, tone: "ok" })),
95
+ "Claimed",
96
+ ),
97
+ ],
98
+ };
99
+ }
100
+ case "release": {
101
+ const released = details.released ?? [];
102
+ const notOwned = details.notOwned ?? [];
103
+ const sections: TailSection<BusRow>[] = [];
104
+ if (released.length > 0) {
105
+ sections.push(
106
+ section(
107
+ released.map((label) => ({ kind: "result", label, tone: "ok" })),
108
+ "Released",
109
+ ),
110
+ );
111
+ }
112
+ if (notOwned.length > 0) {
113
+ sections.push(
114
+ section(
115
+ notOwned.map((label) => ({ kind: "result", label, tone: "warn" })),
116
+ "Not yours",
117
+ ),
118
+ );
119
+ }
120
+ return sections.length > 0 ? { kind: "sections", sections } : undefined;
121
+ }
122
+ case "send": {
123
+ const delivered = details.delivered ?? [];
124
+ if (delivered.length === 0) return undefined;
125
+ return {
126
+ kind: "sections",
127
+ sections: [section(delivered.map((label) => ({ kind: "result", label, tone: "ok" })))],
128
+ };
129
+ }
130
+ case "join":
131
+ return undefined;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * The collapsed result line: the one thing the user needs without expanding.
137
+ *
138
+ * A conflict or a path this session does not own is worth the warning role; a
139
+ * successful claim, release, or send is routine, so those stay calm.
140
+ */
141
+ export function collapsedLine(theme: Theme, details: SessionDetails): string {
142
+ const sep = SEPARATORS.item;
143
+ switch (details.action) {
144
+ case "status": {
145
+ const board = sessionRows(details.status);
146
+ const unread = board.reduce((total, row) => total + row.session.unread, 0);
147
+ const claims = board.reduce((total, row) => total + row.claims.length, 0);
148
+ const cells = [count(board.length, "session")];
149
+ if (unread > 0) cells.push(`${unread} unread`);
150
+ cells.push(count(claims, "claim"));
151
+ const watching = details.status?.watching;
152
+ if (watching) cells.push(watching.wake ? "watch wake" : "watch on");
153
+ const line = paint(theme, "meta", cells.join(sep));
154
+ // A reader that has not joined is looking at a bus it is not on.
155
+ if (details.status?.onBus === false) {
156
+ return `${line}${paint(theme, "faint", sep)}${paint(theme, "warn", "not on the bus \u2014 session join puts you on it")}`;
157
+ }
158
+ return line;
159
+ }
160
+ case "send": {
161
+ const delivered = details.delivered ?? [];
162
+ if (delivered.length === 0) return paint(theme, "meta", "Delivered to nobody");
163
+ return paint(theme, "ok", `Delivered to ${delivered.map((label) => sanitizeLine(label)).join(", ")}`);
164
+ }
165
+ case "inbox": {
166
+ const messages = details.messages ?? [];
167
+ if (messages.length === 0) return paint(theme, "meta", "Inbox empty");
168
+ const senders = [...new Set(messages.map((message) => sanitizeLine(message.fromLabel)))];
169
+ return paint(theme, "meta", `${count(messages.length, "message")}${sep}${senders.join(", ")}`);
170
+ }
171
+ case "claim": {
172
+ const conflicts = details.conflicts ?? [];
173
+ if (conflicts.length > 0) {
174
+ const [first] = conflicts;
175
+ const more = conflicts.length - 1;
176
+ const path = first ? sanitizeLine(first.path) : "a path";
177
+ const owner = first ? sanitizeLine(first.ownerLabel) : "a peer";
178
+ return paint(
179
+ theme,
180
+ "warn",
181
+ `${count(conflicts.length, "conflict")}${sep}${path} held by ${owner}${more > 0 ? ` +${more} more` : ""}`,
182
+ );
183
+ }
184
+ const claimed = details.claimed ?? [];
185
+ if (claimed.length === 0) return paint(theme, "meta", "Claimed nothing");
186
+ return paint(theme, "ok", `Claimed ${claimed.map((path) => sanitizeLine(path)).join(", ")}`);
187
+ }
188
+ case "release": {
189
+ const released = details.released ?? [];
190
+ const notOwned = details.notOwned ?? [];
191
+ const parts: string[] = [];
192
+ if (released.length > 0)
193
+ parts.push(paint(theme, "ok", `Released ${released.map((path) => sanitizeLine(path)).join(", ")}`));
194
+ if (notOwned.length > 0) {
195
+ parts.push(paint(theme, "warn", `Not yours: ${notOwned.map((path) => sanitizeLine(path)).join(", ")}`));
196
+ }
197
+ return parts.length > 0 ? parts.join(paint(theme, "faint", sep)) : paint(theme, "meta", "Nothing to release");
198
+ }
199
+ case "join": {
200
+ const label = paint(theme, "ok", sanitizeLine(details.label ?? ""));
201
+ const state =
202
+ details.watching?.armed !== true
203
+ ? paint(theme, "meta", "not listening")
204
+ : paint(theme, "meta", details.watching.wake === true ? "listening, wake" : "listening");
205
+ return `${label}${paint(theme, "faint", sep)}${state}`;
206
+ }
207
+ }
208
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The bus row grammar: the one place a session, a held path, or a message is
3
+ * turned into a line, shared by the `/sessions` board and the `session` tool
4
+ * block's expanded result.
5
+ *
6
+ * Every field came from another session's record, message, or claim, so it is
7
+ * sanitized here, at the boundary that reaches the terminal, and every row is
8
+ * clipped to the width it is given by `listRow`/`statusRow`.
9
+ */
10
+
11
+ import type { Theme } from "@earendil-works/pi-coding-agent";
12
+ import { formatDuration, sanitizeLine } from "../../lib/format.ts";
13
+ import { GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
14
+ import { listRow, statusRow } from "../../lib/tui/rows.ts";
15
+ import type { TailRowLine } from "../../lib/tui/tool.ts";
16
+ import { paint } from "../../lib/tui/tokens.ts";
17
+ import { idSuffix } from "./format.ts";
18
+ import type { BusMessage, ClaimRecord, SessionRecord, SessionStatus } from "./types.ts";
19
+
20
+ /** Claim paths the focus block lists before it points at the pane. */
21
+ const BLOCK_CLAIMS = 2;
22
+
23
+ /** `1 claim`, `2 claims`. */
24
+ export function count(n: number, noun: string): string {
25
+ return `${n} ${noun}${n === 1 ? "" : "s"}`;
26
+ }
27
+
28
+ /** One session on the board, with the claims it holds. */
29
+ export interface SessionRow {
30
+ session: SessionRecord & { unread: number };
31
+ /** Whether this row is the session reading the board. */
32
+ self: boolean;
33
+ claims: ClaimRecord[];
34
+ }
35
+
36
+ /** A row of the transcript's expanded result: one entity per action. */
37
+ export type BusRow =
38
+ | { kind: "session"; row: SessionRow }
39
+ | { kind: "claim"; claim: Pick<ClaimRecord, "path" | "ownerLabel"> }
40
+ | { kind: "message"; message: BusMessage }
41
+ | { kind: "result"; label: string; tone: "ok" | "warn" };
42
+
43
+ /** Label order by code unit, so a live re-sort is deterministic. */
44
+ function byLabel(a: { label: string }, b: { label: string }): number {
45
+ return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
46
+ }
47
+
48
+ /**
49
+ * The board's rows: this session first, then peers by label, each with its own
50
+ * claims. A status without sessions (a failed read) is an empty board.
51
+ */
52
+ export function sessionRows(status: SessionStatus | undefined): SessionRow[] {
53
+ if (!status) return [];
54
+ const rows = status.sessions.map((session) => ({
55
+ session,
56
+ self: session.id === status.me.id,
57
+ claims: status.claims
58
+ .filter((claim) => claim.owner === session.id)
59
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
60
+ }));
61
+ return rows.sort((a, b) => (a.self === b.self ? byLabel(a.session, b.session) : a.self ? -1 : 1));
62
+ }
63
+
64
+ /** One session row: the label, a `you` tag on this session, and its counts. */
65
+ export function sessionRowLine(
66
+ row: SessionRow,
67
+ theme: Theme,
68
+ width: number,
69
+ options: { selected?: boolean; marker?: boolean } = {},
70
+ ): string {
71
+ const label = `${sanitizeLine(row.session.label)}${row.self ? " · you" : ""}`;
72
+ const counts = [`${row.session.unread} unread`];
73
+ if (row.claims.length > 0) counts.push(count(row.claims.length, "claim"));
74
+ return listRow(theme, width, {
75
+ selected: options.selected === true,
76
+ marker: options.marker !== false,
77
+ label,
78
+ labelRole: options.selected === true ? "accent" : "label",
79
+ trailing: counts.join(SEPARATORS.item),
80
+ });
81
+ }
82
+
83
+ /** One held path: the claim glyph, the path, and the session holding it. */
84
+ export function claimRowLine(
85
+ claim: Pick<ClaimRecord, "path" | "ownerLabel">,
86
+ theme: Theme,
87
+ width: number,
88
+ options: { selected?: boolean; marker?: boolean; self?: boolean } = {},
89
+ ): string {
90
+ const owner = `${sanitizeLine(claim.ownerLabel)}${options.self === true ? " · you" : ""}`;
91
+ return statusRow(theme, width, {
92
+ selected: options.selected === true,
93
+ marker: options.marker !== false,
94
+ glyph: GLYPHS.target,
95
+ glyphRole: "info",
96
+ label: sanitizeLine(claim.path),
97
+ labelRole: options.selected === true ? "accent" : "label",
98
+ trailing: owner,
99
+ });
100
+ }
101
+
102
+ /** One message row: the sender and kind as meta trailing the text. */
103
+ export function messageRowLine(
104
+ message: BusMessage,
105
+ theme: Theme,
106
+ width: number,
107
+ options: { selected?: boolean; marker?: boolean } = {},
108
+ ): string {
109
+ const glyph = message.kind === "event" ? GLYPHS.warn : message.kind === "ask" ? GLYPHS.unknown : GLYPHS.pending;
110
+ return statusRow(theme, width, {
111
+ selected: options.selected === true,
112
+ marker: options.marker !== false,
113
+ glyph,
114
+ glyphRole: message.kind === "event" ? "warn" : "info",
115
+ label: sanitizeLine(message.text),
116
+ labelRole: options.selected === true ? "accent" : "label",
117
+ trailing: `${sanitizeLine(message.fromLabel)}${SEPARATORS.item}${sanitizeLine(message.kind)}`,
118
+ });
119
+ }
120
+
121
+ /** The one row renderer the expanded transcript block uses. */
122
+ export const busRowLine: TailRowLine<BusRow> = (row, width, theme) => {
123
+ switch (row.kind) {
124
+ case "session":
125
+ return sessionRowLine(row.row, theme, width, { marker: false });
126
+ case "claim":
127
+ return claimRowLine(row.claim, theme, width, { marker: false });
128
+ case "message":
129
+ return messageRowLine(row.message, theme, width, { marker: false });
130
+ case "result":
131
+ return statusRow(theme, width, {
132
+ marker: false,
133
+ glyph: row.tone === "ok" ? GLYPHS.check : GLYPHS.cross,
134
+ glyphRole: row.tone,
135
+ label: sanitizeLine(row.label),
136
+ trailing: "",
137
+ });
138
+ }
139
+ };
140
+
141
+ /** The summary cells: the counts a reader scans before the rows. */
142
+ export function summaryCells(
143
+ theme: Theme,
144
+ rows: readonly SessionRow[],
145
+ watching: { armed: boolean; wake: boolean } | undefined,
146
+ onBus = true,
147
+ ): string[] {
148
+ const unread = rows.reduce((total, row) => total + row.session.unread, 0);
149
+ const claims = rows.reduce((total, row) => total + row.claims.length, 0);
150
+ const cells = [paint(theme, "meta", count(rows.length, "session"))];
151
+ if (unread > 0) cells.push(paint(theme, "label", `${unread} unread`));
152
+ cells.push(paint(theme, "meta", count(claims, "claim")));
153
+ if (watching) cells.push(paint(theme, "ok", watching.wake ? "watch wake" : "watch on"));
154
+ if (!onBus) cells.push(paint(theme, "warn", "not on the bus"));
155
+ return cells;
156
+ }
157
+
158
+ /** The focus block under the list: what the row does not already say. */
159
+ export function sessionDetailBlock(row: SessionRow, now: number): string[] {
160
+ const lines = [`cwd: ${sanitizeLine(row.session.cwd)}`];
161
+ if (row.claims.length > 0) {
162
+ const shown = row.claims.slice(0, BLOCK_CLAIMS).map((claim) => sanitizeLine(claim.path));
163
+ const hidden = row.claims.length - shown.length;
164
+ lines.push(`claims: ${shown.join(", ")}${hidden > 0 ? ` +${hidden} more` : ""}`);
165
+ }
166
+ lines.push(
167
+ `mode: ${sanitizeLine(row.session.mode)}${SEPARATORS.item}id: ${idSuffix(row.session.id)}` +
168
+ `${SEPARATORS.item}last seen: ${formatDuration(Math.max(0, now - row.session.lastSeen))} ago`,
169
+ );
170
+ return lines;
171
+ }