pi-background-tasks 2.5.0 → 2.6.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 (203) hide show
  1. package/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
  2. package/README.md +70 -25
  3. package/TESTING.md +42 -2
  4. package/TEST_PLAN.md +7 -4
  5. package/dist/extensions/anthropic-attribution-child.js +4 -0
  6. package/dist/extensions/anthropic-attribution-child.js.map +1 -0
  7. package/dist/extensions/anthropic-attribution.js +107 -0
  8. package/dist/extensions/anthropic-attribution.js.map +1 -0
  9. package/dist/extensions/background-tasks.js +2 -0
  10. package/dist/extensions/background-tasks.js.map +1 -0
  11. package/dist/extensions/delegate-child.js +2 -0
  12. package/dist/extensions/delegate-child.js.map +1 -0
  13. package/dist/extensions/fusion-child.js +2 -0
  14. package/dist/extensions/fusion-child.js.map +1 -0
  15. package/dist/package.json +5 -0
  16. package/dist/src/core/anthropic-attribution-path.js +24 -0
  17. package/dist/src/core/anthropic-attribution-path.js.map +1 -0
  18. package/dist/src/core/anthropic-attribution.js +2247 -0
  19. package/dist/src/core/anthropic-attribution.js.map +1 -0
  20. package/dist/src/core/attested-pi-contract.js +5 -0
  21. package/dist/src/core/attested-pi-contract.js.map +1 -0
  22. package/dist/src/core/attested-pi-run.js +749 -0
  23. package/dist/src/core/attested-pi-run.js.map +1 -0
  24. package/dist/src/core/canonical-json.js +19 -0
  25. package/dist/src/core/canonical-json.js.map +1 -0
  26. package/dist/src/core/common.js +787 -0
  27. package/dist/src/core/common.js.map +1 -0
  28. package/dist/src/core/config.js +78 -0
  29. package/dist/src/core/config.js.map +1 -0
  30. package/dist/src/core/context/parent-snapshot.js +75 -0
  31. package/dist/src/core/context/parent-snapshot.js.map +1 -0
  32. package/dist/src/core/context/token-budget.js +631 -0
  33. package/dist/src/core/context/token-budget.js.map +1 -0
  34. package/dist/src/core/context/visible-conversation-v2.js +390 -0
  35. package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
  36. package/dist/src/core/delegate/artifacts.js +357 -0
  37. package/dist/src/core/delegate/artifacts.js.map +1 -0
  38. package/dist/src/core/delegate/budget.js +266 -0
  39. package/dist/src/core/delegate/budget.js.map +1 -0
  40. package/dist/src/core/delegate/facade-contract.js +7 -0
  41. package/dist/src/core/delegate/facade-contract.js.map +1 -0
  42. package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
  43. package/dist/src/core/delegate/hook-contract.js +124 -0
  44. package/dist/src/core/delegate/hook-contract.js.map +1 -0
  45. package/dist/src/core/delegate/launch.js +366 -0
  46. package/dist/src/core/delegate/launch.js.map +1 -0
  47. package/dist/src/core/delegate/result-package.js +293 -0
  48. package/dist/src/core/delegate/result-package.js.map +1 -0
  49. package/dist/src/core/delegate/runner.js +398 -0
  50. package/dist/src/core/delegate/runner.js.map +1 -0
  51. package/dist/src/core/delegate/seed.js +334 -0
  52. package/dist/src/core/delegate/seed.js.map +1 -0
  53. package/dist/src/core/delegate/types.js +103 -0
  54. package/dist/src/core/delegate/types.js.map +1 -0
  55. package/dist/src/core/durable-fs.js +372 -0
  56. package/dist/src/core/durable-fs.js.map +1 -0
  57. package/dist/src/core/extension-api.js +382 -0
  58. package/dist/src/core/extension-api.js.map +1 -0
  59. package/dist/src/core/fusion/artifacts.js +654 -0
  60. package/dist/src/core/fusion/artifacts.js.map +1 -0
  61. package/dist/src/core/fusion/budget.js +949 -0
  62. package/dist/src/core/fusion/budget.js.map +1 -0
  63. package/dist/src/core/fusion/child-protocol.js +176 -0
  64. package/dist/src/core/fusion/child-protocol.js.map +1 -0
  65. package/dist/src/core/fusion/claude-cache.js +156 -0
  66. package/dist/src/core/fusion/claude-cache.js.map +1 -0
  67. package/dist/src/core/fusion/clean-context.js +64 -0
  68. package/dist/src/core/fusion/clean-context.js.map +1 -0
  69. package/dist/src/core/fusion/config.js +349 -0
  70. package/dist/src/core/fusion/config.js.map +1 -0
  71. package/dist/src/core/fusion/context.js +184 -0
  72. package/dist/src/core/fusion/context.js.map +1 -0
  73. package/dist/src/core/fusion/evaluation.js +696 -0
  74. package/dist/src/core/fusion/evaluation.js.map +1 -0
  75. package/dist/src/core/fusion/facade-contract.js +3 -0
  76. package/dist/src/core/fusion/facade-contract.js.map +1 -0
  77. package/dist/src/core/fusion/orchestrator.js +867 -0
  78. package/dist/src/core/fusion/orchestrator.js.map +1 -0
  79. package/dist/src/core/fusion/output-contract.js +27 -0
  80. package/dist/src/core/fusion/output-contract.js.map +1 -0
  81. package/dist/src/core/fusion/pi-child.js +1741 -0
  82. package/dist/src/core/fusion/pi-child.js.map +1 -0
  83. package/dist/src/core/fusion/prompts.js +260 -0
  84. package/dist/src/core/fusion/prompts.js.map +1 -0
  85. package/dist/src/core/fusion/result-package.js +857 -0
  86. package/dist/src/core/fusion/result-package.js.map +1 -0
  87. package/dist/src/core/fusion/source-policy.js +259 -0
  88. package/dist/src/core/fusion/source-policy.js.map +1 -0
  89. package/dist/src/core/fusion/types.js +207 -0
  90. package/dist/src/core/fusion/types.js.map +1 -0
  91. package/dist/src/core/fusion/web-fetch.js +777 -0
  92. package/dist/src/core/fusion/web-fetch.js.map +1 -0
  93. package/dist/src/core/fusion/workflows.js +126 -0
  94. package/dist/src/core/fusion/workflows.js.map +1 -0
  95. package/dist/src/core/lazy-module.js +181 -0
  96. package/dist/src/core/lazy-module.js.map +1 -0
  97. package/dist/src/core/pi-launch.js +431 -0
  98. package/dist/src/core/pi-launch.js.map +1 -0
  99. package/dist/src/core/registry.js +3331 -0
  100. package/dist/src/core/registry.js.map +1 -0
  101. package/dist/src/core/reload-shell-owner.js +1396 -0
  102. package/dist/src/core/reload-shell-owner.js.map +1 -0
  103. package/dist/src/core/shell-policy.js +80 -0
  104. package/dist/src/core/shell-policy.js.map +1 -0
  105. package/dist/src/core/task-durable.js +51 -0
  106. package/dist/src/core/task-durable.js.map +1 -0
  107. package/dist/src/core/update-check.js +92 -0
  108. package/dist/src/core/update-check.js.map +1 -0
  109. package/dist/src/core/windows-taskkill.js +185 -0
  110. package/dist/src/core/windows-taskkill.js.map +1 -0
  111. package/dist/src/delegate-child-extension.js +776 -0
  112. package/dist/src/delegate-child-extension.js.map +1 -0
  113. package/dist/src/delegate-extension.js +746 -0
  114. package/dist/src/delegate-extension.js.map +1 -0
  115. package/dist/src/extension.js +953 -0
  116. package/dist/src/extension.js.map +1 -0
  117. package/dist/src/fusion-child-extension.js +760 -0
  118. package/dist/src/fusion-child-extension.js.map +1 -0
  119. package/dist/src/fusion-extension.js +1030 -0
  120. package/dist/src/fusion-extension.js.map +1 -0
  121. package/dist/src/ui/background-tasks-manager.js +689 -0
  122. package/dist/src/ui/background-tasks-manager.js.map +1 -0
  123. package/dist/src/ui/fusion-model-selector.js +277 -0
  124. package/dist/src/ui/fusion-model-selector.js.map +1 -0
  125. package/docs/INDEX.md +35 -33
  126. package/docs/api/eventbus-v1.md +21 -5
  127. package/docs/choose-a-workflow.md +2 -0
  128. package/docs/commands/bg-clear.md +3 -3
  129. package/docs/commands/bg-update.md +3 -3
  130. package/docs/commands/bg.md +18 -6
  131. package/docs/commands/claude-cache.md +4 -4
  132. package/docs/commands/fusion-models.md +49 -4
  133. package/docs/commands/fusion.md +3 -3
  134. package/docs/commands/jobs.md +3 -3
  135. package/docs/commands/kill.md +3 -3
  136. package/docs/commands/logs.md +3 -3
  137. package/docs/commands/task-manager.md +5 -5
  138. package/docs/concepts/completion-delivery.md +6 -2
  139. package/docs/getting-started.md +7 -1
  140. package/docs/manifest.json +228 -42
  141. package/docs/operations/configuration.md +58 -5
  142. package/docs/operations/releasing.md +4 -2
  143. package/docs/operations/testing.md +22 -0
  144. package/docs/operations/troubleshooting.md +6 -2
  145. package/docs/read-before-edit.md +44 -31
  146. package/docs/reference/runtime-contracts.md +87 -51
  147. package/docs/reference/shortcuts-and-dock.md +28 -14
  148. package/docs/subsystems/anthropic-attribution.md +23 -11
  149. package/docs/subsystems/attested-pi-runs.md +12 -7
  150. package/docs/subsystems/background-task-runtime.md +53 -11
  151. package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
  152. package/docs/subsystems/delegation.md +16 -3
  153. package/docs/subsystems/docs-freshness-gate.md +14 -7
  154. package/docs/subsystems/fusion.md +36 -11
  155. package/docs/subsystems/host-ui-and-telemetry.md +22 -9
  156. package/docs/tools/bg_delegate.md +3 -1
  157. package/docs/tools/bg_kill.md +3 -1
  158. package/docs/tools/bg_logs.md +3 -1
  159. package/docs/tools/bg_result.md +5 -3
  160. package/docs/tools/bg_run.md +31 -5
  161. package/docs/tools/bg_run_pi_attested.md +3 -1
  162. package/docs/tools/bg_status.md +3 -1
  163. package/docs/tools/fusion_investigate.md +3 -1
  164. package/docs/tools/fusion_reason.md +3 -1
  165. package/docs/tools/fusion_research.md +3 -1
  166. package/docs/tools/fusion_validate.md +3 -1
  167. package/extensions/anthropic-attribution-child.ts +3 -0
  168. package/extensions/anthropic-attribution.ts +166 -1
  169. package/package.json +12 -10
  170. package/src/core/anthropic-attribution-path.ts +5 -4
  171. package/src/core/anthropic-attribution.ts +293 -113
  172. package/src/core/attested-pi-contract.ts +4 -0
  173. package/src/core/attested-pi-run.ts +468 -81
  174. package/src/core/canonical-json.ts +21 -0
  175. package/src/core/common.ts +450 -40
  176. package/src/core/config.ts +121 -0
  177. package/src/core/context/visible-conversation-v2.ts +3 -6
  178. package/src/core/delegate/artifacts.ts +13 -9
  179. package/src/core/delegate/budget.ts +13 -6
  180. package/src/core/delegate/facade-contract.ts +6 -0
  181. package/src/core/delegate/result-package.ts +32 -14
  182. package/src/core/delegate/runner.ts +129 -58
  183. package/src/core/delegate/seed.ts +5 -7
  184. package/src/core/durable-fs.ts +178 -30
  185. package/src/core/extension-api.ts +36 -8
  186. package/src/core/fusion/artifacts.ts +17 -16
  187. package/src/core/fusion/clean-context.ts +8 -5
  188. package/src/core/fusion/config.ts +2 -1
  189. package/src/core/fusion/context.ts +2 -6
  190. package/src/core/fusion/facade-contract.ts +2 -0
  191. package/src/core/fusion/orchestrator.ts +3 -3
  192. package/src/core/fusion/prompts.ts +1 -1
  193. package/src/core/fusion/result-package.ts +259 -72
  194. package/src/core/fusion/source-policy.ts +43 -17
  195. package/src/core/lazy-module.ts +215 -0
  196. package/src/core/pi-launch.ts +390 -65
  197. package/src/core/registry.ts +2010 -381
  198. package/src/core/reload-shell-owner.ts +1662 -0
  199. package/src/core/shell-policy.ts +134 -0
  200. package/src/core/task-durable.ts +67 -0
  201. package/src/delegate-extension.ts +404 -144
  202. package/src/extension.ts +387 -114
  203. package/src/fusion-extension.ts +202 -96
