@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,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 { idSuffix } 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 yet. Press w to join and listen.",
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: idSuffix(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, this.board?.status.onBus !== false);
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
+ * Whether this session is listening for mail, written when it joins.
25
+ *
26
+ * Absent means "not listening": a record written before this existed and a session
27
+ * that stopped listening read the same way, which is the honest answer for a peer
28
+ * deciding whether mail will reach it.
29
+ */
30
+ watching?: { armed: boolean; wake: boolean };
31
+ }
32
+
33
+ /** One message waiting in a recipient's inbox. */
34
+ export interface BusMessage {
35
+ id: string;
36
+ from: string;
37
+ fromLabel: string;
38
+ /** Recipient session id. */
39
+ to: string;
40
+ toLabel: string;
41
+ kind: MessageKind;
42
+ text: string;
43
+ paths?: string[];
44
+ at: number;
45
+ }
46
+
47
+ /** One repository-relative claim. */
48
+ export interface ClaimRecord {
49
+ path: string;
50
+ owner: string;
51
+ ownerLabel: string;
52
+ at: number;
53
+ }
54
+
55
+ /** What `session status` returns. */
56
+ export interface SessionStatus {
57
+ me: { id: string; label: string };
58
+ /** False while this session has only read the bus: it joins by naming itself. */
59
+ onBus: boolean;
60
+ sessions: Array<SessionRecord & { unread: number }>;
61
+ claims: ClaimRecord[];
62
+ /** Present only when this session armed its inbox watch. */
63
+ watching?: { armed: boolean; wake: boolean };
64
+ }
65
+
66
+ /** A conflict returned by `session claim`. */
67
+ export interface ClaimConflict {
68
+ path: string;
69
+ owner: string;
70
+ ownerLabel: string;
71
+ }
72
+
73
+ /**
74
+ * What the board reads in one poll: the bus as it stands, and this session's
75
+ * unread mail. Peeking never consumes, so the notice and `inbox` are unaffected.
76
+ */
77
+ export interface BoardSnapshot {
78
+ status: SessionStatus;
79
+ messages: BusMessage[];
80
+ }
81
+
82
+ /** Structured result carried in the `session` tool's details. */
83
+ export interface SessionDetails {
84
+ action: "status" | "send" | "inbox" | "claim" | "release" | "join";
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; `join` reports it. */
94
+ watching?: { armed: boolean; wake: boolean };
95
+ /** With join: the label the bus settled on, which is what this session is called. */
96
+ label?: string;
97
+ error?: string;
98
+ }
@@ -16,6 +16,7 @@
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
18
  import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
19
+ import { READ_DOC_TOOL, WEB_FETCH_TOOL, WEB_SEARCH_TOOL } from "../../lib/tool-names.ts";
19
20
 
20
21
  /** Where an agent definition came from. */
21
22
  export type AgentSource = "builtin" | "user" | "project";
@@ -44,7 +45,7 @@ export const BUILTIN_AGENTS: readonly AgentConfig[] = [
44
45
  {
45
46
  name: "explorer",
46
47
  description: "Fast codebase recon that returns compressed context for handoff to other agents",
47
- tools: ["read", "grep", "find", "ls"],
48
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL],
48
49
  source: "builtin",
49
50
  systemPrompt: `You are an explorer. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
50
51
 
@@ -94,7 +95,7 @@ Which file to look at first and why.`,
94
95
  name: "planner",
95
96
  description:
96
97
  "Delegated implementation planning in an isolated context (headless/chain; use plan mode for interactive planning)",
97
- tools: ["read", "grep", "find", "ls"],
98
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL],
98
99
  source: "builtin",
99
100
  systemPrompt: `You are a delegated planning specialist working in an isolated context. The caller hands you planning when it wants a plan produced without adding exploration to its own transcript — for example an explorer → planner → worker chain, or a headless run.
100
101
 
@@ -134,7 +135,7 @@ Keep the plan concrete. The worker agent will execute it verbatim.`,
134
135
  {
135
136
  name: "reviewer",
136
137
  description: "Code review specialist for quality and security analysis",
137
- tools: ["read", "grep", "find", "ls", "bash"],
138
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL, "bash"],
138
139
  source: "builtin",
