@gavin-hu/my-pi-agent 0.14.0 → 0.15.1

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 (229) hide show
  1. package/README.md +54 -34
  2. package/README.zh-CN.md +44 -28
  3. package/extensions/bytefly/README.md +193 -140
  4. package/extensions/bytefly/block/component.ts +7 -15
  5. package/extensions/bytefly/block/register.ts +6 -1
  6. package/extensions/bytefly/doctor/register.ts +24 -5
  7. package/extensions/bytefly/doctor/tools.ts +4 -0
  8. package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
  9. package/extensions/bytefly/footer/config.ts +10 -6
  10. package/extensions/bytefly/footer/price/price.ts +5 -3
  11. package/extensions/bytefly/footer/price/rates.ts +63 -26
  12. package/extensions/bytefly/footer/render/status.ts +3 -3
  13. package/extensions/bytefly/header/component.ts +3 -2
  14. package/extensions/bytefly/index.ts +3 -3
  15. package/extensions/bytefly/notice/component.ts +8 -3
  16. package/extensions/bytefly/notify/config.ts +6 -11
  17. package/extensions/bytefly/notify/dispatch.ts +13 -5
  18. package/extensions/bytefly/panel/commands.ts +21 -1
  19. package/extensions/bytefly/panel/format.ts +5 -2
  20. package/extensions/bytefly/panel/settings-runtime.ts +26 -6
  21. package/extensions/bytefly/panel/settings.ts +41 -4
  22. package/extensions/bytefly/panel/themes.ts +32 -0
  23. package/extensions/bytefly/panel/tui/editing.ts +77 -0
  24. package/extensions/bytefly/panel/tui/entries.ts +24 -0
  25. package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
  26. package/extensions/bytefly/panel/tui/frame.ts +121 -4
  27. package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
  28. package/extensions/bytefly/panel/tui.ts +94 -152
  29. package/extensions/bytefly/picker/discovery.ts +1 -0
  30. package/extensions/bytefly/picker/types.ts +2 -0
  31. package/extensions/bytefly/separator/component.ts +14 -4
  32. package/extensions/bytefly/update/cache.ts +58 -19
  33. package/extensions/bytefly/update/check.ts +35 -0
  34. package/extensions/bytefly/update/format.ts +76 -35
  35. package/extensions/bytefly/update/register.ts +144 -55
  36. package/extensions/bytefly/update/state.ts +4 -2
  37. package/extensions/bytefly/update/suppress.ts +66 -0
  38. package/extensions/bytefly/update/types.ts +15 -0
  39. package/extensions/check/README.md +253 -0
  40. package/extensions/check/commands.ts +128 -0
  41. package/extensions/check/config.ts +39 -0
  42. package/extensions/check/detect.ts +269 -0
  43. package/extensions/check/failures.ts +230 -0
  44. package/extensions/check/format.ts +135 -0
  45. package/extensions/check/index.ts +26 -0
  46. package/extensions/check/render.ts +185 -0
  47. package/extensions/check/runner.ts +176 -0
  48. package/extensions/check/runtime.ts +244 -0
  49. package/extensions/check/source.ts +80 -0
  50. package/extensions/check/tools.ts +173 -0
  51. package/extensions/check/tui.ts +195 -0
  52. package/extensions/check/types.ts +99 -0
  53. package/extensions/document/README.md +1 -1
  54. package/extensions/document/extract/pdf.ts +8 -16
  55. package/extensions/document/formats.ts +2 -2
  56. package/extensions/file-browse/README.md +126 -37
  57. package/extensions/file-browse/client.ts +98 -33
  58. package/extensions/file-browse/config.ts +24 -9
  59. package/extensions/file-browse/files.ts +183 -48
  60. package/extensions/file-browse/fuzzy.ts +65 -11
  61. package/extensions/file-browse/git.ts +37 -47
  62. package/extensions/file-browse/html.ts +97 -171
  63. package/extensions/file-browse/live-client.ts +158 -0
  64. package/extensions/file-browse/live.ts +222 -0
  65. package/extensions/file-browse/markdown.ts +110 -92
  66. package/extensions/file-browse/raw-html.ts +478 -0
  67. package/extensions/file-browse/router.ts +65 -78
  68. package/extensions/file-browse/server.ts +75 -59
  69. package/extensions/file-browse/styles.ts +124 -0
  70. package/extensions/file-browse/urls.ts +80 -0
  71. package/extensions/goal/README.md +103 -47
  72. package/extensions/goal/clarify-parse.ts +4 -9
  73. package/extensions/goal/clarify.ts +27 -4
  74. package/extensions/goal/commands.ts +24 -11
  75. package/extensions/goal/format.ts +41 -10
  76. package/extensions/goal/index.ts +39 -16
  77. package/extensions/goal/plan-route.ts +51 -0
  78. package/extensions/goal/schema.ts +6 -23
  79. package/extensions/goal/state.ts +39 -57
  80. package/extensions/goal/tools.ts +26 -20
  81. package/extensions/goal/types.ts +11 -1
  82. package/extensions/job/README.md +20 -6
  83. package/extensions/job/format.ts +18 -68
  84. package/extensions/job/index.ts +26 -41
  85. package/extensions/job/log-writer.ts +208 -0
  86. package/extensions/job/logs.ts +3 -2
  87. package/extensions/job/process.ts +6 -99
  88. package/extensions/job/runtime.ts +42 -181
  89. package/extensions/job/schema.ts +1 -4
  90. package/extensions/job/tools.ts +1 -15
  91. package/extensions/job/tui.ts +117 -307
  92. package/extensions/job/ui.ts +7 -33
  93. package/extensions/memory/README.md +1 -1
  94. package/extensions/memory/errors.ts +5 -6
  95. package/extensions/memory/tui.ts +14 -24
  96. package/extensions/plan/README.md +94 -18
  97. package/extensions/plan/commands.ts +9 -5
  98. package/extensions/plan/format.ts +87 -0
  99. package/extensions/plan/index.ts +31 -15
  100. package/extensions/plan/plans.ts +2 -3
  101. package/extensions/plan/policy.ts +0 -6
  102. package/extensions/plan/runtime.ts +4 -2
  103. package/extensions/plan/steps.ts +37 -8
  104. package/extensions/plan/todos.ts +44 -3
  105. package/extensions/plan/tools/enter.ts +94 -0
  106. package/extensions/plan/tools/exit.ts +296 -0
  107. package/extensions/plan/tools/nested.ts +21 -0
  108. package/extensions/plan/tools/steps.ts +136 -0
  109. package/extensions/plan/tools/todo-seed.ts +76 -0
  110. package/extensions/plan/tools/write.ts +127 -0
  111. package/extensions/plan/tools.ts +18 -703
  112. package/extensions/plan/web-client.ts +6 -22
  113. package/extensions/plan/web-page.ts +19 -30
  114. package/extensions/plan/web-router.ts +32 -79
  115. package/extensions/plan/web.ts +21 -42
  116. package/extensions/question/answers.ts +3 -8
  117. package/extensions/question/tui.ts +6 -6
  118. package/extensions/rewind/README.md +95 -22
  119. package/extensions/rewind/checklist.ts +192 -0
  120. package/extensions/rewind/commands.ts +8 -0
  121. package/extensions/rewind/config.ts +3 -11
  122. package/extensions/rewind/format.ts +220 -2
  123. package/extensions/rewind/git.ts +101 -21
  124. package/extensions/rewind/hunk-picker.ts +114 -0
  125. package/extensions/rewind/patch.ts +88 -0
  126. package/extensions/rewind/path-picker.ts +67 -100
  127. package/extensions/rewind/paths.ts +93 -19
  128. package/extensions/rewind/restore.ts +53 -10
  129. package/extensions/rewind/review-tui.ts +320 -0
  130. package/extensions/rewind/review.ts +397 -0
  131. package/extensions/rewind/rewind-tui.ts +4 -3
  132. package/extensions/rewind/runtime.ts +94 -17
  133. package/extensions/rewind/snapshot.ts +13 -4
  134. package/extensions/subagent/README.md +61 -6
  135. package/extensions/subagent/background-tui.ts +208 -0
  136. package/extensions/subagent/background.ts +470 -0
  137. package/extensions/subagent/commands.ts +80 -0
  138. package/extensions/subagent/config.ts +4 -0
  139. package/extensions/subagent/format.ts +24 -1
  140. package/extensions/subagent/index.ts +91 -2
  141. package/extensions/subagent/render.ts +13 -0
  142. package/extensions/subagent/run.ts +17 -16
  143. package/extensions/subagent/schema.ts +12 -0
  144. package/extensions/subagent/types.ts +11 -0
  145. package/extensions/subagent/ui.ts +50 -0
  146. package/extensions/todo/README.md +18 -13
  147. package/extensions/todo/format.ts +3 -2
  148. package/extensions/todo/runtime.ts +3 -2
  149. package/extensions/todo/schema.ts +8 -84
  150. package/extensions/todo/state.ts +5 -46
  151. package/extensions/todo/tools.ts +2 -1
  152. package/extensions/todo/types.ts +6 -15
  153. package/extensions/watch/README.md +14 -6
  154. package/extensions/watch/child.ts +150 -0
  155. package/extensions/watch/format.ts +8 -48
  156. package/extensions/watch/index.ts +42 -58
  157. package/extensions/watch/runtime.ts +49 -128
  158. package/extensions/watch/schema.ts +1 -4
  159. package/extensions/watch/tools.ts +1 -11
  160. package/extensions/watch/tui.ts +103 -273
  161. package/extensions/watch/ui.ts +5 -32
  162. package/extensions/web-access/README.md +1 -1
  163. package/extensions/web-access/fetch/page.ts +1 -1
  164. package/extensions/web-access/fetch/tool.ts +6 -2
  165. package/extensions/web-access/search/search.ts +2 -5
  166. package/extensions/wechat/README.md +1 -1
  167. package/extensions/wechat/client.ts +23 -0
  168. package/extensions/wechat/commands.ts +57 -42
  169. package/extensions/wechat/deliver.ts +5 -9
  170. package/extensions/wechat/errors.ts +3 -6
  171. package/extensions/wechat/inbound.ts +2 -5
  172. package/extensions/wechat/index.ts +2 -8
  173. package/extensions/wechat/outbound.ts +2 -4
  174. package/extensions/wechat/peer-worker.ts +3 -8
  175. package/extensions/wechat/poll.ts +10 -9
  176. package/extensions/wechat/presence.ts +35 -62
  177. package/extensions/wechat/reply-pipeline.ts +3 -1
  178. package/extensions/wechat/status-screen.ts +1 -1
  179. package/extensions/wechat/timer.ts +57 -0
  180. package/extensions/worktree/config.ts +8 -20
  181. package/extensions/worktree/enter.ts +7 -12
  182. package/extensions/worktree/git.ts +38 -69
  183. package/extensions/worktree/index.ts +5 -4
  184. package/extensions/worktree/registry.ts +7 -7
  185. package/extensions/worktree/root-tools.ts +2 -2
  186. package/extensions/worktree/status.ts +15 -9
  187. package/lib/README.md +23 -8
  188. package/lib/config.ts +50 -3
  189. package/lib/errors.ts +12 -0
  190. package/lib/format.ts +50 -0
  191. package/lib/git/read.ts +28 -0
  192. package/lib/goal-state.ts +23 -8
  193. package/lib/http/headers.ts +74 -0
  194. package/lib/http/index.ts +13 -0
  195. package/lib/http/response.ts +46 -0
  196. package/lib/http/router.ts +52 -0
  197. package/lib/http/server.ts +109 -0
  198. package/lib/lines.ts +77 -0
  199. package/lib/pdf.ts +82 -0
  200. package/lib/plan-entry.ts +71 -0
  201. package/lib/plan-state.ts +34 -0
  202. package/lib/pool.ts +62 -0
  203. package/lib/report.ts +99 -0
  204. package/lib/requirements.ts +25 -19
  205. package/lib/spawn.ts +170 -0
  206. package/lib/todo-state.ts +110 -23
  207. package/lib/tool-names.ts +7 -0
  208. package/lib/tool-result.ts +33 -0
  209. package/lib/tui/README.md +68 -3
  210. package/lib/tui/chrome.ts +28 -4
  211. package/lib/tui/columns.ts +6 -1
  212. package/lib/tui/confirm.ts +65 -0
  213. package/lib/tui/dock-screen.ts +418 -0
  214. package/lib/tui/glyphs.ts +2 -1
  215. package/lib/tui/indent.ts +1 -1
  216. package/lib/tui/index.ts +2 -0
  217. package/lib/tui/rows.ts +21 -0
  218. package/lib/tui/status-controller.ts +35 -1
  219. package/lib/tui/tool.ts +59 -1
  220. package/lib/web/components.ts +20 -1
  221. package/lib/web/markup.ts +32 -0
  222. package/lib/web/runtime.ts +50 -0
  223. package/package.json +22 -5
  224. package/skills/init/SKILL.md +165 -0
  225. package/skills/init/references/agents-md-quality.md +103 -0
  226. package/skills/init/references/interview-questions.md +81 -0
  227. package/skills/refactor/SKILL.md +141 -0
  228. package/extensions/watch/process.ts +0 -63
  229. 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,7 +31,10 @@ 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.
