@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 (200) hide show
  1. package/README.md +29 -15
  2. package/README.zh-CN.md +27 -15
  3. package/extensions/bytefly/README.md +195 -115
  4. package/extensions/bytefly/footer/collect/entries.ts +1 -6
  5. package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
  6. package/extensions/bytefly/footer/price/price.ts +12 -8
  7. package/extensions/bytefly/footer/register.ts +13 -1
  8. package/extensions/bytefly/footer/types.ts +11 -0
  9. package/extensions/bytefly/footer/unpriced.ts +36 -0
  10. package/extensions/bytefly/header/component.ts +15 -29
  11. package/extensions/bytefly/header/identity.ts +71 -0
  12. package/extensions/bytefly/header/register.ts +9 -3
  13. package/extensions/bytefly/index.ts +13 -5
  14. package/extensions/bytefly/message/format.ts +20 -13
  15. package/extensions/bytefly/message/register.ts +8 -7
  16. package/extensions/bytefly/notify/types.ts +1 -1
  17. package/extensions/bytefly/panel/commands.ts +46 -25
  18. package/extensions/bytefly/panel/format.ts +49 -14
  19. package/extensions/bytefly/panel/join.ts +35 -9
  20. package/extensions/bytefly/panel/settings.ts +29 -39
  21. package/extensions/bytefly/panel/skills.ts +51 -0
  22. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
  23. package/extensions/bytefly/panel/tui/frame.ts +9 -1
  24. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  25. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  26. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  27. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  28. package/extensions/bytefly/panel/tui.ts +223 -331
  29. package/extensions/bytefly/panel/types.ts +26 -8
  30. package/extensions/bytefly/picker/discovery.ts +32 -27
  31. package/extensions/bytefly/picker/resources.ts +16 -8
  32. package/extensions/bytefly/picker/runtime.ts +62 -34
  33. package/extensions/bytefly/picker/toggle.ts +16 -10
  34. package/extensions/bytefly/picker/types.ts +31 -13
  35. package/extensions/bytefly/update/format.ts +66 -8
  36. package/extensions/bytefly/update/patch.ts +66 -0
  37. package/extensions/bytefly/update/register.ts +4 -11
  38. package/extensions/bytefly/update/release.ts +65 -0
  39. package/extensions/bytefly/update/state.ts +14 -0
  40. package/extensions/bytefly/update/suppress.ts +47 -29
  41. package/extensions/bytefly/update/types.ts +15 -5
  42. package/extensions/check/tui.ts +2 -0
  43. package/extensions/context/README.md +21 -11
  44. package/extensions/context/format.ts +10 -8
  45. package/extensions/context/report.ts +21 -8
  46. package/extensions/context/tui.ts +26 -10
  47. package/extensions/context/types.ts +5 -3
  48. package/extensions/file-browse/README.md +89 -22
  49. package/extensions/file-browse/chrome.ts +83 -0
  50. package/extensions/file-browse/client.ts +74 -459
  51. package/extensions/file-browse/config.ts +12 -3
  52. package/extensions/file-browse/diff-html.ts +94 -0
  53. package/extensions/file-browse/html.ts +26 -152
  54. package/extensions/file-browse/live-client.ts +47 -1
  55. package/extensions/file-browse/live.ts +35 -14
  56. package/extensions/file-browse/palette.ts +224 -0
  57. package/extensions/file-browse/rev.ts +42 -0
  58. package/extensions/file-browse/router.ts +27 -6
  59. package/extensions/file-browse/server.ts +7 -0
  60. package/extensions/file-browse/styles.ts +10 -3
  61. package/extensions/file-browse/tree.ts +381 -0
  62. package/extensions/file-read/README.md +4 -1
  63. package/extensions/file-read/extract/odt.ts +7 -5
  64. package/extensions/goal/plan-route.ts +8 -2
  65. package/extensions/goal/tools.ts +6 -11
  66. package/extensions/job/README.md +7 -5
  67. package/extensions/job/index.ts +35 -52
  68. package/extensions/job/tools.ts +51 -77
  69. package/extensions/memory/README.md +3 -3
  70. package/extensions/memory/tools.ts +4 -6
  71. package/extensions/memory/tui.ts +5 -1
  72. package/extensions/plan/README.md +333 -84
  73. package/extensions/plan/commands.ts +73 -7
  74. package/extensions/plan/feedback.ts +300 -63
  75. package/extensions/plan/index.ts +1 -1
  76. package/extensions/plan/plan-index.ts +4 -2
  77. package/extensions/plan/plan-text.ts +18 -1
  78. package/extensions/plan/plans.ts +184 -58
  79. package/extensions/plan/review-prompt.ts +93 -0
  80. package/extensions/plan/review.ts +95 -95
  81. package/extensions/plan/runtime.ts +4 -2
  82. package/extensions/plan/split.ts +87 -0
  83. package/extensions/plan/steps.ts +123 -107
  84. package/extensions/plan/tools/cancel.ts +79 -0
  85. package/extensions/plan/tools/enter.ts +17 -12
  86. package/extensions/plan/tools/exit.ts +148 -59
  87. package/extensions/plan/tools/steps.ts +7 -19
  88. package/extensions/plan/tools/write.ts +29 -31
  89. package/extensions/plan/tools.ts +6 -3
  90. package/extensions/plan/tui.ts +60 -15
  91. package/extensions/plan/types.ts +45 -3
  92. package/extensions/plan/web-chrome.ts +102 -0
  93. package/extensions/plan/web-client.ts +177 -446
  94. package/extensions/plan/web-collab.ts +207 -0
  95. package/extensions/plan/web-comments.ts +269 -0
  96. package/extensions/plan/web-markdown.ts +222 -0
  97. package/extensions/plan/web-page.ts +131 -19
  98. package/extensions/plan/web-router.ts +148 -32
  99. package/extensions/plan/web-share-session.ts +197 -0
  100. package/extensions/plan/web-share.ts +169 -0
  101. package/extensions/plan/web-streams.ts +124 -0
  102. package/extensions/plan/web-tokens.ts +63 -0
  103. package/extensions/plan/web.ts +193 -45
  104. package/extensions/question/README.md +3 -0
  105. package/extensions/question/dialogs.ts +91 -1
  106. package/extensions/question/tools.ts +2 -1
  107. package/extensions/rewind/checklist.ts +4 -0
  108. package/extensions/rewind/review-tui.ts +3 -1
  109. package/extensions/rewind/rewind-tui.ts +4 -0
  110. package/extensions/session-bus/README.md +247 -0
  111. package/extensions/session-bus/commands.ts +165 -0
  112. package/extensions/session-bus/format.ts +124 -0
  113. package/extensions/session-bus/index.ts +69 -0
  114. package/extensions/session-bus/notice.ts +58 -0
  115. package/extensions/session-bus/protocol.ts +134 -0
  116. package/extensions/session-bus/result.ts +243 -0
  117. package/extensions/session-bus/rows.ts +169 -0
  118. package/extensions/session-bus/runtime.ts +575 -0
  119. package/extensions/session-bus/store.ts +329 -0
  120. package/extensions/session-bus/team.ts +102 -0
  121. package/extensions/session-bus/tools.ts +227 -0
  122. package/extensions/session-bus/tui.ts +394 -0
  123. package/extensions/session-bus/types.ts +98 -0
  124. package/extensions/subagent/README.md +26 -13
  125. package/extensions/subagent/format.ts +72 -25
  126. package/extensions/subagent/index.ts +21 -47
  127. package/extensions/subagent/orchestrate.ts +18 -41
  128. package/extensions/subagent/render.ts +29 -42
  129. package/extensions/subagent/stream.ts +24 -18
  130. package/extensions/subagent/types.ts +7 -0
  131. package/extensions/todo/tools.ts +5 -6
  132. package/extensions/todo/tui.ts +5 -1
  133. package/extensions/watch/README.md +9 -5
  134. package/extensions/watch/index.ts +25 -44
  135. package/extensions/watch/tools.ts +31 -59
  136. package/extensions/wechat/README.md +45 -36
  137. package/extensions/wechat/bridge.ts +46 -48
  138. package/extensions/wechat/client.ts +1 -4
  139. package/extensions/wechat/commands.ts +3 -3
  140. package/extensions/wechat/deliver.ts +57 -37
  141. package/extensions/wechat/interaction.ts +54 -18
  142. package/extensions/wechat/login-screen.ts +2 -3
  143. package/extensions/wechat/notices.ts +9 -7
  144. package/extensions/wechat/peer-worker.ts +19 -34
  145. package/extensions/wechat/presence.ts +3 -4
  146. package/extensions/wechat/prompt.ts +53 -23
  147. package/extensions/wechat/reply-pipeline.ts +12 -4
  148. package/extensions/wechat/slash.ts +14 -0
  149. package/extensions/wechat/status-screen.ts +2 -2
  150. package/extensions/wechat/tools.ts +3 -11
  151. package/extensions/worktree/README.md +48 -12
  152. package/extensions/worktree/commands.ts +109 -65
  153. package/extensions/worktree/enter.ts +1 -1
  154. package/extensions/worktree/exit.ts +11 -1
  155. package/extensions/worktree/git.ts +5 -0
  156. package/extensions/worktree/lifecycle.ts +9 -1
  157. package/extensions/worktree/registry.ts +11 -5
  158. package/extensions/worktree/screen.ts +263 -0
  159. package/extensions/worktree/status.ts +84 -40
  160. package/lib/README.md +12 -7
  161. package/lib/claims/overlap.ts +43 -0
  162. package/lib/config.ts +12 -0
  163. package/lib/format.ts +6 -2
  164. package/lib/http/router.ts +40 -13
  165. package/lib/http/server.ts +27 -13
  166. package/lib/interaction.ts +28 -1
  167. package/lib/notice.ts +29 -2
  168. package/lib/plan-entry.ts +7 -2
  169. package/lib/plans/sections.ts +172 -0
  170. package/lib/plans/setting.ts +52 -0
  171. package/lib/plans/split.ts +215 -0
  172. package/lib/plans/store.ts +19 -0
  173. package/lib/plans/team.ts +151 -0
  174. package/lib/qr.ts +64 -0
  175. package/lib/repo-key.ts +74 -0
  176. package/lib/report.ts +117 -25
  177. package/lib/requirements.ts +1 -2
  178. package/lib/tool-result.ts +54 -4
  179. package/lib/tui/README.md +30 -8
  180. package/lib/tui/chrome.ts +27 -6
  181. package/lib/tui/dock-screen.ts +91 -19
  182. package/lib/tui/gauge.ts +9 -5
  183. package/lib/tui/line-pane.ts +15 -4
  184. package/lib/tui/tool.ts +23 -4
  185. package/lib/usage.ts +26 -0
  186. package/lib/web/README.md +131 -0
  187. package/lib/web/base.ts +19 -2
  188. package/lib/web/components.ts +10 -6
  189. package/lib/web/layout.ts +2 -0
  190. package/lib/web/markup.ts +2 -1
  191. package/lib/web/runtime.ts +103 -25
  192. package/lib/web/tokens.ts +23 -7
  193. package/package.json +5 -2
  194. package/skills/changelog/SKILL.md +57 -45
  195. package/skills/init/SKILL.md +80 -45
  196. package/skills/init/references/agents-md-quality.md +26 -2
  197. package/skills/init/references/interview-questions.md +36 -14
  198. package/skills/next/SKILL.md +209 -0
  199. package/skills/parallel-dev/SKILL.md +150 -0
  200. package/extensions/wechat/qrcode.ts +0 -46