139
140
  systemPrompt: `You are a senior code reviewer. Analyze code for quality, security, and maintainability.
140
141
 
@@ -168,7 +169,7 @@ Be specific with file paths and line numbers.`,
168
169
  {
169
170
  name: "worker",
170
171
  description: "General-purpose subagent with full capabilities and an isolated context window",
171
- tools: ["read", "write", "edit", "bash", "grep", "find", "ls"],
172
+ tools: ["read", "write", "edit", "bash", "grep", "find", "ls", READ_DOC_TOOL],
172
173
  source: "builtin",
173
174
  systemPrompt: `You are a worker agent with full capabilities. You operate in an isolated context window to handle delegated tasks without polluting the main conversation.
174
175
 
@@ -192,7 +193,7 @@ If handing off to another agent (e.g. reviewer), include:
192
193
  {
193
194
  name: "researcher",
194
195
  description: "Web research specialist that returns sourced findings on external questions",
195
- tools: ["read", "grep", "find", "ls", "web_search", "web_fetch"],
196
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL, WEB_SEARCH_TOOL, WEB_FETCH_TOOL],
196
197
  source: "builtin",
197
198
  systemPrompt: `You are a research specialist. You investigate questions using web_search and web_fetch, then return findings another agent can act on.
198
199
 
@@ -228,7 +229,7 @@ What the caller should do with this, in 2-3 sentences.`,
228
229
  {
229
230
  name: "tester",
230
231
  description: "Writes or updates tests for a change and runs them",
231
- tools: ["read", "write", "edit", "grep", "find", "ls", "bash"],
232
+ tools: ["read", "write", "edit", "grep", "find", "ls", READ_DOC_TOOL, "bash"],
232
233
  source: "builtin",
233
234
  systemPrompt: `You are a test engineer. You add or update tests for a change and run them.
234
235
 
@@ -260,7 +261,7 @@ What is still untested and why.`,
260
261
  {
261
262
  name: "debugger",
262
263
  description: "Root-cause analysis for a failing test or bug, read-only plus running tests",
263
- tools: ["read", "grep", "find", "ls", "bash"],
264
+ tools: ["read", "grep", "find", "ls", READ_DOC_TOOL, "bash"],
264
265
  source: "builtin",
265
266
  systemPrompt: `You are a debugging specialist. You find the root cause of a failure from evidence, not guesses.
266
267
 
@@ -295,7 +296,7 @@ What else the bug or the fix could affect.`,
295
296
  {
296
297
  name: "documenter",
297
298
  description: "Updates docs, READMEs, and changelog entries to match the code",
298
- tools: ["read", "write", "edit", "grep", "find", "ls", "bash"],
299
+ tools: ["read", "write", "edit", "grep", "find", "ls", READ_DOC_TOOL, "bash"],
299
300
  source: "builtin",
300
301
  systemPrompt: `You are a documentation specialist. You keep docs accurate and consistent with the code.
301
302
 
@@ -6,13 +6,15 @@
6
6
  * import for invocation concerns.
7
7
  */
8
8
 
9
+ import { WORKSPACE_READERS } from "../../lib/policy.ts";
10
+ import { WEB_FETCH_TOOL, WEB_SEARCH_TOOL } from "../../lib/tool-names.ts";
9
11
  import type { AgentConfig } from "./agents.ts";
10
12
  import type { DispatchDefaults } from "./types.ts";
11
13
 
12
14
  export { getPiInvocation, type PiInvocation } from "../../lib/pi-invocation.ts";
13
15
 
14
- /** Tools a read-only delegation may use: structured readers and web readers only. */
15
- export const READ_ONLY_TOOLS = ["read", "grep", "find", "ls", "web_search", "web_fetch"] as const;
16
+ /** Tools a read-only delegation may use: structured readers, documents, and web readers only. */
17
+ export const READ_ONLY_TOOLS = [...WORKSPACE_READERS, WEB_SEARCH_TOOL, WEB_FETCH_TOOL] as const;
16
18
 
17
19
  /** Appended to a read-only agent's system prompt; the `--tools` list is the real gate. */
18
20
  export const READ_ONLY_NOTE =