@@ -44,6 +53,7 @@ How the `subagent` call and its result look in the transcript — the call line
44
53
  | Field | Value |
45
54
  |---|---|
46
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`. |
47
57
  | `tasks[]` | Parallel mode: independent `{agent, task, cwd?}` entries, max 8. |
48
58
  | `chain[]` | Chain mode: ordered `{agent, task, cwd?}` steps, max 8; `{previous}` is replaced by the prior step's final output. |
49
59
  | `cwd` | Optional working directory for the spawned process (single mode; per-step for the others). A relative path resolves against the active root. |
@@ -60,6 +70,21 @@ How the `subagent` call and its result look in the transcript — the call line
60
70
  { "agent": "explorer", "task": "Find every place the HTTP client is retried", "cwd": "." }
61
71
  ```
62
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
+
63
88
  ### Parallel
64
89
 
65
90
  Runs up to 8 tasks, at most 4 at once. Independent tasks only.
@@ -92,11 +117,20 @@ at most 8 steps (like the parallel task cap).
92
117
  }
93
118
  ```
94
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
+
95
129
  ## Configuration
96
130
 
97
131
  Read from the shared `bytefly.json` section `subagent` (global then project),
98
132
  once per session. Keys and bounds live in the
99
- [configuration reference](../../docs/references/bytefly-config.md#subagent--delegated-runs);
133
+ [configuration page](../../docs/configuration.md#subagent--delegated-runs);
100
134
  the behaviour is here.
101
135
 
102
136
  - **`timeoutMs`** (default ten minutes, `0` disables) bounds one child run. The
@@ -109,6 +143,10 @@ the behaviour is here.
109
143
  runs on disk, all of them or only the failed ones.
110
144
  - **`retentionDays`** (default 14) and **`maxRuns`** (default 100) bound the
111
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.
112
150
 
113
151
  ### Transcripts
114
152
 
@@ -289,6 +327,19 @@ step's `Output`.
289
327
  starts from zero, and a run that never produced an assistant message publishes
290
328
  nothing.
291
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
+
292
343
  ## Pi integration
293
344
 
294
345
  | Aspect | Detail |
@@ -298,8 +349,8 @@ step's `Output`.
298
349
  | Annotations | `readOnlyHint: false`, `openWorldHint: true` |
299
350
  | Output schema | none; the full result lives in tool-result `details` |
300
351
  | Execution | spawns `pi --mode json` subprocesses; external agents discovered from `<agent-dir>/agents` and `.pi/agents` |
301
- | State storage | tool-result `details`; optional per-run transcripts under `.pi/subagents/`, never the session |
302
- | Lifecycle | `session_start` loads the config and prunes expired transcripts; the tool itself is stateless |
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 |
303
354
 
304
355
  ## Design notes
305
356
 
@@ -373,7 +424,11 @@ step's `Output`.
373
424
  | `stream.ts` | JSON-event parsing, usage accounting, output helpers (pure). |
374
425
  | `transcripts.ts` | Per-run transcript buffer, run ids, the store directory, and pruning. |
375
426
  | `config.ts` | The `subagent` section of `bytefly.json`: deadline, transcripts, retention. |
376
- | `run.ts` | Runs one subagent process and captures its result. |
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. |
377
432
  | `format.ts` | Token/usage/tool-call formatting (pure). |
378
433
  | `render.ts` | `renderCall` / `renderResult` transcript rendering. |
379
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.focusedRow;
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
+ }