@@ -0,0 +1,689 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { formatSize } from '@earendil-works/pi-coding-agent';
3
+ import { matchesKey, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
4
+ import { boundedRead, compactWhitespace, formatCompactNumber, formatDuration, taskDisplayName, truncateChars, } from '../core/common.js';
5
+ const STATUS_INTERVAL_MS = 1000;
6
+ // Detail-view scrollback reservoir. Larger than the model-facing log cap because
7
+ // this is a UI-only read; a local 128KiB tail read per second is negligible and
8
+ // gives thousands of lines of scrollback when the user pauses the live tail.
9
+ const DETAIL_TAIL_BYTES = 128 * 1024;
10
+ const LIST_VISIBLE_ROWS = 14;
11
+ const DETAIL_VISIBLE_OUTPUT_LINES = 12;
12
+ const LIGHT_BLUE_BG = '\x1b[48;2;183;223;255m';
13
+ const LIGHT_BLUE_FG = '\x1b[38;2;11;70;110m';
14
+ const LIGHT_BLUE_BORDER = '\x1b[38;2;83;160;215m';
15
+ const ANSI_RESET = '\x1b[0m';
16
+ function formatTime(timestamp) {
17
+ return new Date(timestamp).toLocaleTimeString();
18
+ }
19
+ function formatCount(value) {
20
+ return value.toString();
21
+ }
22
+ function pluralSuffix(count) {
23
+ return count === 1 ? '' : 's';
24
+ }
25
+ function formatExitCodeText(exitCode) {
26
+ return typeof exitCode === 'number' ? ` exit=${formatCount(exitCode)}` : '';
27
+ }
28
+ function formatPidText(pid) {
29
+ return typeof pid === 'number' ? ` · pid ${formatCount(pid)}` : '';
30
+ }
31
+ /** Normalize raw output bytes into display lines, dropping only the trailing empty line from a final newline. */
32
+ function toOutputLines(content) {
33
+ return content
34
+ .replace(/\r/g, '')
35
+ .split('\n')
36
+ .filter((line, index, array) => line.length > 0 || index < array.length - 1);
37
+ }
38
+ function padAnsi(value, width) {
39
+ return value + ' '.repeat(Math.max(0, width - visibleWidth(value)));
40
+ }
41
+ function lightBlue(value) {
42
+ return `${LIGHT_BLUE_BG}${LIGHT_BLUE_FG}${value}${ANSI_RESET}`;
43
+ }
44
+ function blueBorder(value) {
45
+ return `${LIGHT_BLUE_BORDER}${value}${ANSI_RESET}`;
46
+ }
47
+ function statusLabel(status) {
48
+ if (status === 'completed')
49
+ return 'done';
50
+ if (status === 'failed')
51
+ return 'error';
52
+ if (status === 'killed')
53
+ return 'stopped';
54
+ return 'running';
55
+ }
56
+ function statusColor(theme, status, text = statusLabel(status)) {
57
+ if (status === 'completed')
58
+ return theme.fg('success', text);
59
+ if (status === 'failed')
60
+ return theme.fg('error', text);
61
+ if (status === 'killed')
62
+ return theme.fg('warning', text);
63
+ return theme.fg('accent', text);
64
+ }
65
+ function taskAge(task, now = Date.now()) {
66
+ return formatDuration((task.endTime ?? now) - task.startTime);
67
+ }
68
+ function formatContextUsage(task) {
69
+ const usage = task.contextUsage;
70
+ if (!usage?.contextWindow)
71
+ return '—';
72
+ const window = formatCompactNumber(usage.contextWindow);
73
+ if (usage.percent === null || usage.tokens === null)
74
+ return `?/${window}`;
75
+ return `${usage.percent.toFixed(1)}%/${window}`;
76
+ }
77
+ function formatContextDetail(task) {
78
+ const usage = task.contextUsage;
79
+ if (!usage?.contextWindow)
80
+ return 'not reported by this background task';
81
+ const window = formatCompactNumber(usage.contextWindow);
82
+ if (usage.percent === null || usage.tokens === null)
83
+ return `unknown tokens / ${window} window`;
84
+ return `${usage.percent.toFixed(1)}% of ${window} window (${formatCompactNumber(usage.tokens)} tokens)`;
85
+ }
86
+ function formatTokenUsage(task) {
87
+ const usage = task.tokenUsage;
88
+ if (!usage || usage.totalTokens <= 0)
89
+ return '';
90
+ return `tok ${formatCompactNumber(usage.totalTokens)}`;
91
+ }
92
+ function formatTokenDetail(task) {
93
+ const usage = task.tokenUsage;
94
+ if (!usage || usage.totalTokens <= 0)
95
+ return 'not reported by this background task';
96
+ const parts = [
97
+ `input ${formatCompactNumber(usage.input)}`,
98
+ `output ${formatCompactNumber(usage.output)}`,
99
+ `cache read ${formatCompactNumber(usage.cacheRead)}`,
100
+ `cache write ${formatCompactNumber(usage.cacheWrite)}`,
101
+ `total ${formatCompactNumber(usage.totalTokens)}`,
102
+ ];
103
+ return parts.join(' · ');
104
+ }
105
+ function formatToolUsage(task) {
106
+ const usage = task.toolUsage;
107
+ if (!usage || (usage.total <= 0 && usage.failed <= 0))
108
+ return '';
109
+ const total = formatCount(usage.total);
110
+ const failed = formatCount(usage.failed);
111
+ return usage.failed > 0 ? `tools ${total}/${failed} failed` : `tools ${total}`;
112
+ }
113
+ function formatToolDetail(task) {
114
+ const usage = task.toolUsage;
115
+ if (!usage || (usage.total <= 0 && usage.failed <= 0))
116
+ return 'not reported by this background task';
117
+ const byName = Object.entries(usage.byName)
118
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
119
+ .slice(0, 6)
120
+ .map(([name, count]) => `${name} ${formatCount(count)}`);
121
+ const parts = [`${formatCount(usage.total)} total`];
122
+ if (usage.failed > 0)
123
+ parts.push(`${formatCount(usage.failed)} failed`);
124
+ parts.push(...byName);
125
+ return parts.join(' · ');
126
+ }
127
+ function shortModelName(model) {
128
+ const slash = model.lastIndexOf('/');
129
+ return slash >= 0 ? model.slice(slash + 1) : model;
130
+ }
131
+ function formatModel(task) {
132
+ if (!task.model)
133
+ return '';
134
+ return `model ${shortModelName(task.model)}`;
135
+ }
136
+ function formatModelDetail(task) {
137
+ if (!task.model)
138
+ return 'not reported by this background task';
139
+ return task.model;
140
+ }
141
+ function contextColor(theme, task, text) {
142
+ const percent = task.contextUsage?.percent ?? 0;
143
+ if (percent > 90)
144
+ return theme.fg('error', text);
145
+ if (percent > 70)
146
+ return theme.fg('warning', text);
147
+ return theme.fg('dim', text);
148
+ }
149
+ function sortTasksForUi(tasks) {
150
+ const rank = (task) => task.status === 'running' ? 0 : task.status === 'failed' ? 1 : task.status === 'killed' ? 2 : 3;
151
+ return [...tasks].sort((a, b) => {
152
+ const rankDiff = rank(a) - rank(b);
153
+ if (rankDiff !== 0)
154
+ return rankDiff;
155
+ return (b.endTime ?? b.startTime) - (a.endTime ?? a.startTime);
156
+ });
157
+ }
158
+ export class BackgroundTasksManager {
159
+ tui;
160
+ theme;
161
+ done;
162
+ options;
163
+ mode = 'list';
164
+ selectedIndex = 0;
165
+ listScroll = 0;
166
+ detailTaskId;
167
+ showHistory = false;
168
+ detailLines = [];
169
+ detailFollow = true;
170
+ detailScrollTop = 0;
171
+ tailBytesRead = 0;
172
+ tailTotalBytes = 0;
173
+ tailTruncated = false;
174
+ tailError;
175
+ actionMessage;
176
+ confirmStopAllArmed = false;
177
+ lastBytesByTask = new Map();
178
+ recentActivityByTask = new Map();
179
+ refreshTimer;
180
+ constructor(tui, theme, done, options) {
181
+ this.tui = tui;
182
+ this.theme = theme;
183
+ this.done = done;
184
+ this.options = options;
185
+ if (options.initialTaskId) {
186
+ this.detailTaskId = options.initialTaskId;
187
+ this.mode = 'detail';
188
+ this.options.markSeen(options.initialTaskId);
189
+ void this.refreshTail();
190
+ }
191
+ else {
192
+ const allTasks = options.getTasks();
193
+ const hasRunning = allTasks.some((task) => task.status === 'running');
194
+ const hasFinished = allTasks.some((task) => task.status !== 'running');
195
+ if (!hasRunning && hasFinished)
196
+ this.showHistory = true;
197
+ }
198
+ this.refreshTimer = setInterval(() => {
199
+ if (this.mode === 'detail')
200
+ void this.refreshTail();
201
+ this.tui.requestRender();
202
+ }, STATUS_INTERVAL_MS);
203
+ }
204
+ dispose() {
205
+ clearInterval(this.refreshTimer);
206
+ }
207
+ invalidate() {
208
+ this.tui.requestRender();
209
+ }
210
+ handleInput(data) {
211
+ const stopAllKey = data === 'a' || data === 'A' || data === 'K';
212
+ if (!stopAllKey)
213
+ this.confirmStopAllArmed = false;
214
+ this.actionMessage = undefined;
215
+ if (matchesKey(data, 'escape') ||
216
+ data === 'q' ||
217
+ data === 'Q' ||
218
+ data === 'x' ||
219
+ data === 'X') {
220
+ this.close();
221
+ return;
222
+ }
223
+ if (this.mode === 'detail') {
224
+ this.handleDetailInput(data);
225
+ return;
226
+ }
227
+ this.handleListInput(data);
228
+ }
229
+ render(width) {
230
+ const boxWidth = Math.max(2, Math.min(width, 118));
231
+ return this.mode === 'detail' ? this.renderDetail(boxWidth) : this.renderList(boxWidth);
232
+ }
233
+ close() {
234
+ this.done('closed');
235
+ }
236
+ handleListInput(data) {
237
+ const tasks = this.currentTasks();
238
+ if (tasks.length === 0) {
239
+ if (matchesKey(data, 'return'))
240
+ this.close();
241
+ if (data === 'h' || data === 'H') {
242
+ this.showHistory = !this.showHistory;
243
+ this.tui.requestRender();
244
+ }
245
+ return;
246
+ }
247
+ if (matchesKey(data, 'up')) {
248
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
249
+ this.ensureSelectionVisible();
250
+ this.tui.requestRender();
251
+ return;
252
+ }
253
+ if (matchesKey(data, 'down')) {
254
+ this.selectedIndex = Math.min(tasks.length - 1, this.selectedIndex + 1);
255
+ this.ensureSelectionVisible();
256
+ this.tui.requestRender();
257
+ return;
258
+ }
259
+ if (matchesKey(data, 'pageUp')) {
260
+ this.selectedIndex = Math.max(0, this.selectedIndex - LIST_VISIBLE_ROWS);
261
+ this.ensureSelectionVisible();
262
+ this.tui.requestRender();
263
+ return;
264
+ }
265
+ if (matchesKey(data, 'pageDown')) {
266
+ this.selectedIndex = Math.min(tasks.length - 1, this.selectedIndex + LIST_VISIBLE_ROWS);
267
+ this.ensureSelectionVisible();
268
+ this.tui.requestRender();
269
+ return;
270
+ }
271
+ if (matchesKey(data, 'return') || matchesKey(data, 'right')) {
272
+ const task = tasks[this.selectedIndex];
273
+ if (!task)
274
+ return;
275
+ this.openDetail(task.id);
276
+ return;
277
+ }
278
+ if (data === 'k') {
279
+ const task = tasks[this.selectedIndex];
280
+ if (task)
281
+ void this.stopTaskFromUi(task);
282
+ return;
283
+ }
284
+ if (data === 'K' || data === 'a' || data === 'A') {
285
+ void this.stopAllFromUi();
286
+ return;
287
+ }
288
+ if (data === 'R') {
289
+ const task = tasks[this.selectedIndex];
290
+ if (task)
291
+ void this.rerunTaskFromUi(task);
292
+ return;
293
+ }
294
+ if (data === 'c' || data === 'C') {
295
+ const task = tasks[this.selectedIndex];
296
+ if (task)
297
+ this.showOutputPathFromUi(task);
298
+ return;
299
+ }
300
+ if (data === 'h' || data === 'H') {
301
+ this.showHistory = !this.showHistory;
302
+ this.selectedIndex = 0;
303
+ this.listScroll = 0;
304
+ this.tui.requestRender();
305
+ }
306
+ }
307
+ handleDetailInput(data) {
308
+ const task = this.detailTask();
309
+ if (matchesKey(data, 'left')) {
310
+ this.mode = 'list';
311
+ this.detailTaskId = undefined;
312
+ this.tui.requestRender();
313
+ return;
314
+ }
315
+ if (matchesKey(data, 'up')) {
316
+ this.scrollDetail(-1);
317
+ return;
318
+ }
319
+ if (matchesKey(data, 'down')) {
320
+ this.scrollDetail(1);
321
+ return;
322
+ }
323
+ if (matchesKey(data, 'pageUp')) {
324
+ this.scrollDetail(-DETAIL_VISIBLE_OUTPUT_LINES);
325
+ return;
326
+ }
327
+ if (matchesKey(data, 'pageDown')) {
328
+ this.scrollDetail(DETAIL_VISIBLE_OUTPUT_LINES);
329
+ return;
330
+ }
331
+ if (data === 'r') {
332
+ this.detailFollow = true;
333
+ this.detailScrollTop = 0;
334
+ void this.refreshTail();
335
+ return;
336
+ }
337
+ if (data === 'R' && task) {
338
+ void this.rerunTaskFromUi(task);
339
+ return;
340
+ }
341
+ if (data === 'k' && task) {
342
+ void this.stopTaskFromUi(task);
343
+ return;
344
+ }
345
+ if ((data === 'c' || data === 'C') && task) {
346
+ this.showOutputPathFromUi(task);
347
+ }
348
+ }
349
+ currentTasks() {
350
+ const allTasks = this.options.getTasks();
351
+ let visible = this.showHistory
352
+ ? allTasks
353
+ : allTasks.filter((task) => task.status === 'running');
354
+ if (!this.showHistory &&
355
+ visible.length === 0 &&
356
+ allTasks.some((task) => task.status !== 'running')) {
357
+ this.showHistory = true;
358
+ visible = allTasks;
359
+ }
360
+ return sortTasksForUi(visible);
361
+ }
362
+ detailTask() {
363
+ return this.options.getTasks().find((task) => task.id === this.detailTaskId);
364
+ }
365
+ openDetail(taskId) {
366
+ this.detailTaskId = taskId;
367
+ this.mode = 'detail';
368
+ this.detailLines = [];
369
+ this.detailFollow = true;
370
+ this.detailScrollTop = 0;
371
+ this.tailError = undefined;
372
+ this.options.markSeen(taskId);
373
+ void this.refreshTail();
374
+ this.tui.requestRender();
375
+ }
376
+ ensureSelectionVisible() {
377
+ if (this.selectedIndex < this.listScroll)
378
+ this.listScroll = this.selectedIndex;
379
+ if (this.selectedIndex >= this.listScroll + LIST_VISIBLE_ROWS)
380
+ this.listScroll = this.selectedIndex - LIST_VISIBLE_ROWS + 1;
381
+ }
382
+ async stopTaskFromUi(task) {
383
+ if (task.status !== 'running') {
384
+ this.actionMessage = `${taskDisplayName(task)} is ${task.status}; nothing to stop.`;
385
+ this.tui.requestRender();
386
+ return;
387
+ }
388
+ this.actionMessage = `Stopping ${taskDisplayName(task)}…`;
389
+ this.tui.requestRender();
390
+ try {
391
+ await this.options.stopTask(task);
392
+ this.actionMessage = `Stopped ${taskDisplayName(task)}.`;
393
+ }
394
+ catch (error) {
395
+ this.actionMessage = `Stop failed: ${error instanceof Error ? error.message : String(error)}`;
396
+ }
397
+ this.tui.requestRender();
398
+ }
399
+ async stopAllFromUi() {
400
+ const running = this.options.getTasks().filter((task) => task.status === 'running');
401
+ if (running.length === 0) {
402
+ this.actionMessage = 'No running background tasks to stop.';
403
+ this.tui.requestRender();
404
+ return;
405
+ }
406
+ if (!this.confirmStopAllArmed) {
407
+ this.confirmStopAllArmed = true;
408
+ this.actionMessage = `Press a/K again to stop all ${formatCount(running.length)} running task${pluralSuffix(running.length)}.`;
409
+ this.tui.requestRender();
410
+ return;
411
+ }
412
+ this.confirmStopAllArmed = false;
413
+ this.actionMessage = `Stopping ${formatCount(running.length)} running task${pluralSuffix(running.length)}…`;
414
+ this.tui.requestRender();
415
+ try {
416
+ const result = await this.options.stopAllRunning();
417
+ this.actionMessage =
418
+ result.failures.length > 0
419
+ ? `Stopped ${formatCount(result.stopped)}; ${formatCount(result.failures.length)} failed: ${result.failures.join('; ')}`
420
+ : `Stopped ${formatCount(result.stopped)} running task${pluralSuffix(result.stopped)}.`;
421
+ }
422
+ catch (error) {
423
+ this.actionMessage = `Stop-all failed: ${error instanceof Error ? error.message : String(error)}`;
424
+ }
425
+ this.tui.requestRender();
426
+ }
427
+ async rerunTaskFromUi(task) {
428
+ this.actionMessage = `Rerunning ${taskDisplayName(task)}…`;
429
+ this.tui.requestRender();
430
+ try {
431
+ const rerun = await this.options.rerunTask(task);
432
+ this.showHistory = false;
433
+ const tasks = this.currentTasks();
434
+ const index = tasks.findIndex((candidate) => candidate.id === rerun.id);
435
+ if (index >= 0) {
436
+ this.selectedIndex = index;
437
+ this.ensureSelectionVisible();
438
+ }
439
+ this.actionMessage = `Reran as ${taskDisplayName(rerun)} (${rerun.id}).`;
440
+ }
441
+ catch (error) {
442
+ this.actionMessage = `Rerun failed: ${error instanceof Error ? error.message : String(error)}`;
443
+ }
444
+ this.tui.requestRender();
445
+ }
446
+ showOutputPathFromUi(task) {
447
+ this.options.showOutputPath(task);
448
+ this.actionMessage = `Output path shown for ${taskDisplayName(task)}.`;
449
+ this.tui.requestRender();
450
+ }
451
+ /** Scroll the detail output window. Scrolling up pauses the live tail; reaching the bottom resumes it. */
452
+ scrollDetail(delta) {
453
+ const maxTop = Math.max(0, this.detailLines.length - DETAIL_VISIBLE_OUTPUT_LINES);
454
+ if (maxTop === 0)
455
+ return;
456
+ if (this.detailFollow) {
457
+ this.detailFollow = false;
458
+ this.detailScrollTop = maxTop;
459
+ }
460
+ this.detailScrollTop = Math.min(maxTop, Math.max(0, this.detailScrollTop + delta));
461
+ if (this.detailScrollTop >= maxTop) {
462
+ this.detailFollow = true;
463
+ this.detailScrollTop = 0;
464
+ void this.refreshTail();
465
+ }
466
+ this.tui.requestRender();
467
+ }
468
+ async refreshTail() {
469
+ const task = this.detailTask();
470
+ if (!task)
471
+ return;
472
+ // While the user has scrolled up, the buffer is frozen so their view stays stable.
473
+ if (!this.detailFollow)
474
+ return;
475
+ try {
476
+ if (!existsSync(task.outputAbsPath)) {
477
+ this.detailLines = [];
478
+ this.tailBytesRead = 0;
479
+ this.tailTotalBytes = 0;
480
+ this.tailTruncated = false;
481
+ this.tailError = `Output file not found: ${task.outputPath}`;
482
+ this.tui.requestRender();
483
+ return;
484
+ }
485
+ const read = await boundedRead(task.outputAbsPath, DETAIL_TAIL_BYTES, true);
486
+ this.detailLines = toOutputLines(read.content);
487
+ this.tailBytesRead = read.bytesRead;
488
+ this.tailTotalBytes = read.totalBytes;
489
+ this.tailTruncated = read.truncated;
490
+ this.tailError = undefined;
491
+ }
492
+ catch (error) {
493
+ this.tailError = `Output read failed: ${error instanceof Error ? error.message : String(error)}`;
494
+ }
495
+ this.tui.requestRender();
496
+ }
497
+ activityLabel(task) {
498
+ if (task.status !== 'running')
499
+ return '';
500
+ const now = Date.now();
501
+ const previous = this.lastBytesByTask.get(task.id);
502
+ this.lastBytesByTask.set(task.id, task.bytesWritten);
503
+ if (previous !== undefined && task.bytesWritten > previous) {
504
+ const delta = task.bytesWritten - previous;
505
+ this.recentActivityByTask.set(task.id, { delta, timestamp: now });
506
+ return `+${formatSize(delta)} ↑`;
507
+ }
508
+ const recent = this.recentActivityByTask.get(task.id);
509
+ if (recent && now - recent.timestamp < 3000)
510
+ return `+${formatSize(recent.delta)} ↑`;
511
+ return '';
512
+ }
513
+ frame(title, subtitle, body, footer, width) {
514
+ const inner = Math.max(1, width - 2);
515
+ const top = blueBorder(`╭${'─'.repeat(inner)}╮`);
516
+ const bottom = blueBorder(`╰${'─'.repeat(inner)}╯`);
517
+ const row = (content = '') => `${blueBorder('│')}${padAnsi(truncateToWidth(content, inner), inner)}${blueBorder('│')}`;
518
+ const header = lightBlue(padAnsi(` ${title}`, inner));
519
+ const subtitleLine = subtitle
520
+ ? lightBlue(padAnsi(` ${subtitle}`, inner))
521
+ : lightBlue(' '.repeat(inner));
522
+ const lines = [top, row(header), row(subtitleLine), row()];
523
+ for (const line of body)
524
+ lines.push(row(line));
525
+ lines.push(row());
526
+ lines.push(row(footer));
527
+ lines.push(bottom);
528
+ return lines;
529
+ }
530
+ renderList(width) {
531
+ const tasks = this.currentTasks();
532
+ if (this.selectedIndex >= tasks.length)
533
+ this.selectedIndex = Math.max(0, tasks.length - 1);
534
+ this.ensureSelectionVisible();
535
+ const allTasks = this.options.getTasks();
536
+ const allRunning = allTasks.filter((task) => task.status === 'running').length;
537
+ const historyCount = allTasks.length - allRunning;
538
+ const unseenFailed = allTasks.filter((task) => task.status === 'failed' && !this.options.isSeen(task.id)).length;
539
+ const unseenStopped = allTasks.filter((task) => task.status === 'killed' && !this.options.isSeen(task.id)).length;
540
+ const unseenDone = allTasks.filter((task) => task.status === 'completed' && !this.options.isSeen(task.id)).length;
541
+ const unread = unseenFailed + unseenStopped + unseenDone;
542
+ const subtitleParts = this.showHistory
543
+ ? [`${formatCount(allRunning)} active`, `${formatCount(historyCount)} history`]
544
+ : allRunning > 0
545
+ ? [`${formatCount(allRunning)} active shell${pluralSuffix(allRunning)}`]
546
+ : ['No active shells'];
547
+ if (unseenFailed)
548
+ subtitleParts.push(`${formatCount(unseenFailed)} failed`);
549
+ if (unseenStopped)
550
+ subtitleParts.push(`${formatCount(unseenStopped)} stopped`);
551
+ if (unseenDone)
552
+ subtitleParts.push(`${formatCount(unseenDone)} done`);
553
+ if (unread)
554
+ subtitleParts.push(`${formatCount(unread)} unread`);
555
+ const subtitle = subtitleParts.join(' · ');
556
+ const body = [];
557
+ if (tasks.length === 0) {
558
+ const message = allTasks.length === 0
559
+ ? ' No background tasks in this session.'
560
+ : this.showHistory
561
+ ? ' No background tasks in this view.'
562
+ : ' No running background tasks. Press h to show recent history.';
563
+ body.push(this.theme.fg('dim', message));
564
+ }
565
+ else {
566
+ const maxNameWidth = Math.max(12, width - 78);
567
+ const visible = tasks.slice(this.listScroll, this.listScroll + LIST_VISIBLE_ROWS);
568
+ for (let i = 0; i < visible.length; i++) {
569
+ const task = visible[i];
570
+ if (!task)
571
+ continue;
572
+ const index = this.listScroll + i;
573
+ const selected = index === this.selectedIndex;
574
+ const pointer = selected ? '›' : ' ';
575
+ const unseen = task.status !== 'running' && !this.options.isSeen(task.id);
576
+ const unreadMark = unseen ? this.theme.fg('warning', '●') : ' ';
577
+ const rawName = truncateChars(taskDisplayName(task), maxNameWidth);
578
+ const name = task.status === 'failed'
579
+ ? this.theme.fg('error', rawName)
580
+ : task.status === 'killed'
581
+ ? this.theme.fg('warning', rawName)
582
+ : task.status === 'completed'
583
+ ? this.theme.fg('success', rawName)
584
+ : this.theme.fg('text', rawName);
585
+ const status = statusColor(this.theme, task.status, statusLabel(task.status));
586
+ const runtime = taskAge(task);
587
+ const size = formatSize(task.bytesWritten);
588
+ const context = formatContextUsage(task);
589
+ const contextText = ` ${contextColor(this.theme, task, `ctx ${context}`)}`;
590
+ const model = formatModel(task);
591
+ const modelText = model ? ` ${this.theme.fg('dim', model)}` : '';
592
+ const tokenUsage = formatTokenUsage(task);
593
+ const tokenText = tokenUsage ? ` ${this.theme.fg('dim', tokenUsage)}` : '';
594
+ const toolUsage = formatToolUsage(task);
595
+ const toolText = toolUsage ? ` ${this.theme.fg('dim', toolUsage)}` : '';
596
+ const activity = this.activityLabel(task);
597
+ const activityText = activity ? ` ${this.theme.fg('warning', activity)}` : '';
598
+ const exit = task.status !== 'running' ? this.theme.fg('dim', formatExitCodeText(task.exitCode)) : '';
599
+ let row = ` ${pointer} ${unreadMark} ${name} ${this.theme.fg('dim', task.id)} ${this.theme.fg('dim', '·')} ${status}${exit} ${this.theme.fg('dim', `${runtime} ${size}`)}${contextText}${modelText}${tokenText}${toolText}${activityText}`;
600
+ if (selected)
601
+ row = lightBlue(padAnsi(truncateToWidth(row, width - 4), width - 4));
602
+ body.push(row);
603
+ }
604
+ if (tasks.length > LIST_VISIBLE_ROWS) {
605
+ const firstVisible = formatCount(this.listScroll + 1);
606
+ const lastVisible = formatCount(Math.min(tasks.length, this.listScroll + LIST_VISIBLE_ROWS));
607
+ const totalVisible = formatCount(tasks.length);
608
+ body.push(this.theme.fg('dim', ` Showing ${firstVisible}-${lastVisible} of ${totalVisible}`));
609
+ }
610
+ }
611
+ if (this.actionMessage)
612
+ body.push(this.theme.fg('warning', ` ${this.actionMessage}`));
613
+ return this.frame('bg tasks focused', subtitle, body, ` ${this.theme.fg('dim', `↑/↓ select · Enter logs · k stop · a stop all · h ${this.showHistory ? 'hide' : 'show'} history · R rerun · c path · x close`)}`, width);
614
+ }
615
+ renderDetail(width) {
616
+ const task = this.detailTask();
617
+ if (!task) {
618
+ this.mode = 'list';
619
+ return this.renderList(width);
620
+ }
621
+ const name = taskDisplayName(task);
622
+ const status = statusColor(this.theme, task.status);
623
+ const exit = formatExitCodeText(task.exitCode);
624
+ const body = [
625
+ ` ${this.theme.fg('toolTitle', 'Name:')} ${this.theme.fg('accent', name)}`,
626
+ ` ${this.theme.fg('toolTitle', 'ID:')} ${this.theme.fg('accent', task.id)}`,
627
+ ` ${this.theme.fg('toolTitle', 'Status:')} ${status}${this.theme.fg('dim', exit)}`,
628
+ ` ${this.theme.fg('toolTitle', 'Runtime:')} ${taskAge(task)}${this.theme.fg('dim', formatPidText(task.pid))}`,
629
+ ` ${this.theme.fg('toolTitle', 'Started:')} ${formatTime(task.startTime)}${task.endTime ? this.theme.fg('dim', ` · ended ${formatTime(task.endTime)}`) : ''}`,
630
+ ` ${this.theme.fg('toolTitle', 'Output:')} ${this.theme.fg('accent', task.outputPath)}`,
631
+ ];
632
+ if (task.description && compactWhitespace(task.description) !== compactWhitespace(name)) {
633
+ body.push(` ${this.theme.fg('toolTitle', 'Description:')} ${truncateToWidth(task.description, width - 16)}`);
634
+ }
635
+ const modelDetail = formatModelDetail(task);
636
+ body.push(` ${this.theme.fg('toolTitle', 'Model:')} ${task.model ? this.theme.fg('accent', modelDetail) : this.theme.fg('dim', modelDetail)}`);
637
+ const context = formatContextDetail(task);
638
+ body.push(` ${this.theme.fg('toolTitle', 'Context:')} ${contextColor(this.theme, task, context)}`);
639
+ body.push(` ${this.theme.fg('toolTitle', 'Tokens:')} ${this.theme.fg('dim', formatTokenDetail(task))}`);
640
+ body.push(` ${this.theme.fg('toolTitle', 'Tools:')} ${this.theme.fg('dim', formatToolDetail(task))}`);
641
+ body.push(` ${this.theme.fg('toolTitle', 'Command:')} ${truncateToWidth(task.command, width - 13)}`);
642
+ if (task.error)
643
+ body.push(` ${this.theme.fg('error', `Error: ${task.error}`)}`);
644
+ body.push('', ` ${this.theme.fg('toolTitle', 'Output tail:')}`);
645
+ body.push(...this.renderOutputBox(width - 4));
646
+ if (this.actionMessage)
647
+ body.push(this.theme.fg('warning', ` ${this.actionMessage}`));
648
+ const subtitle = `${task.id} · ${task.status === 'running' ? 'live tail refreshes every second' : 'final output'}`;
649
+ const footer = ` ${this.theme.fg('dim', '↑/↓ scroll · ← list · r refresh · k stop · R rerun · c path · x close')}`;
650
+ return this.frame(`bg: ${truncateChars(name, 64)}`, subtitle, body, footer, width);
651
+ }
652
+ renderOutputBox(width) {
653
+ const inner = Math.max(1, width - 2);
654
+ const top = ` ${blueBorder(`╭${'─'.repeat(inner)}╮`)}`;
655
+ const bottom = ` ${blueBorder(`╰${'─'.repeat(inner)}╯`)}`;
656
+ const row = (content = '') => ` ${blueBorder('│')}${padAnsi(truncateToWidth(content, inner), inner)}${blueBorder('│')}`;
657
+ const lines = [top];
658
+ if (this.tailError) {
659
+ lines.push(row(this.theme.fg('error', this.tailError)));
660
+ }
661
+ else if (this.detailLines.length === 0) {
662
+ lines.push(row(this.theme.fg('dim', 'No output yet')));
663
+ }
664
+ else {
665
+ const maxTop = Math.max(0, this.detailLines.length - DETAIL_VISIBLE_OUTPUT_LINES);
666
+ const start = this.detailFollow ? maxTop : Math.min(this.detailScrollTop, maxTop);
667
+ const windowLines = this.detailLines.slice(start, start + DETAIL_VISIBLE_OUTPUT_LINES);
668
+ for (const line of windowLines)
669
+ lines.push(row(this.theme.fg('toolOutput', line)));
670
+ }
671
+ while (lines.length < DETAIL_VISIBLE_OUTPUT_LINES + 1)
672
+ lines.push(row());
673
+ lines.push(bottom);
674
+ lines.push(` ${this.theme.fg('dim', this.outputStatusLine())}`);
675
+ return lines;
676
+ }
677
+ outputStatusLine() {
678
+ const total = this.detailLines.length;
679
+ if (!this.detailFollow && total > 0) {
680
+ const maxTop = Math.max(0, total - DETAIL_VISIBLE_OUTPUT_LINES);
681
+ const start = Math.min(this.detailScrollTop, maxTop);
682
+ const end = Math.min(total, start + DETAIL_VISIBLE_OUTPUT_LINES);
683
+ return `lines ${formatCount(start + 1)}\u2013${formatCount(end)} of ${formatCount(total)} · ↑/↓ PgUp/PgDn scroll · ↓ at bottom follows`;
684
+ }
685
+ const suffix = this.tailTruncated ? ` of ${formatSize(this.tailTotalBytes)}` : '';
686
+ return `following tail ${formatSize(this.tailBytesRead)}${suffix} · ↑ scroll`;
687
+ }
688
+ }
689
+ //# sourceMappingURL=background-tasks-manager.js.map