@gavin-hu/my-pi-agent 0.13.1 → 0.15.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 (237) hide show
  1. package/README.md +66 -41
  2. package/README.zh-CN.md +53 -36
  3. package/extensions/bytefly/README.md +321 -186
  4. package/extensions/bytefly/block/component.ts +7 -15
  5. package/extensions/bytefly/block/register.ts +6 -1
  6. package/extensions/bytefly/doctor/config.ts +4 -1
  7. package/extensions/bytefly/doctor/register.ts +24 -5
  8. package/extensions/bytefly/doctor/tools.ts +4 -0
  9. package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
  10. package/extensions/bytefly/footer/config.ts +10 -6
  11. package/extensions/bytefly/footer/price/config.ts +3 -2
  12. package/extensions/bytefly/footer/price/price.ts +5 -3
  13. package/extensions/bytefly/footer/price/rates.ts +63 -26
  14. package/extensions/bytefly/footer/render/status.ts +3 -3
  15. package/extensions/bytefly/header/component.ts +33 -10
  16. package/extensions/bytefly/header/config.ts +22 -0
  17. package/extensions/bytefly/header/register.ts +2 -1
  18. package/extensions/bytefly/index.ts +6 -6
  19. package/extensions/bytefly/notice/component.ts +8 -3
  20. package/extensions/bytefly/notify/config.ts +7 -12
  21. package/extensions/bytefly/notify/dispatch.ts +13 -5
  22. package/extensions/bytefly/panel/commands.ts +22 -2
  23. package/extensions/bytefly/panel/format.ts +17 -21
  24. package/extensions/bytefly/panel/settings-runtime.ts +86 -22
  25. package/extensions/bytefly/panel/settings.ts +279 -46
  26. package/extensions/bytefly/panel/themes.ts +32 -0
  27. package/extensions/bytefly/panel/tui/editing.ts +77 -0
  28. package/extensions/bytefly/panel/tui/entries.ts +24 -0
  29. package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
  30. package/extensions/bytefly/panel/tui/frame.ts +121 -4
  31. package/extensions/bytefly/panel/tui/settings-view.ts +98 -13
  32. package/extensions/bytefly/panel/tui.ts +176 -166
  33. package/extensions/bytefly/picker/discovery.ts +1 -0
  34. package/extensions/bytefly/picker/types.ts +2 -0
  35. package/extensions/bytefly/separator/component.ts +17 -6
  36. package/extensions/bytefly/separator/config.ts +15 -0
  37. package/extensions/bytefly/separator/format.ts +60 -6
  38. package/extensions/bytefly/separator/register.ts +68 -9
  39. package/extensions/bytefly/separator/types.ts +8 -0
  40. package/extensions/bytefly/update/cache.ts +58 -19
  41. package/extensions/bytefly/update/check.ts +35 -0
  42. package/extensions/bytefly/update/format.ts +76 -35
  43. package/extensions/bytefly/update/register.ts +144 -55
  44. package/extensions/bytefly/update/state.ts +4 -2
  45. package/extensions/bytefly/update/suppress.ts +66 -0
  46. package/extensions/bytefly/update/types.ts +15 -0
  47. package/extensions/bytefly/working/config.ts +4 -5
  48. package/extensions/bytefly/working/format.ts +24 -7
  49. package/extensions/bytefly/working/register.ts +34 -3
  50. package/extensions/check/README.md +253 -0
  51. package/extensions/check/commands.ts +128 -0
  52. package/extensions/check/config.ts +39 -0
  53. package/extensions/check/detect.ts +269 -0
  54. package/extensions/check/failures.ts +230 -0
  55. package/extensions/check/format.ts +135 -0
  56. package/extensions/check/index.ts +26 -0
  57. package/extensions/check/render.ts +185 -0
  58. package/extensions/check/runner.ts +176 -0
  59. package/extensions/check/runtime.ts +244 -0
  60. package/extensions/check/source.ts +80 -0
  61. package/extensions/check/tools.ts +173 -0
  62. package/extensions/check/tui.ts +195 -0
  63. package/extensions/check/types.ts +99 -0
  64. package/extensions/document/README.md +1 -1
  65. package/extensions/document/extract/pdf.ts +8 -16
  66. package/extensions/document/formats.ts +2 -2
  67. package/extensions/file-browse/README.md +66 -32
  68. package/extensions/file-browse/client.ts +95 -32
  69. package/extensions/file-browse/config.ts +9 -9
  70. package/extensions/file-browse/git.ts +37 -47
  71. package/extensions/file-browse/html.ts +39 -58
  72. package/extensions/file-browse/markdown.ts +110 -92
  73. package/extensions/file-browse/raw-html.ts +478 -0
  74. package/extensions/file-browse/router.ts +32 -70
  75. package/extensions/file-browse/server.ts +18 -56
  76. package/extensions/file-browse/urls.ts +80 -0
  77. package/extensions/goal/README.md +103 -47
  78. package/extensions/goal/clarify-parse.ts +4 -9
  79. package/extensions/goal/clarify.ts +27 -4
  80. package/extensions/goal/commands.ts +24 -11
  81. package/extensions/goal/format.ts +41 -10
  82. package/extensions/goal/index.ts +39 -16
  83. package/extensions/goal/plan-route.ts +51 -0
  84. package/extensions/goal/schema.ts +6 -23
  85. package/extensions/goal/state.ts +39 -57
  86. package/extensions/goal/tools.ts +26 -20
  87. package/extensions/goal/types.ts +11 -1
  88. package/extensions/job/README.md +20 -6
  89. package/extensions/job/format.ts +18 -68
  90. package/extensions/job/index.ts +26 -41
  91. package/extensions/job/log-writer.ts +208 -0
  92. package/extensions/job/logs.ts +3 -2
  93. package/extensions/job/process.ts +6 -99
  94. package/extensions/job/runtime.ts +42 -181
  95. package/extensions/job/schema.ts +1 -4
  96. package/extensions/job/tools.ts +1 -15
  97. package/extensions/job/tui.ts +117 -307
  98. package/extensions/job/ui.ts +7 -33
  99. package/extensions/memory/README.md +3 -1
  100. package/extensions/memory/errors.ts +5 -6
  101. package/extensions/memory/tui.ts +14 -24
  102. package/extensions/plan/README.md +106 -18
  103. package/extensions/plan/commands.ts +9 -5
  104. package/extensions/plan/format.ts +87 -0
  105. package/extensions/plan/index.ts +31 -15
  106. package/extensions/plan/plans.ts +2 -3
  107. package/extensions/plan/policy.ts +0 -6
  108. package/extensions/plan/runtime.ts +4 -2
  109. package/extensions/plan/steps.ts +37 -8
  110. package/extensions/plan/todos.ts +44 -3
  111. package/extensions/plan/tools/enter.ts +94 -0
  112. package/extensions/plan/tools/exit.ts +296 -0
  113. package/extensions/plan/tools/nested.ts +21 -0
  114. package/extensions/plan/tools/steps.ts +136 -0
  115. package/extensions/plan/tools/todo-seed.ts +76 -0
  116. package/extensions/plan/tools/write.ts +127 -0
  117. package/extensions/plan/tools.ts +18 -703
  118. package/extensions/plan/web-client.ts +6 -22
  119. package/extensions/plan/web-page.ts +19 -30
  120. package/extensions/plan/web-router.ts +34 -71
  121. package/extensions/plan/web.ts +13 -40
  122. package/extensions/question/answers.ts +3 -8
  123. package/extensions/question/tui.ts +6 -6
  124. package/extensions/rewind/README.md +95 -22
  125. package/extensions/rewind/checklist.ts +185 -0
  126. package/extensions/rewind/commands.ts +8 -0
  127. package/extensions/rewind/config.ts +3 -11
  128. package/extensions/rewind/format.ts +220 -2
  129. package/extensions/rewind/git.ts +74 -7
  130. package/extensions/rewind/hunk-picker.ts +114 -0
  131. package/extensions/rewind/patch.ts +88 -0
  132. package/extensions/rewind/path-picker.ts +67 -100
  133. package/extensions/rewind/paths.ts +93 -19
  134. package/extensions/rewind/restore.ts +53 -10
  135. package/extensions/rewind/review-tui.ts +320 -0
  136. package/extensions/rewind/review.ts +397 -0
  137. package/extensions/rewind/rewind-tui.ts +4 -3
  138. package/extensions/rewind/runtime.ts +94 -17
  139. package/extensions/subagent/README.md +118 -8
  140. package/extensions/subagent/background-tui.ts +208 -0
  141. package/extensions/subagent/background.ts +470 -0
  142. package/extensions/subagent/commands.ts +80 -0
  143. package/extensions/subagent/config.ts +62 -0
  144. package/extensions/subagent/format.ts +24 -1
  145. package/extensions/subagent/index.ts +116 -1
  146. package/extensions/subagent/orchestrate.ts +16 -3
  147. package/extensions/subagent/render.ts +25 -0
  148. package/extensions/subagent/run.ts +113 -49
  149. package/extensions/subagent/schema.ts +12 -0
  150. package/extensions/subagent/transcripts.ts +202 -0
  151. package/extensions/subagent/types.ts +23 -0
  152. package/extensions/subagent/ui.ts +50 -0
  153. package/extensions/todo/README.md +18 -13
  154. package/extensions/todo/format.ts +3 -2
  155. package/extensions/todo/runtime.ts +3 -2
  156. package/extensions/todo/schema.ts +8 -84
  157. package/extensions/todo/state.ts +5 -46
  158. package/extensions/todo/tools.ts +2 -1
  159. package/extensions/todo/types.ts +6 -15
  160. package/extensions/watch/README.md +16 -6
  161. package/extensions/watch/child.ts +150 -0
  162. package/extensions/watch/format.ts +8 -48
  163. package/extensions/watch/index.ts +42 -58
  164. package/extensions/watch/runtime.ts +49 -128
  165. package/extensions/watch/schema.ts +1 -4
  166. package/extensions/watch/tools.ts +1 -11
  167. package/extensions/watch/tui.ts +103 -273
  168. package/extensions/watch/ui.ts +5 -32
  169. package/extensions/web-access/README.md +1 -1
  170. package/extensions/web-access/fetch/page.ts +1 -1
  171. package/extensions/web-access/fetch/tool.ts +6 -2
  172. package/extensions/web-access/search/search.ts +2 -5
  173. package/extensions/wechat/README.md +3 -1
  174. package/extensions/wechat/client.ts +23 -0
  175. package/extensions/wechat/commands.ts +57 -42
  176. package/extensions/wechat/deliver.ts +5 -9
  177. package/extensions/wechat/errors.ts +3 -6
  178. package/extensions/wechat/inbound.ts +2 -5
  179. package/extensions/wechat/index.ts +2 -8
  180. package/extensions/wechat/outbound.ts +2 -4
  181. package/extensions/wechat/peer-worker.ts +3 -8
  182. package/extensions/wechat/poll.ts +10 -9
  183. package/extensions/wechat/presence.ts +35 -62
  184. package/extensions/wechat/reply-pipeline.ts +3 -1
  185. package/extensions/wechat/status-screen.ts +1 -1
  186. package/extensions/wechat/timer.ts +57 -0
  187. package/extensions/worktree/config.ts +8 -20
  188. package/extensions/worktree/enter.ts +7 -12
  189. package/extensions/worktree/git.ts +38 -69
  190. package/extensions/worktree/index.ts +5 -4
  191. package/extensions/worktree/registry.ts +7 -7
  192. package/extensions/worktree/root-tools.ts +2 -2
  193. package/extensions/worktree/status.ts +3 -2
  194. package/lib/README.md +22 -8
  195. package/lib/config.ts +83 -12
  196. package/lib/errors.ts +12 -0
  197. package/lib/format.ts +56 -0
  198. package/lib/git/read.ts +8 -0
  199. package/lib/goal-state.ts +23 -8
  200. package/lib/http/headers.ts +54 -0
  201. package/lib/http/index.ts +13 -0
  202. package/lib/http/response.ts +46 -0
  203. package/lib/http/router.ts +52 -0
  204. package/lib/http/server.ts +91 -0
  205. package/lib/lines.ts +77 -0
  206. package/lib/pdf.ts +82 -0
  207. package/lib/plan-entry.ts +71 -0
  208. package/lib/plan-state.ts +34 -0
  209. package/lib/report.ts +99 -0
  210. package/lib/requirements.ts +25 -19
  211. package/lib/spawn.ts +170 -0
  212. package/lib/todo-state.ts +110 -23
  213. package/lib/tool-names.ts +7 -0
  214. package/lib/tool-result.ts +33 -0
  215. package/lib/tui/README.md +68 -3
  216. package/lib/tui/chrome.ts +28 -4
  217. package/lib/tui/columns.ts +6 -1
  218. package/lib/tui/confirm.ts +65 -0
  219. package/lib/tui/dock-screen.ts +410 -0
  220. package/lib/tui/glyphs.ts +2 -1
  221. package/lib/tui/indent.ts +1 -1
  222. package/lib/tui/index.ts +2 -0
  223. package/lib/tui/rows.ts +21 -0
  224. package/lib/tui/status-controller.ts +35 -1
  225. package/lib/tui/tool.ts +59 -1
  226. package/lib/web/components.ts +20 -1
  227. package/lib/web/markup.ts +32 -0
  228. package/lib/web/runtime.ts +50 -0
  229. package/package.json +23 -5
  230. package/skills/init/SKILL.md +165 -0
  231. package/skills/init/references/agents-md-quality.md +103 -0
  232. package/skills/init/references/interview-questions.md +81 -0
  233. package/skills/refactor/SKILL.md +141 -0
  234. package/themes/bytefly-dark.json +2 -3
  235. package/themes/bytefly-light.json +2 -3
  236. package/extensions/watch/process.ts +0 -63
  237. package/extensions/web-access/fetch/pdf.ts +0 -45