@@ -0,0 +1,394 @@
1
+ /**
2
+ * The `/sessions` board: a docked list over the bus, with three scopes.
3
+ *
4
+ * `Tab` cycles Sessions → Inbox → Claims. Sessions and claims come from the bus
5
+ * snapshot, mail from a read-only peek, so the board announces nothing and
6
+ * consumes nothing. The rows, summaries, and focus blocks are in `rows.ts`; this
7
+ * module owns the screen, the scope it shows, and the two verbs that change this
8
+ * session's own state.
9
+ */
10
+
11
+ import type { Theme } from "@earendil-works/pi-coding-agent";
12
+ import { matchesKey, Key } from "@earendil-works/pi-tui";
13
+ import { messageOf } from "../../lib/errors.ts";
14
+ import { formatDuration, sanitizeLine } from "../../lib/format.ts";
15
+ import type { ScreenHint, ViewportRowsSource } from "../../lib/tui/chrome.ts";
16
+ import { type DetailContents, DockListScreen } from "../../lib/tui/dock-screen.ts";
17
+ import { SEPARATORS } from "../../lib/tui/glyphs.ts";
18
+ import { keyValueRow } from "../../lib/tui/rows.ts";
19
+ import { paint } from "../../lib/tui/tokens.ts";
20
+ import { shortId } from "./format.ts";
21
+ import {
22
+ type SessionRow,
23
+ claimRowLine,
24
+ count,
25
+ messageRowLine,
26
+ sessionDetailBlock,
27
+ sessionRowLine,
28
+ sessionRows,
29
+ summaryCells,
30
+ } from "./rows.ts";
31
+ import type { BoardSnapshot, BusMessage, ClaimRecord } from "./types.ts";
32
+
33
+ /** The scopes `Tab` cycles through. */
34
+ export type BoardScope = "sessions" | "inbox" | "claims";
35
+
36
+ /** One row of the board, whichever scope is showing. */
37
+ type BoardRow =
38
+ | { kind: "session"; row: SessionRow }
39
+ | { kind: "message"; message: BusMessage }
40
+ | { kind: "claim"; claim: ClaimRecord };
41
+
42
+ const SCOPE_ORDER: readonly BoardScope[] = ["sessions", "inbox", "claims"];
43
+
44
+ const SCOPE_TITLE: Readonly<Record<BoardScope, string>> = {
45
+ sessions: "Sessions",
46
+ inbox: "Inbox",
47
+ claims: "Claims",
48
+ };
49
+
50
+ const SCOPE_EMPTY: Readonly<Record<BoardScope, string>> = {
51
+ sessions: "No sessions on this bus.",
52
+ inbox: "No unread mail.",
53
+ claims: "No claims.",
54
+ };
55
+
56
+ /** A relative age for a message or a claim, e.g. `2.0s ago`, `5m00s ago`. */
57
+ function ago(at: number, now: number): string {
58
+ return `${formatDuration(Math.max(0, now - at))} ago`;
59
+ }
60
+
61
+ /** A message body as prose: one paragraph per source line, control characters gone. */
62
+ function messageProse(message: BusMessage): string {
63
+ return message.text
64
+ .split("\n")
65
+ .map((line) => sanitizeLine(line))
66
+ .join("\n");
67
+ }
68
+
69
+ /** The paths a message mentions, or an empty string. */
70
+ function messagePaths(message: BusMessage): string {
71
+ return message.paths?.length ? message.paths.map((path) => sanitizeLine(path)).join(", ") : "";
72
+ }
73
+
74
+ /** The pane's width-dependent facts: identity, above the claim body. */
75
+ function paneFacts(theme: Theme, row: SessionRow, now: number, width: number): string[] {
76
+ const labelWidth = 10;
77
+ return [
78
+ keyValueRow(theme, width, { label: "mode", labelWidth, value: sanitizeLine(row.session.mode) }),
79
+ keyValueRow(theme, width, { label: "id", labelWidth, value: shortId(row.session.id) }),
80
+ keyValueRow(theme, width, {
81
+ label: "last seen",
82
+ labelWidth,
83
+ value: ago(row.session.lastSeen, now),
84
+ }),
85
+ ];
86
+ }
87
+
88
+ /** The pane body: every claim this session holds, in path order. */
89
+ function paneLines(row: SessionRow, now: number): string[] {
90
+ return row.claims.map((claim) => `${sanitizeLine(claim.path)}${SEPARATORS.item}held ${ago(claim.at, now)}`);
91
+ }
92
+
93
+ /** A cheap signature of what the board draws, so a poll repaints only on a change. */
94
+ function boardKey(board: BoardSnapshot | undefined): string {
95
+ if (!board) return "";
96
+ return [
97
+ ...board.status.sessions.map(
98
+ (session) =>
99
+ `${session.id}:${session.label}:${session.unread}:${session.lastSeen}:${session.mode}:${session.cwd}`,
100
+ ),
101
+ ...board.status.claims.map((claim) => `${claim.path}:${claim.owner}:${claim.ownerLabel}:${claim.at}`),
102
+ board.status.watching ? `${board.status.watching.armed}:${board.status.watching.wake}` : "",
103
+ ...board.messages.map((message) => `${message.id}:${message.at}`),
104
+ ].join("|");
105
+ }
106
+
107
+ export interface SessionListCallbacks {
108
+ /** Arm (or disarm) this session's inbox watch, returning the line to show. */
109
+ toggleWatch(armed: boolean): Promise<string>;
110
+ /** Release one of this session's claims, returning the line to show. */
111
+ releaseClaim(path: string): Promise<string>;
112
+ }
113
+
114
+ interface SessionListOptions {
115
+ /** The latest board, or undefined before the first read. */
116
+ latest(): BoardSnapshot | undefined;
117
+ theme: Theme;
118
+ callbacks: SessionListCallbacks;
119
+ onClose(): void;
120
+ requestRender(): void;
121
+ now?: () => number;
122
+ viewportRowsSource?: ViewportRowsSource;
123
+ }
124
+
125
+ /**
126
+ * The `/sessions` board.
127
+ *
128
+ * The rows are derived from a board the command's poll supplies, so the screen
129
+ * never reads the bus itself; `liveDetail` keeps an open pane in step with that
130
+ * board, so a peer that expires or a message that is consumed closes its own pane.
131
+ * The focus is anchored by a scope-prefixed id, so switching scopes lands on the
132
+ * first row of the new list instead of a row that is no longer there.
133
+ */
134
+ export class SessionListComponent extends DockListScreen<BoardRow> {
135
+ private board: BoardSnapshot | undefined;
136
+ private scope: BoardScope = "sessions";
137
+ private readonly callbacks: SessionListCallbacks;
138
+ private readonly now: () => number;
139
+
140
+ constructor(options: SessionListOptions) {
141
+ super({
142
+ theme: options.theme,
143
+ title: "Sessions",
144
+ detailTitle: "Details",
145
+ emptyList: "No sessions on this bus.",
146
+ emptyDetail: "Nothing to show.",
147
+ requestRender: options.requestRender,
148
+ onClose: options.onClose,
149
+ viewportRowsSource: options.viewportRowsSource,
150
+ });
151
+ this.callbacks = options.callbacks;
152
+ this.now = options.now ?? Date.now;
153
+ this.board = options.latest();
154
+ }
155
+
156
+ /** Replace the board; a poll that changed nothing does not repaint. */
157
+ setBoard(board: BoardSnapshot | undefined): void {
158
+ const before = boardKey(this.board);
159
+ this.board = board;
160
+ if (boardKey(board) !== before) this.options.requestRender();
161
+ }
162
+
163
+ /** The scope currently listed, for a test or a caller that wants to label it. */
164
+ get currentScope(): BoardScope {
165
+ return this.scope;
166
+ }
167
+
168
+ protected headerLabel(): string {
169
+ return SCOPE_TITLE[this.scope];
170
+ }
171
+
172
+ protected emptyLabel(): string {
173
+ return SCOPE_EMPTY[this.scope];
174
+ }
175
+
176
+ protected rows(): readonly BoardRow[] {
177
+ switch (this.scope) {
178
+ case "sessions":
179
+ return sessionRows(this.board?.status).map((row) => ({ kind: "session", row }));
180
+ case "inbox":
181
+ return (this.board?.messages ?? []).map((message) => ({ kind: "message", message }));
182
+ case "claims":
183
+ return (this.board?.status.claims ?? []).map((claim) => ({ kind: "claim", claim }));
184
+ }
185
+ }
186
+
187
+ protected idOf(row: BoardRow): string {
188
+ switch (row.kind) {
189
+ case "session":
190
+ return `s:${row.row.session.id}`;
191
+ case "message":
192
+ return `m:${row.message.id}`;
193
+ case "claim":
194
+ return `c:${row.claim.path}`;
195
+ }
196
+ }
197
+
198
+ protected rowLine(row: BoardRow, width: number, selected: boolean): string {
199
+ switch (row.kind) {
200
+ case "session":
201
+ return sessionRowLine(row.row, this.theme, width, { selected });
202
+ case "message":
203
+ return messageRowLine(row.message, this.theme, width, { selected });
204
+ case "claim":
205
+ return claimRowLine(row.claim, this.theme, width, {
206
+ selected,
207
+ self: row.claim.owner === this.me(),
208
+ });
209
+ }
210
+ }
211
+
212
+ protected detailBlock(row: BoardRow): string[] {
213
+ switch (row.kind) {
214
+ case "session":
215
+ return sessionDetailBlock(row.row, this.now());
216
+ case "message": {
217
+ const lines = [`received: ${ago(row.message.at, this.now())}`];
218
+ const paths = messagePaths(row.message);
219
+ if (paths) lines.push(`paths: ${paths}`);
220
+ return lines;
221
+ }
222
+ case "claim":
223
+ return [`held ${ago(row.claim.at, this.now())}`];
224
+ }
225
+ }
226
+
227
+ protected summaryCells(rows: readonly BoardRow[]): string[] {
228
+ const watching = this.board?.status.watching;
229
+ switch (this.scope) {
230
+ case "sessions": {
231
+ const sessions = rows.flatMap((row) => (row.kind === "session" ? [row.row] : []));
232
+ return summaryCells(this.theme, sessions, watching);
233
+ }
234
+ case "inbox": {
235
+ const cells = [paint(this.theme, "meta", count(rows.length, "unread message"))];
236
+ if (watching) cells.push(this.watchCell(watching));
237
+ return cells;
238
+ }
239
+ case "claims": {
240
+ const own = rows.flatMap((row) => (row.kind === "claim" && row.claim.owner === this.me() ? [row] : []));
241
+ const cells = [paint(this.theme, "meta", count(rows.length, "claim"))];
242
+ if (own.length > 0) cells.push(paint(this.theme, "label", `${own.length} yours`));
243
+ return cells;
244
+ }
245
+ }
246
+ }
247
+
248
+ protected listHints(): ScreenHint[] {
249
+ const tab = (scope: BoardScope): ScreenHint => ({ text: `Tab ${SCOPE_TITLE[scope].toLowerCase()}` });
250
+ const enter = (verb: string): ScreenHint => ({ text: `Enter ${verb}`, color: "accent" });
251
+ const hints: ScreenHint[] = ["Esc", tab(this.nextScope())];
252
+ switch (this.scope) {
253
+ case "sessions":
254
+ hints.push(enter("open"), this.watchHint(), "↑/↓", "PgUp/PgDn");
255
+ break;
256
+ case "inbox":
257
+ hints.push(enter("read"), this.watchHint(), "↑/↓", "PgUp/PgDn");
258
+ break;
259
+ case "claims":
260
+ hints.push(enter("open"), { text: "x release", color: "warning" }, "↑/↓", "PgUp/PgDn");
261
+ break;
262
+ }
263
+ return hints;
264
+ }
265
+
266
+ protected detailHints(row: BoardRow | undefined): ScreenHint[] {
267
+ if (row?.kind === "claim" && row.claim.owner === this.me()) {
268
+ return ["Esc back", { text: "x release", color: "warning" }, "↑/↓ scroll", "g/G ends"];
269
+ }
270
+ return ["Esc back", "↑/↓ scroll", "g/G ends", "PgUp/PgDn"];
271
+ }
272
+
273
+ protected detailFor(row: BoardRow): DetailContents {
274
+ switch (row.kind) {
275
+ case "session":
276
+ return {
277
+ title: `${sanitizeLine(row.row.session.label)}${row.row.self ? " · you" : ""}`,
278
+ subtitle: sanitizeLine(row.row.session.cwd),
279
+ lines: paneLines(row.row, this.now()),
280
+ tail: false,
281
+ facts: (width) => paneFacts(this.theme, row.row, this.now(), width),
282
+ };
283
+ case "message": {
284
+ const paths = messagePaths(row.message);
285
+ return {
286
+ title: `${sanitizeLine(row.message.fromLabel)}${SEPARATORS.item}${sanitizeLine(row.message.kind)}`,
287
+ subtitle: `${ago(row.message.at, this.now())}${paths ? `${SEPARATORS.item}paths: ${paths}` : ""}`,
288
+ text: messageProse(row.message),
289
+ tail: false,
290
+ };
291
+ }
292
+ case "claim": {
293
+ // A claim's pane is the session that holds it: identity plus everything it holds.
294
+ const owner = this.ownerRow(row.claim.owner);
295
+ return {
296
+ title: sanitizeLine(row.claim.ownerLabel),
297
+ subtitle: `holds ${sanitizeLine(row.claim.path)}${SEPARATORS.item}held ${ago(row.claim.at, this.now())}`,
298
+ lines: owner ? paneLines(owner, this.now()) : [],
299
+ tail: false,
300
+ ...(owner ? { facts: (width: number) => paneFacts(this.theme, owner, this.now(), width) } : {}),
301
+ };
302
+ }
303
+ }
304
+ }
305
+
306
+ protected detailById(id: string): { lines: string[] } | undefined {
307
+ const row = this.rows().find((candidate) => this.idOf(candidate) === id);
308
+ if (!row) return undefined;
309
+ switch (row.kind) {
310
+ case "session":
311
+ return { lines: paneLines(row.row, this.now()) };
312
+ case "claim": {
313
+ const owner = this.ownerRow(row.claim.owner);
314
+ return { lines: owner ? paneLines(owner, this.now()) : [] };
315
+ }
316
+ case "message":
317
+ // The pane's body is prose, which the base owns; its presence is all this answers.
318
+ return { lines: [] };
319
+ }
320
+ }
321
+
322
+ protected liveDetail(): boolean {
323
+ return true;
324
+ }
325
+
326
+ protected handleVerb(key: string, focused: BoardRow | undefined): void {
327
+ if (matchesKey(key, Key.tab)) {
328
+ this.cycleScope();
329
+ return;
330
+ }
331
+ if (key === "w") {
332
+ this.toggleWatch();
333
+ return;
334
+ }
335
+ if (key === "x" && focused?.kind === "claim") this.release(focused.claim);
336
+ }
337
+
338
+ protected handleDetailVerb(key: string, row: BoardRow): boolean {
339
+ if (key !== "x" || row.kind !== "claim") return false;
340
+ this.release(row.claim);
341
+ return true;
342
+ }
343
+
344
+ private me(): string {
345
+ return this.board?.status.me.id ?? "";
346
+ }
347
+
348
+ /** The row for the session with `id`, when it is still on the board. */
349
+ private ownerRow(id: string): SessionRow | undefined {
350
+ return sessionRows(this.board?.status).find((row) => row.session.id === id);
351
+ }
352
+
353
+ private nextScope(): BoardScope {
354
+ const index = SCOPE_ORDER.indexOf(this.scope);
355
+ return SCOPE_ORDER[(index + 1) % SCOPE_ORDER.length] ?? "sessions";
356
+ }
357
+
358
+ private cycleScope(): void {
359
+ this.scope = this.nextScope();
360
+ this.options.requestRender();
361
+ }
362
+
363
+ private watchCell(watching: { armed: boolean; wake: boolean }): string {
364
+ return paint(this.theme, "ok", watching.wake ? "watch wake" : "watch on");
365
+ }
366
+
367
+ private watchHint(): ScreenHint {
368
+ return this.board?.status.watching
369
+ ? { text: "w watch off", color: "warning" }
370
+ : { text: "w watch on", color: "accent" };
371
+ }
372
+
373
+ private toggleWatch(): void {
374
+ const armed = this.board?.status.watching !== undefined;
375
+ void this.callbacks
376
+ .toggleWatch(armed)
377
+ .then((line) => this.notify(line))
378
+ .catch((error) => this.notify(messageOf(error)));
379
+ }
380
+
381
+ private release(claim: ClaimRecord): void {
382
+ const path = sanitizeLine(claim.path);
383
+ if (claim.owner !== this.me()) {
384
+ this.notify(`${path} is ${sanitizeLine(claim.ownerLabel)}'s claim; not released.`);
385
+ return;
386
+ }
387
+ this.confirm(`Release ${path}? y/N`, () => {
388
+ void this.callbacks
389
+ .releaseClaim(claim.path)
390
+ .then((line) => this.notify(line))
391
+ .catch((error) => this.notify(messageOf(error)));
392
+ });
393
+ }
394
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Shared shapes for the session bus.
3
+ *
4
+ * These describe the files and the tool result only; the file I/O lives in
5
+ * `store.ts` and the lifecycle in `runtime.ts`.
6
+ */
7
+
8
+ import type { MessageKind, SessionMode } from "./protocol.ts";
9
+
10
+ /** One registered Pi session. */
11
+ export interface SessionRecord {
12
+ id: string;
13
+ label: string;
14
+ pid: number;
15
+ /** Working directory of the session; the worktree root when there is one. */
16
+ cwd: string;
17
+ /** Repository root the session belongs to, for relative claims. */
18
+ repoRoot: string;
19
+ /** The mode the session was started in. */
20
+ mode: SessionMode;
21
+ startedAt: number;
22
+ lastSeen: number;
23
+ }
24
+
25
+ /** One message waiting in a recipient's inbox. */
26
+ export interface BusMessage {
27
+ id: string;
28
+ from: string;
29
+ fromLabel: string;
30
+ /** Recipient session id. */
31
+ to: string;
32
+ toLabel: string;
33
+ kind: MessageKind;
34
+ text: string;
35
+ paths?: string[];
36
+ at: number;
37
+ }
38
+
39
+ /** One repository-relative claim. */
40
+ export interface ClaimRecord {
41
+ path: string;
42
+ owner: string;
43
+ ownerLabel: string;
44
+ at: number;
45
+ }
46
+
47
+ /** What `session status` returns. */
48
+ export interface SessionStatus {
49
+ me: { id: string; label: string };
50
+ sessions: Array<SessionRecord & { unread: number }>;
51
+ claims: ClaimRecord[];
52
+ /** The newest split in this repository, and who holds each unit, on an explicit status call. */
53
+ units?: {
54
+ team: string;
55
+ joined: number;
56
+ total: number;
57
+ rows: Array<{ unit: string; lead?: boolean; holder?: string }>;
58
+ };
59
+ /** Present only when this session armed its inbox watch. */
60
+ watching?: { armed: boolean; wake: boolean };
61
+ }
62
+
63
+ /** A conflict returned by `session claim`. */
64
+ export interface ClaimConflict {
65
+ path: string;
66
+ owner: string;
67
+ ownerLabel: string;
68
+ }
69
+
70
+ /**
71
+ * What the board reads in one poll: the bus as it stands, and this session's
72
+ * unread mail. Peeking never consumes, so the notice and `inbox` are unaffected.
73
+ */
74
+ export interface BoardSnapshot {
75
+ status: SessionStatus;
76
+ messages: BusMessage[];
77
+ }
78
+
79
+ /** Structured result carried in the `session` tool's details. */
80
+ export interface SessionDetails {
81
+ action: "status" | "send" | "inbox" | "claim" | "release" | "join" | "leave" | "watch" | "name";
82
+ /** With join/leave: the split and the unit, so a resumed transcript still says which. */
83
+ team?: string;
84
+ unit?: string;
85
+ status?: SessionStatus;
86
+ delivered?: string[];
87
+ messageId?: string;
88
+ messages?: BusMessage[];
89
+ claimed?: string[];
90
+ conflicts?: ClaimConflict[];
91
+ released?: string[];
92
+ notOwned?: string[];
93
+ /** Whether the inbox watch is on, and whether it raises a turn. */
94
+ watching?: { armed: boolean; wake: boolean };
95
+ /** With name: the label the bus settled on, which is what this session is called. */
96
+ label?: string;
97
+ error?: string;
98
+ }
@@ -210,7 +210,7 @@ for another agent (typically `worker`) to execute, and it works headlessly. It
210
210
  returns the plan as text and writes nothing. For **interactive** planning where
211
211
  the user reviews and approves a saved plan, use the plan-mode extension instead
212
212
  (`enter_plan_mode` / `write_plan` / `exit_plan_mode`), which operates on the
213
- main session and persists to `.pi/plans`. Plan mode keeps `subagent` available
213
+ main session and persists to the agent directory. Plan mode keeps `subagent` available
214
214
  for exploration but forces it read-only, so delegated recon is safe mid-plan.
215
215
 
216
216
  ## External agents
@@ -275,9 +275,18 @@ child's system prompt for behavior; the tool list is the enforcement.
275
275
  → grep /retry/ in ~/src
276
276
  → read ~/src/http.ts:40-120
277
277
  Retries are configured in http.ts:64...
278
- 2 turns ↑12.4k ↓1.1k R8.0k $0.0042 ctx:18.2k claude-sonnet-4-5
278
+
279
+ 2 turns ↑12.4k ↓1.1k R8.0k ctx:18.2k claude-sonnet-4-5
279
280
  ```
280
281
 
282
+ The footnote is the same in every mode: a blank line, the faint usage row, then —
283
+ when the run has more to show — the expand affordance, so the hint is always the
284
+ block's last line. The row carries turns, token counts, context size, and the
285
+ model — never money. A price needs a currency and a tariff, and those belong to
286
+ the status bar, which shows every child run as its own `↳` roll-up priced in the
287
+ configured currency (see [Non-goals](#non-goals)). A parallel or chain `Total:`
288
+ row sums the counters, and names a model only when every task ran the same one.
289
+
281
290
  **Expanded** (the `app.tools.expand` key, `ctrl+o` by default): the full task,
282
291
  every intermediate assistant message and tool call, the final output rendered as
283
292
  Markdown, and per-step/per-task usage with a total.
@@ -296,7 +305,9 @@ the first failing step also reports `stopped at step N`. Running tasks show an
296
305
  elapsed-time label on the shared 1s repaint timer (the package's common
297
306
  duration format, e.g. `12.0s`), and a finished run keeps its duration. A result
298
307
  that hit tool errors appends `N tool errors` to its
299
- usage line (and to the multi `Total:`).
308
+ usage line (and to the multi `Total:`). A usage payload the child reports with a
309
+ non-number (a string, `NaN`) contributes nothing, so a malformed event cannot
310
+ corrupt the row.
300
311
 
301
312
  A chain step's expanded `Task` section shows the step's original instruction,
302
313
  with `{previous}` left in place, rather than the substituted prior output that
@@ -317,15 +328,17 @@ step's `Output`.
317
328
  details; keeping it on disk is opt-in, parent-written, and bounded by age and
318
329
  count. It is an audit and handoff record, not a resume point: replaying a run
319
330
  whose writes already landed is not attempted.
320
- - **No cost attribution to the parent session.** A subagent's tokens and cost are
331
+ - **No cost attribution to the parent session.** A subagent's token counts are
321
332
  captured in the tool result and shown in the transcript, but they belong to a
322
- separate process and are not added to the dispatching session's totals. Instead,
323
- each finished run publishes `my-pi-agent/subagent-finished`
324
- (`SubagentFinishedEvent` in `lib/events.ts`) and bytefly's footer shows the
325
- session's child runs as their own `↳` roll-up, priced separately from the
326
- session total (the last 500 runs). The roll-up is per process: a resumed session
327
- starts from zero, and a run that never produced an assistant message publishes
328
- nothing.
333
+ separate process and are not added to the dispatching session's totals. The
334
+ child's own dollar cost stays in the tool details (and in a run's `meta.json`)
335
+ and is never printed in the transcript: only the status bar prices a delegated
336
+ run, in the configured currency and tariff. Each finished run publishes
337
+ `my-pi-agent/subagent-finished` (`SubagentFinishedEvent` in `lib/events.ts`)
338
+ and bytefly's footer shows the session's child runs as their own `↳` roll-up,
339
+ priced separately from the session total (the last 500 runs). The roll-up is
340
+ per process: a resumed session starts from zero, and a run that never produced
341
+ an assistant message publishes nothing.
329
342
 
330
343
  ## Limitations
331
344
 
@@ -421,7 +434,7 @@ step's `Output`.
421
434
  | `agents.ts` | Built-in agent definitions, external markdown discovery, and pool resolution. |
422
435
  | `schema.ts` | TypeBox params, limits, and `resolveMode`. |
423
436
  | `invocation.ts` | `pi` subprocess invocation and argument construction. |
424
- | `stream.ts` | JSON-event parsing, usage accounting, output helpers (pure). |
437
+ | `stream.ts` | JSON-event parsing, guarded usage accounting, output helpers (pure). |
425
438
  | `transcripts.ts` | Per-run transcript buffer, run ids, the store directory, and pruning. |
426
439
  | `config.ts` | The `subagent` section of `bytefly.json`: deadline, transcripts, retention. |
427
440
  | `run.ts` | Runs one subagent process and captures its result (including the background abort-as-result path). |
@@ -429,6 +442,6 @@ step's `Output`.
429
442
  | `background-tui.ts` | The `/agents` board: a `DockListScreen` with a live output pane and width-aware facts. |
430
443
  | `commands.ts` | The `/agents` command. |
431
444
  | `ui.ts` | The running-count footer chip and finish notices. |
432
- | `format.ts` | Token/usage/tool-call formatting (pure). |
445
+ | `format.ts` | Token/usage/total line and tool-call formatting (pure, money-free). |
433
446
  | `render.ts` | `renderCall` / `renderResult` transcript rendering. |
434
447
  | `types.ts` | Shared result and process types. |