@@ -2,7 +2,9 @@
2
2
 
3
3
  A `subagent` tool that hands a task to a specialized agent running in its own
4
4
  `pi --mode json` process. The subagent gets a fresh context window, so broad
5
- exploration or parallel work does not fill the main conversation.
5
+ exploration or parallel work does not fill the main conversation. A single run
6
+ can also start in the background and be inspected, collected, or cancelled with
7
+ `/agents`.
6
8
 
7
9
  ```bash
8
10
  pi --extension ./extensions/subagent # load just this extension
@@ -16,6 +18,10 @@ How the `subagent` call and its result look in the transcript — the call line
16
18
 
17
19
  <img src="../../assets/tools/subagent.png" alt="The subagent tool's call and result: the call line and an indented body with the two tasks, each agent's section rule, its summary, and the usage total" width="100%">
18
20
 
21
+ ## Screens
22
+
23
+ <img src="../../assets/agents.png" alt="The /agents screen: running and finished background helpers with status, task, elapsed time, and usage" width="100%">
24
+
19
25
  ## What it does
20
26
 
21
27
  - **Isolated context.** Each subagent is a separate `pi` process with its own
@@ -25,10 +31,17 @@ How the `subagent` call and its result look in the transcript — the call line
25
31
  - **External agents.** Optionally load markdown agents from `~/.pi/agents`
26
32
  (user) and the nearest `.pi/agents` (project). They are opt-in per scope and
27
33
  never replace the built-ins silently.
28
- - **Three modes.** One agent, a parallel batch, or a sequential chain.
34
+ - **Three modes, plus background.** One agent, a parallel batch, or a sequential
35
+ chain; a single run can also continue after the tool returns and be collected
36
+ from `/agents`. A running helper adds a `↳ N` footer chip, and a finish notice
37
+ points back at `/agents`.
29
38
  - **Streaming.** Tool calls and progress update live in the transcript while the
30
39
  subagent runs; the `app.tools.expand` key (`ctrl+o` by default) expands the
31
40
  full task, tool calls, Markdown output, and usage.
41
+ - **Bounded runs.** A per-run deadline kills a child that hangs, and reports it
42
+ as a failed run instead of rejecting the whole call.
43
+ - **Optional transcripts.** Opt-in per-run stream under `.pi/subagents/`,
44
+ written by the parent and pruned by age and count.
32
45
  - **Inherited model.** Subagents use the dispatching session's model and thinking
33
46
  level unless an agent overrides them (none do).
34
47
 
@@ -40,6 +53,7 @@ How the `subagent` call and its result look in the transcript — the call line
40
53
  | Field | Value |
41
54
  |---|---|
42
55
  | `agent` + `task` | Single mode: one built-in/external agent and its task. |
56
+ | `background` | Single mode only: start the run and return immediately; manage it with `/agents`. |
43
57
  | `tasks[]` | Parallel mode: independent `{agent, task, cwd?}` entries, max 8. |
44
58
  | `chain[]` | Chain mode: ordered `{agent, task, cwd?}` steps, max 8; `{previous}` is replaced by the prior step's final output. |
45
59
  | `cwd` | Optional working directory for the spawned process (single mode; per-step for the others). A relative path resolves against the active root. |
@@ -56,6 +70,21 @@ How the `subagent` call and its result look in the transcript — the call line
56
70
  { "agent": "explorer", "task": "Find every place the HTTP client is retried", "cwd": "." }
57
71
  ```
58
72
 
73
+ ### Background
74
+
75
+ ```json
76
+ { "agent": "explorer", "task": "Map the auth module", "background": true }
77
+ ```
78
+
79
+ The tool returns immediately with a run id such as `a2`. `/agents` lists every
80
+ run with its status, elapsed time, and usage; `Enter` opens the run, `c` collects
81
+ a finished result into the conversation, and `x` cancels a running one. While a
82
+ run is active, its detail pane follows a bounded preview of the latest output,
83
+ and a `↳ N` chip in the footer counts running helpers. A finish notice points
84
+ back at `/agents`; the finalized result replaces the preview when the run
85
+ finishes. A background run is session-owned and is bounded by the same timeout
86
+ and transcript settings as a foreground run.
87
+
59
88
  ### Parallel
60
89
 
61
90
  Runs up to 8 tasks, at most 4 at once. Independent tasks only.
@@ -88,6 +117,55 @@ at most 8 steps (like the parallel task cap).
88
117
  }
89
118
  ```
90
119
 
120
+ ## Command
121
+
122
+ | Command | What it does |
123
+ |---|---|
124
+ | `/agents` | Open the background-run board: inspect, collect, or cancel. |
125
+
126
+ The board is TUI-only for actions; RPC, print, and JSON print the run list.
127
+ Collecting sends the result as a user message, which starts the next turn.
128
+
129
+ ## Configuration
130
+
131
+ Read from the shared `bytefly.json` section `subagent` (global then project),
132
+ once per session. Keys and bounds live in the
133
+ [configuration page](../../docs/configuration.md#subagent--delegated-runs);
134
+ the behaviour is here.
135
+
136
+ - **`timeoutMs`** (default ten minutes, `0` disables) bounds one child run. The
137
+ limit is per run, not per call, so a chain is bounded step by step and a
138
+ parallel batch bounds each task separately. On expiry the child gets the same
139
+ `SIGTERM`, then `SIGKILL` after 5 s, as a user abort — but the run fails
140
+ instead of throwing: a chain stops at that step, a parallel call counts a
141
+ failure, and the model sees `Timed out after 10m00s`.
142
+ - **`transcripts`** (`off`, `failed`, or `all`; default `off`) keeps finished
143
+ runs on disk, all of them or only the failed ones.
144
+ - **`retentionDays`** (default 14) and **`maxRuns`** (default 100) bound the
145
+ store. It is pruned at `session_start`, and only while transcripts are on.
146
+ - **`backgroundMax`** (default 4, `1`–`8`) bounds concurrently running
147
+ background children. Finished runs do not consume a slot. A run that is still
148
+ running when the session ends is interrupted; the next `/agents` shows the
149
+ start entry without a finish.
150
+
151
+ ### Transcripts
152
+
153
+ ```
154
+ .pi/subagents/20261015-143012-a1b2c3/
155
+ meta.json agent, task, cwd, model, timings, exit, status, usage
156
+ events.jsonl the child's raw stdout lines, verbatim
157
+ stderr.log only when the child wrote any
158
+ ```
159
+
160
+ The store is resolved against the **session** cwd, so inside a worktree it lands
161
+ in the main checkout's `.pi/` and outlives that worktree — the anchoring the
162
+ `job` registry uses. Outside a repository it falls back to
163
+ `<agent-dir>/subagents`. The parent writes it and the child never learns it
164
+ exists, so `readOnly`, the `--tools` allowlist, and the project-agent trust gate
165
+ are unchanged. Writing is best-effort: an unwritable checkout records the failure
166
+ in the result and never fails a run. The stream is buffered in the parent up to
167
+ 32 MiB, then truncated with a marker rather than silently dropped.
168
+
91
169
  ## Security
92
170
 
93
171
  A subagent is a real `pi` process and shares Pi's OS permissions. The built-in
@@ -206,7 +284,10 @@ Markdown, and per-step/per-task usage with a total.
206
284
 
207
285
  A failed or aborted result always shows its `errorMessage`, falling back to the
208
286
  subprocess `stderr`, in both the collapsed and expanded views, in every mode. A
209
- failure with neither falls back to `Error: Subprocess exited with code N.` and an
287
+ run that exceeded its deadline reports `Timed out after …` the same way, and the
288
+ expanded view adds the transcript path, the truncation marker, or why the
289
+ transcript could not be written. A failure with neither falls back to
290
+ `Error: Subprocess exited with code N.` and an
210
291
  `[exit N]` bracket, so a non-zero exit is never silent. A child terminated by a
211
292
  signal (an OOM kill, a crash) has no exit code, so it is recorded as
212
293
  `Terminated by <signal>` and reported as a failure. Parallel and chain
@@ -232,8 +313,10 @@ step's `Output`.
232
313
  does not reimplement tool execution or model streaming.
233
314
  - **No pinned models.** Built-in agents set no `model`, so a subprocess inherits
234
315
  the dispatching session's provider/model and thinking level.
235
- - **No persistence.** Subagent transcripts live only in the tool result details;
236
- they are not written to the parent session.
316
+ - **No persistence by default.** A run's transcript lives in the tool result
317
+ details; keeping it on disk is opt-in, parent-written, and bounded by age and
318
+ count. It is an audit and handoff record, not a resume point: replaying a run
319
+ whose writes already landed is not attempted.
237
320
  - **No cost attribution to the parent session.** A subagent's tokens and cost are
238
321
  captured in the tool result and shown in the transcript, but they belong to a
239
322
  separate process and are not added to the dispatching session's totals. Instead,
@@ -244,6 +327,19 @@ step's `Output`.
244
327
  starts from zero, and a run that never produced an assistant message publishes
245
328
  nothing.
246
329
 
330
+ ## Limitations
331
+
332
+ - Background runs are single-mode only. Parallel and chain calls wait as before.
333
+ - A background run is session-owned: `/tree` does not move it, and a process
334
+ restart cannot resume it. A start entry without a finish is shown as
335
+ `interrupted`.
336
+ - The collected output is capped at 50 KiB; the optional transcript keeps the
337
+ full stream. The `/agents` detail keeps only a bounded live preview, and a
338
+ finished run keeps its bounded output in the session record rather than the
339
+ child's full result.
340
+ - A finish notice appears in the TUI, but there is no OS notification; the run
341
+ remains listed in `/agents`.
342
+
247
343
  ## Pi integration
248
344
 
249
345
  | Aspect | Detail |
@@ -253,8 +349,8 @@ step's `Output`.
253
349
  | Annotations | `readOnlyHint: false`, `openWorldHint: true` |
254
350
  | Output schema | none; the full result lives in tool-result `details` |
255
351
  | Execution | spawns `pi --mode json` subprocesses; external agents discovered from `<agent-dir>/agents` and `.pi/agents` |
256
- | State storage | tool-result `details` only (no session persistence) |
257
- | Lifecycle | none; the tool is stateless and spawns per call |
352
+ | State storage | tool-result `details`; optional per-run transcripts under `.pi/subagents/`; background start/finish entries (`subagent-background`) on the session branch |
353
+ | Lifecycle | `session_start` loads the config, prunes expired transcripts, and attaches the footer chip; `session_shutdown` aborts every live background child and clears the chip |
258
354
 
259
355
  ## Design notes
260
356
 
@@ -284,6 +380,14 @@ step's `Output`.
284
380
  results arrive as `message_end` (messages, usage, model, stop reason) and
285
381
  failures as `tool_execution_end`. A tool error does not by itself fail the run,
286
382
  because the agent may recover.
383
+ - **A deadline reuses the abort kill path but not its outcome.** One kill
384
+ closure serves both triggers, so the SIGTERM → 5 s SIGKILL escalation is
385
+ identical; only the reporting differs, because a deadline must leave the
386
+ finished steps in `details` while a user abort rejects the call.
387
+ - **The transcript is the parent's.** Teeing the child's stdout and stderr in
388
+ `spawnAndCollect` and flushing in `runSingleAgent`'s `finally` means the
389
+ artifact exists even on the abort path, and the child needs no tool, no prompt
390
+ line, and no writable store.
287
391
  - **Bounded parallel output.** Parallel mode runs at most 4 of up to 8 tasks at
288
392
  once and caps each model-facing section at 50 KiB, appending an omission notice
289
393
  pointing at the tool details. The full results stay in `details` for rendering.
@@ -318,7 +422,13 @@ step's `Output`.
318
422
  | `schema.ts` | TypeBox params, limits, and `resolveMode`. |
319
423
  | `invocation.ts` | `pi` subprocess invocation and argument construction. |
320
424
  | `stream.ts` | JSON-event parsing, usage accounting, output helpers (pure). |
321
- | `run.ts` | Runs one subagent process and captures its result. |
425
+ | `transcripts.ts` | Per-run transcript buffer, run ids, the store directory, and pruning. |
426
+ | `config.ts` | The `subagent` section of `bytefly.json`: deadline, transcripts, retention. |
427
+ | `run.ts` | Runs one subagent process and captures its result (including the background abort-as-result path). |
428
+ | `background.ts` | Background run entries, replay, capacity, cancel, collect, and shutdown. |
429
+ | `background-tui.ts` | The `/agents` board: a `DockListScreen` with a live output pane and width-aware facts. |
430
+ | `commands.ts` | The `/agents` command. |
431
+ | `ui.ts` | The running-count footer chip and finish notices. |
322
432
  | `format.ts` | Token/usage/tool-call formatting (pure). |
323
433
  | `render.ts` | `renderCall` / `renderResult` transcript rendering. |
324
434
  | `types.ts` | Shared result and process types. |
@@ -0,0 +1,208 @@
1
+ /**
2
+ * The `/agents` board: background runs, live status, and a per-run detail.
3
+ *
4
+ * The board reads runs from a getter on every render, so a finish or cancel is
5
+ * reflected without rebuilding the component. The frame, the id-anchored cursor,
6
+ * the navigation and wheel keys, and the pane lifecycle come from the shared
7
+ * `DockListScreen`, so this module owns only the run vocabulary: the row, the
8
+ * status view, the key/value facts, the hints, and what `c` and `x` do.
9
+ *
10
+ * Two of that base's optional members are why the board can be one: the pane is
11
+ * live (`liveDetail`), because a running child's output arrives between frames
12
+ * and nothing polls it, and the facts are a `facts` hook, because the key/value
13
+ * rows are built from the current width.
14
+ */
15
+
16
+ import type { Theme } from "@earendil-works/pi-coding-agent";
17
+ import { sanitizeLine, stripControlChars } from "../../lib/format.ts";
18
+ import type { ScreenHint, ViewportRowsSource } from "../../lib/tui/chrome.ts";
19
+ import { DockListScreen, type DetailContents } from "../../lib/tui/dock-screen.ts";
20
+ import { GLYPHS, SEPARATORS } from "../../lib/tui/glyphs.ts";
21
+ import { keyValueRow, statusRow } from "../../lib/tui/rows.ts";
22
+ import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
23
+ import type { BackgroundRun, BackgroundStatus } from "./background.ts";
24
+ import { canCollect } from "./background.ts";
25
+ import { formatBackgroundDuration, formatUsageStats } from "./format.ts";
26
+
27
+ /** One status's glyph, role, and readable label. */
28
+ function statusView(status: BackgroundStatus): { glyph: string; role: TuiRole; label: string } {
29
+ switch (status) {
30
+ case "running":
31
+ return { glyph: GLYPHS.jobsRunning, role: "warn", label: "running" };
32
+ case "finished":
33
+ return { glyph: GLYPHS.check, role: "ok", label: "finished" };
34
+ case "failed":
35
+ return { glyph: GLYPHS.jobsFailure, role: "bad", label: "failed" };
36
+ case "cancelled":
37
+ return { glyph: GLYPHS.stopped, role: "warn", label: "cancelled" };
38
+ case "timedout":
39
+ return { glyph: GLYPHS.warn, role: "bad", label: "timed out" };
40
+ case "interrupted":
41
+ return { glyph: GLYPHS.unknown, role: "faint", label: "interrupted" };
42
+ }
43
+ }
44
+
45
+ /** A short task label for a list row. */
46
+ function taskLabel(run: BackgroundRun): string {
47
+ return sanitizeLine(run.task).split("\n")[0] ?? "";
48
+ }
49
+
50
+ /** The output rows for the detail pane. */
51
+ function outputLines(run: BackgroundRun): string[] {
52
+ const text =
53
+ run.output?.trim() ||
54
+ (run.status === "running"
55
+ ? "Still running…"
56
+ : run.status === "interrupted"
57
+ ? "Interrupted before it finished."
58
+ : "(no output recorded)");
59
+ return text.split("\n").map((line) => stripControlChars(line).replaceAll("\t", " ").slice(0, 2_000));
60
+ }
61
+
62
+ /** The detail key/value rows. */
63
+ function detailRows(run: BackgroundRun, now: number, theme: Theme, width: number): string[] {
64
+ const usage = run.usage ? formatUsageStats(run.usage) : "(not reported)";
65
+ const rows = [
66
+ keyValueRow(theme, width, { label: "Task", value: taskLabel(run), labelWidth: 12 }),
67
+ keyValueRow(theme, width, { label: "Cwd", value: sanitizeLine(run.cwd), labelWidth: 12 }),
68
+ keyValueRow(theme, width, { label: "Status", value: statusView(run.status).label, labelWidth: 12 }),
69
+ keyValueRow(theme, width, { label: "Duration", value: formatBackgroundDuration(run, now), labelWidth: 12 }),
70
+ keyValueRow(theme, width, { label: "Usage", value: usage, labelWidth: 12 }),
71
+ ];
72
+ if (run.errorMessage) {
73
+ rows.push(keyValueRow(theme, width, { label: "Error", value: sanitizeLine(run.errorMessage), labelWidth: 12 }));
74
+ }
75
+ if (run.artifactPath) {
76
+ rows.push(
77
+ keyValueRow(theme, width, { label: "Transcript", value: sanitizeLine(run.artifactPath), labelWidth: 12 }),
78
+ );
79
+ }
80
+ return rows;
81
+ }
82
+
83
+ export interface AgentsBoardOptions {
84
+ runs: () => readonly BackgroundRun[];
85
+ theme: Theme;
86
+ now?: () => number;
87
+ /** Collect a finished run; the command closes the board and sends it. */
88
+ onCollect: (runId: string) => void;
89
+ /** Cancel a running run; the board stays open. */
90
+ onCancel: (runId: string) => void;
91
+ onClose: () => void;
92
+ requestRender: () => void;
93
+ viewportRows?: ViewportRowsSource;
94
+ }
95
+
96
+ /** The list/detail board for background subagent runs. */
97
+ export class AgentsBoardComponent extends DockListScreen<BackgroundRun> {
98
+ constructor(private readonly board: AgentsBoardOptions) {
99
+ super({
100
+ theme: board.theme,
101
+ title: "Agents",
102
+ detailTitle: "Run",
103
+ emptyList: "No background subagents yet.",
104
+ emptyDetail: "(no output recorded)",
105
+ requestRender: board.requestRender,
106
+ onClose: board.onClose,
107
+ viewportRowsSource: board.viewportRows,
108
+ });
109
+ }
110
+
111
+ private get now(): number {
112
+ return (this.board.now ?? Date.now)();
113
+ }
114
+
115
+ /** `c` collects a finished run and `x` cancels a running one, in either mode. */
116
+ private act(key: string, run: BackgroundRun | undefined): boolean {
117
+ if (!run) return false;
118
+ if (key === "c" && canCollect(run)) {
119
+ this.board.onCollect(run.id);
120
+ return true;
121
+ }
122
+ if (key === "x" && run.status === "running") {
123
+ this.board.onCancel(run.id);
124
+ return true;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ protected rows(): readonly BackgroundRun[] {
130
+ return this.board.runs();
131
+ }
132
+
133
+ protected idOf(run: BackgroundRun): string {
134
+ return run.id;
135
+ }
136
+
137
+ protected rowLine(run: BackgroundRun, width: number, selected: boolean): string {
138
+ const view = statusView(run.status);
139
+ const usage = run.usage ? formatUsageStats(run.usage) : "";
140
+ const trailing = [view.label, formatBackgroundDuration(run, this.now), usage].filter(Boolean).join(SEPARATORS.item);
141
+ return statusRow(this.theme, width, {
142
+ selected,
143
+ glyph: view.glyph,
144
+ glyphRole: view.role,
145
+ label: `${run.id} ${sanitizeLine(run.agent)} ${taskLabel(run)}`,
146
+ labelRole: selected ? "accent" : "label",
147
+ trailing,
148
+ });
149
+ }
150
+
151
+ /** The board has never carried a focus block: the detail pane states the facts. */
152
+ protected detailBlock(): string[] {
153
+ return [];
154
+ }
155
+
156
+ protected summaryCells(rows: readonly BackgroundRun[]): string[] {
157
+ const running = rows.filter((run) => run.status === "running").length;
158
+ const cells = [paint(this.theme, "meta", `${rows.length} run${rows.length === 1 ? "" : "s"}`)];
159
+ if (running > 0) cells.push(paint(this.theme, "meta", `${running} running`));
160
+ return cells;
161
+ }
162
+
163
+ protected listHints(): ScreenHint[] {
164
+ const focused = this.focused;
165
+ const hints: ScreenHint[] = ["Esc close", { text: "Enter detail", color: "accent" }];
166
+ if (focused && canCollect(focused)) hints.push({ text: "c collect", color: "accent" });
167
+ if (focused?.status === "running") hints.push({ text: "x cancel", color: "warning" });
168
+ hints.push("↑/↓ select");
169
+ return hints;
170
+ }
171
+
172
+ protected detailHints(run: BackgroundRun | undefined): ScreenHint[] {
173
+ const hints: ScreenHint[] = ["Esc back"];
174
+ if (run && canCollect(run)) hints.push({ text: "c collect", color: "accent" });
175
+ if (run?.status === "running") hints.push({ text: "x cancel", color: "warning" });
176
+ hints.push("j/k scroll");
177
+ return hints;
178
+ }
179
+
180
+ protected detailFor(run: BackgroundRun): DetailContents {
181
+ return {
182
+ title: `${run.id} ${sanitizeLine(run.agent)}`,
183
+ subtitle: `${statusView(run.status).label}${SEPARATORS.item}${formatBackgroundDuration(run, this.now)}`,
184
+ // The key/value rows need the current width, so they are rebuilt per frame.
185
+ facts: (width) => detailRows(run, this.now, this.theme, width),
186
+ lines: outputLines(run),
187
+ };
188
+ }
189
+
190
+ /** The output a live run keeps producing; a removed run has none, which closes the pane. */
191
+ protected detailById(id: string): { lines: string[] } | undefined {
192
+ const run = this.rows().find((entry) => entry.id === id);
193
+ return run ? { lines: outputLines(run) } : undefined;
194
+ }
195
+
196
+ /** Re-read the pane on every frame: a child's output arrives without a poll. */
197
+ protected liveDetail(): boolean {
198
+ return true;
199
+ }
200
+
201
+ protected handleVerb(key: string, focused: BackgroundRun | undefined): void {
202
+ this.act(key, focused);
203
+ }
204
+
205
+ protected handleDetailVerb(key: string, run: BackgroundRun): boolean {
206
+ return this.act(key, run);
207
+ }
208
+ }