pi-crew 0.2.3 → 0.2.5

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 (348) hide show
  1. package/AGENTS.md +57 -32
  2. package/CHANGELOG.md +466 -448
  3. package/LICENSE +21 -21
  4. package/NOTICE.md +16 -16
  5. package/README.md +323 -323
  6. package/docs/FEATURE_INTAKE.md +126 -0
  7. package/docs/HARNESS.md +86 -0
  8. package/docs/HARNESS_BACKLOG.md +41 -0
  9. package/docs/TEST_MATRIX.md +49 -0
  10. package/docs/actions-reference.md +595 -595
  11. package/docs/architecture.md +180 -180
  12. package/docs/code-review-2026-05-11.md +592 -592
  13. package/docs/commands-reference.md +347 -347
  14. package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
  15. package/docs/decisions/0001-durable-state.md +41 -0
  16. package/docs/decisions/0002-child-process-for-async.md +42 -0
  17. package/docs/decisions/0003-depth-guard.md +36 -0
  18. package/docs/decisions/0004-execfile-over-exec.md +34 -0
  19. package/docs/decisions/0005-no-parameter-properties.md +49 -0
  20. package/docs/decisions/0006-publish-bundled-esm.md +63 -0
  21. package/docs/decisions/0007-active-run-binary-index.md +54 -0
  22. package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
  23. package/docs/decisions/README.md +23 -0
  24. package/docs/followup-review-round4-2026-05-13.md +107 -0
  25. package/docs/implementation-plan-top3.md +333 -0
  26. package/docs/live-mailbox-runtime.md +36 -36
  27. package/docs/next-upgrade-roadmap.md +808 -808
  28. package/docs/oh-my-pi-research.md +509 -0
  29. package/docs/perf/baseline-2026-05.md +113 -0
  30. package/docs/perf/final-report-2026-05.md +206 -0
  31. package/docs/perf/sprint-1-report.md +71 -0
  32. package/docs/perf/sprint-2-report.md +81 -0
  33. package/docs/perf/sprint-2.5-report.md +53 -0
  34. package/docs/perf/sprint-3-report.md +36 -0
  35. package/docs/perf/sprint-4-report.md +47 -0
  36. package/docs/perf/sprint-5-report.md +51 -0
  37. package/docs/perf/sprint-6-report.md +94 -0
  38. package/docs/perf/sprint-7-report.md +74 -0
  39. package/docs/perf/upgrade-plan-2026-05.md +147 -0
  40. package/docs/pi-subagents3-deep-analysis.md +508 -0
  41. package/docs/product/README.md +31 -0
  42. package/docs/product/platform.md +27 -0
  43. package/docs/product/runtime-safety.md +37 -0
  44. package/docs/product/team-run.md +39 -0
  45. package/docs/product/team-tool.md +37 -0
  46. package/docs/publishing.md +65 -65
  47. package/docs/resource-formats.md +134 -134
  48. package/docs/runtime-analysis-child-vs-live.md +171 -0
  49. package/docs/runtime-flow.md +148 -148
  50. package/docs/runtime-migration-in-process-analysis.md +250 -0
  51. package/docs/stories/README.md +30 -0
  52. package/docs/stories/backlog.md +36 -0
  53. package/docs/templates/decision.md +27 -0
  54. package/docs/templates/story.md +44 -0
  55. package/docs/templates/validation-report.md +32 -0
  56. package/docs/usage.md +238 -238
  57. package/index.ts +7 -6
  58. package/install.mjs +65 -65
  59. package/package.json +107 -100
  60. package/schema.json +222 -222
  61. package/skills/child-pi-spawning/SKILL.md +213 -0
  62. package/skills/context-artifact-hygiene/SKILL.md +32 -0
  63. package/skills/event-log-tracing/SKILL.md +299 -0
  64. package/skills/git-master/SKILL.md +225 -24
  65. package/skills/live-agent-lifecycle/SKILL.md +192 -0
  66. package/skills/mailbox-interactive/SKILL.md +300 -19
  67. package/skills/model-routing-context/SKILL.md +94 -0
  68. package/skills/multi-perspective-review/SKILL.md +88 -0
  69. package/skills/read-only-explorer/SKILL.md +250 -26
  70. package/skills/safe-bash/SKILL.md +307 -21
  71. package/skills/verification-before-done/SKILL.md +11 -2
  72. package/skills/widget-rendering/SKILL.md +258 -0
  73. package/skills/workspace-isolation/SKILL.md +202 -0
  74. package/skills/worktree-isolation/SKILL.md +202 -18
  75. package/src/adapters/claude-adapter.ts +25 -25
  76. package/src/adapters/codex-adapter.ts +21 -21
  77. package/src/adapters/cursor-adapter.ts +17 -17
  78. package/src/adapters/export-util.ts +137 -137
  79. package/src/adapters/index.ts +15 -15
  80. package/src/adapters/registry.ts +18 -18
  81. package/src/adapters/types.ts +23 -23
  82. package/src/agents/agent-config.ts +38 -38
  83. package/src/agents/agent-serializer.ts +38 -38
  84. package/src/agents/discover-agents.ts +121 -118
  85. package/src/config/config.ts +740 -858
  86. package/src/config/defaults.ts +96 -96
  87. package/src/config/drift-detector.ts +211 -211
  88. package/src/config/markers.ts +327 -327
  89. package/src/config/resilient-parser.ts +109 -108
  90. package/src/config/suggestions.ts +74 -74
  91. package/src/config/types.ts +199 -0
  92. package/src/extension/async-notifier.ts +123 -89
  93. package/src/extension/autonomous-policy.ts +169 -169
  94. package/src/extension/cross-extension-rpc.ts +104 -104
  95. package/src/extension/help.ts +47 -47
  96. package/src/extension/import-index.ts +69 -69
  97. package/src/extension/management.ts +395 -382
  98. package/src/extension/notification-router.ts +116 -116
  99. package/src/extension/notification-sink.ts +51 -51
  100. package/src/extension/project-init.ts +168 -168
  101. package/src/extension/register.ts +859 -668
  102. package/src/extension/registration/artifact-cleanup.ts +15 -15
  103. package/src/extension/registration/command-utils.ts +54 -54
  104. package/src/extension/registration/commands.ts +559 -452
  105. package/src/extension/registration/compaction-guard.ts +125 -125
  106. package/src/extension/registration/subagent-helpers.ts +102 -102
  107. package/src/extension/registration/subagent-tools.ts +220 -159
  108. package/src/extension/registration/team-tool.ts +159 -99
  109. package/src/extension/registration/viewers.ts +29 -0
  110. package/src/extension/result-watcher.ts +128 -128
  111. package/src/extension/run-bundle-schema.ts +89 -89
  112. package/src/extension/run-export.ts +73 -73
  113. package/src/extension/run-import.ts +84 -84
  114. package/src/extension/run-index.ts +94 -94
  115. package/src/extension/run-maintenance.ts +142 -142
  116. package/src/extension/session-summary.ts +8 -8
  117. package/src/extension/team-manager-command.ts +96 -96
  118. package/src/extension/team-recommendation.ts +188 -188
  119. package/src/extension/team-tool/api.ts +5 -2
  120. package/src/extension/team-tool/cancel.ts +224 -209
  121. package/src/extension/team-tool/config-patch.ts +36 -36
  122. package/src/extension/team-tool/context.ts +60 -60
  123. package/src/extension/team-tool/doctor.ts +242 -242
  124. package/src/extension/team-tool/handle-settings.ts +421 -195
  125. package/src/extension/team-tool/inspect.ts +41 -41
  126. package/src/extension/team-tool/lifecycle-actions.ts +139 -139
  127. package/src/extension/team-tool/parallel-dispatch.ts +156 -156
  128. package/src/extension/team-tool/plan.ts +19 -19
  129. package/src/extension/team-tool/respond.ts +112 -111
  130. package/src/extension/team-tool/run.ts +246 -229
  131. package/src/extension/team-tool/status.ts +110 -110
  132. package/src/extension/team-tool-types.ts +13 -13
  133. package/src/extension/team-tool.ts +344 -344
  134. package/src/extension/tool-result.ts +16 -16
  135. package/src/extension/validate-resources.ts +77 -77
  136. package/src/hooks/registry.ts +61 -61
  137. package/src/hooks/types.ts +40 -40
  138. package/src/i18n.ts +184 -184
  139. package/src/observability/correlation.ts +35 -35
  140. package/src/observability/event-to-metric.ts +68 -68
  141. package/src/observability/exporters/adapter.ts +30 -30
  142. package/src/observability/exporters/otlp-exporter.ts +106 -92
  143. package/src/observability/exporters/prometheus-exporter.ts +54 -54
  144. package/src/observability/metric-registry.ts +87 -87
  145. package/src/observability/metric-retention.ts +54 -54
  146. package/src/observability/metric-sink.ts +81 -56
  147. package/src/observability/metrics-primitives.ts +167 -167
  148. package/src/prompt/prompt-runtime.ts +72 -72
  149. package/src/runtime/adaptive-plan.ts +338 -0
  150. package/src/runtime/agent-control.ts +169 -169
  151. package/src/runtime/agent-memory.ts +72 -72
  152. package/src/runtime/agent-observability.ts +114 -114
  153. package/src/runtime/async-marker.ts +26 -26
  154. package/src/runtime/async-runner.ts +153 -153
  155. package/src/runtime/attention-events.ts +28 -28
  156. package/src/runtime/auto-resume.ts +100 -100
  157. package/src/runtime/background-runner.ts +122 -89
  158. package/src/runtime/cancellation.ts +61 -61
  159. package/src/runtime/capability-inventory.ts +116 -116
  160. package/src/runtime/child-pi-pool.ts +68 -0
  161. package/src/runtime/child-pi.ts +541 -461
  162. package/src/runtime/code-summary.ts +247 -247
  163. package/src/runtime/compaction-summary.ts +271 -271
  164. package/src/runtime/concurrency.ts +58 -58
  165. package/src/runtime/crash-recovery.ts +317 -301
  166. package/src/runtime/crew-agent-records.ts +379 -281
  167. package/src/runtime/crew-agent-runtime.ts +60 -60
  168. package/src/runtime/cross-extension-rpc.ts +72 -0
  169. package/src/runtime/custom-tools/irc-tool.ts +201 -201
  170. package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
  171. package/src/runtime/deadletter.ts +47 -47
  172. package/src/runtime/delivery-coordinator.ts +176 -176
  173. package/src/runtime/delta-conflict.ts +360 -360
  174. package/src/runtime/diagnostic-export.ts +102 -102
  175. package/src/runtime/direct-run.ts +35 -35
  176. package/src/runtime/effectiveness.ts +82 -81
  177. package/src/runtime/errors/crew-errors.ts +166 -0
  178. package/src/runtime/event-stream-bridge.ts +92 -92
  179. package/src/runtime/foreground-control.ts +82 -82
  180. package/src/runtime/green-contract.ts +46 -46
  181. package/src/runtime/group-join.ts +234 -106
  182. package/src/runtime/heartbeat-watcher.ts +145 -124
  183. package/src/runtime/iteration-hooks.ts +267 -267
  184. package/src/runtime/live-agent-control.ts +88 -88
  185. package/src/runtime/live-agent-manager.ts +377 -179
  186. package/src/runtime/live-control-realtime.ts +36 -36
  187. package/src/runtime/live-session-runtime.ts +676 -600
  188. package/src/runtime/loop-gates.ts +129 -129
  189. package/src/runtime/manifest-cache.ts +263 -263
  190. package/src/runtime/mcp-proxy.ts +113 -113
  191. package/src/runtime/metric-parser.ts +40 -40
  192. package/src/runtime/model-fallback.ts +282 -274
  193. package/src/runtime/model-resolver.ts +118 -0
  194. package/src/runtime/output-validator.ts +187 -187
  195. package/src/runtime/overflow-recovery.ts +175 -175
  196. package/src/runtime/parallel-research.ts +44 -44
  197. package/src/runtime/parallel-utils.ts +156 -156
  198. package/src/runtime/parent-guard.ts +80 -80
  199. package/src/runtime/phase-progress.ts +217 -217
  200. package/src/runtime/pi-args.ts +165 -165
  201. package/src/runtime/pi-json-output.ts +111 -111
  202. package/src/runtime/pi-spawn.ts +167 -167
  203. package/src/runtime/policy-engine.ts +79 -79
  204. package/src/runtime/post-checks.ts +125 -125
  205. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  206. package/src/runtime/process-status.ts +97 -73
  207. package/src/runtime/progress-event-coalescer.ts +43 -43
  208. package/src/runtime/recovery-recipes.ts +74 -74
  209. package/src/runtime/retry-executor.ts +81 -81
  210. package/src/runtime/role-permission.ts +39 -39
  211. package/src/runtime/run-tracker.ts +99 -0
  212. package/src/runtime/runtime-policy.ts +21 -0
  213. package/src/runtime/runtime-resolver.ts +94 -91
  214. package/src/runtime/scheduler.ts +294 -0
  215. package/src/runtime/semaphore.ts +131 -131
  216. package/src/runtime/sensitive-paths.ts +92 -92
  217. package/src/runtime/session-usage.ts +79 -79
  218. package/src/runtime/settings-store.ts +103 -0
  219. package/src/runtime/sidechain-output.ts +29 -29
  220. package/src/runtime/skill-instructions.ts +222 -222
  221. package/src/runtime/stale-reconciler.ts +198 -189
  222. package/src/runtime/streaming-output.ts +47 -0
  223. package/src/runtime/subagent-manager.ts +404 -400
  224. package/src/runtime/subprocess-tool-registry.ts +67 -67
  225. package/src/runtime/task-display.ts +38 -38
  226. package/src/runtime/task-graph-scheduler.ts +122 -122
  227. package/src/runtime/task-graph.ts +207 -207
  228. package/src/runtime/task-output-context.ts +177 -177
  229. package/src/runtime/task-packet.ts +93 -93
  230. package/src/runtime/task-quality.ts +207 -207
  231. package/src/runtime/task-runner/capabilities.ts +78 -78
  232. package/src/runtime/task-runner/live-executor.ts +131 -113
  233. package/src/runtime/task-runner/progress.ts +119 -119
  234. package/src/runtime/task-runner/prompt-builder.ts +139 -139
  235. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  236. package/src/runtime/task-runner/result-utils.ts +14 -14
  237. package/src/runtime/task-runner/run-projection.ts +103 -103
  238. package/src/runtime/task-runner/state-helpers.ts +22 -22
  239. package/src/runtime/task-runner.ts +469 -459
  240. package/src/runtime/team-runner.ts +693 -945
  241. package/src/runtime/usage-tracker.ts +71 -0
  242. package/src/runtime/worker-heartbeat.ts +21 -21
  243. package/src/runtime/worker-startup.ts +57 -57
  244. package/src/runtime/workflow-state.ts +187 -187
  245. package/src/runtime/yield-handler.ts +190 -190
  246. package/src/schema/config-schema.ts +172 -168
  247. package/src/schema/team-tool-schema.ts +126 -126
  248. package/src/schema/validation-types.ts +151 -148
  249. package/src/skills/discover-skills.ts +67 -67
  250. package/src/skills/skill-templates.ts +374 -374
  251. package/src/state/active-run-registry.ts +227 -191
  252. package/src/state/artifact-store.ts +130 -129
  253. package/src/state/atomic-write.ts +262 -195
  254. package/src/state/blob-store.ts +116 -116
  255. package/src/state/contracts.ts +111 -111
  256. package/src/state/event-log-rotation.ts +161 -158
  257. package/src/state/event-log.ts +383 -303
  258. package/src/state/event-reconstructor.ts +217 -217
  259. package/src/state/jsonl-writer.ts +82 -82
  260. package/src/state/locks.ts +146 -146
  261. package/src/state/mailbox.ts +446 -405
  262. package/src/state/state-store.ts +364 -351
  263. package/src/state/task-claims.ts +44 -44
  264. package/src/state/types.ts +285 -285
  265. package/src/state/usage.ts +29 -29
  266. package/src/subagents/async-entry.ts +1 -1
  267. package/src/subagents/index.ts +3 -3
  268. package/src/subagents/live/control.ts +1 -1
  269. package/src/subagents/live/manager.ts +1 -1
  270. package/src/subagents/live/realtime.ts +1 -1
  271. package/src/subagents/live/session-runtime.ts +1 -1
  272. package/src/subagents/manager.ts +1 -1
  273. package/src/subagents/spawn.ts +1 -1
  274. package/src/teams/discover-teams.ts +116 -116
  275. package/src/teams/team-config.ts +27 -27
  276. package/src/teams/team-serializer.ts +38 -38
  277. package/src/types/diff.d.ts +18 -18
  278. package/src/ui/agent-management-overlay.ts +144 -144
  279. package/src/ui/crew-widget.ts +487 -370
  280. package/src/ui/dashboard-panes/agents-pane.ts +109 -28
  281. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  282. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  283. package/src/ui/dashboard-panes/health-pane.ts +30 -30
  284. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  285. package/src/ui/dashboard-panes/progress-pane.ts +30 -30
  286. package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
  287. package/src/ui/heartbeat-aggregator.ts +63 -63
  288. package/src/ui/keybinding-map.ts +97 -94
  289. package/src/ui/live-conversation-overlay.ts +152 -0
  290. package/src/ui/live-run-sidebar.ts +180 -180
  291. package/src/ui/mascot.ts +442 -442
  292. package/src/ui/overlays/agent-picker-overlay.ts +57 -57
  293. package/src/ui/overlays/confirm-overlay.ts +58 -58
  294. package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
  295. package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
  296. package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
  297. package/src/ui/pi-ui-compat.ts +57 -57
  298. package/src/ui/powerbar-publisher.ts +221 -197
  299. package/src/ui/render-scheduler.ts +216 -143
  300. package/src/ui/run-action-dispatcher.ts +118 -118
  301. package/src/ui/run-dashboard.ts +526 -464
  302. package/src/ui/run-event-bus.ts +208 -208
  303. package/src/ui/run-snapshot-cache.ts +826 -777
  304. package/src/ui/settings-overlay.ts +721 -0
  305. package/src/ui/snapshot-types.ts +86 -70
  306. package/src/ui/theme-adapter.ts +190 -190
  307. package/src/ui/tool-progress-formatter.ts +89 -0
  308. package/src/ui/transcript-cache.ts +94 -94
  309. package/src/ui/transcript-viewer.ts +335 -335
  310. package/src/utils/conflict-detect.ts +662 -0
  311. package/src/utils/file-coalescer.ts +86 -86
  312. package/src/utils/frontmatter.ts +68 -68
  313. package/src/utils/fs-watch.ts +88 -31
  314. package/src/utils/gh-protocol.ts +479 -0
  315. package/src/utils/ids.ts +17 -17
  316. package/src/utils/incremental-reader.ts +104 -104
  317. package/src/utils/internal-error.ts +6 -6
  318. package/src/utils/names.ts +27 -27
  319. package/src/utils/paths.ts +102 -63
  320. package/src/utils/redaction.ts +44 -44
  321. package/src/utils/safe-paths.ts +47 -47
  322. package/src/utils/scan-cache.ts +136 -136
  323. package/src/utils/sse-parser.ts +134 -134
  324. package/src/utils/task-name-generator.ts +337 -337
  325. package/src/utils/timings.ts +33 -33
  326. package/src/utils/visual.ts +243 -198
  327. package/src/workflows/discover-workflows.ts +139 -139
  328. package/src/workflows/validate-workflow.ts +40 -40
  329. package/src/workflows/workflow-config.ts +26 -26
  330. package/src/workflows/workflow-serializer.ts +32 -32
  331. package/src/worktree/branch-freshness.ts +45 -45
  332. package/src/worktree/cleanup.ts +75 -75
  333. package/src/worktree/worktree-manager.ts +188 -188
  334. package/teams/default.team.md +12 -12
  335. package/teams/fast-fix.team.md +11 -11
  336. package/teams/implementation.team.md +18 -18
  337. package/teams/parallel-research.team.md +14 -14
  338. package/teams/research.team.md +11 -11
  339. package/teams/review.team.md +12 -12
  340. package/tsconfig.json +19 -19
  341. package/workflows/default.workflow.md +30 -30
  342. package/workflows/fast-fix.workflow.md +23 -23
  343. package/workflows/implementation.workflow.md +43 -43
  344. package/workflows/parallel-research.workflow.md +46 -46
  345. package/workflows/research.workflow.md +22 -22
  346. package/workflows/review.workflow.md +30 -30
  347. package/skills/task-packet/SKILL.md +0 -28
  348. package/skills/verify-evidence/SKILL.md +0 -27
@@ -1,370 +1,487 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import type { CrewUiConfig } from "../config/config.ts";
3
- import { listRecentRuns } from "../extension/run-index.ts";
4
- import { readCrewAgents } from "../runtime/crew-agent-records.ts";
5
- import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
6
- import { isDisplayActiveRun } from "../runtime/process-status.ts";
7
- import type { TeamRunManifest } from "../state/types.ts";
8
- import type { ManifestCache } from "../runtime/manifest-cache.ts";
9
- import { colorForStatus, iconForStatus, type RunStatus } from "./status-colors.ts";
10
- import { pad, truncate } from "../utils/visual.ts";
11
- import type { CrewTheme } from "./theme-adapter.ts";
12
- import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
13
- import { Box, Text } from "./layout-primitives.ts";
14
- import { requestRender, setExtensionWidget } from "./pi-ui-compat.ts";
15
- import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
16
- import { runEventBus } from "./run-event-bus.ts";
17
- import { DEFAULT_UI } from "../config/defaults.ts";
18
- import { computePhaseProgress, formatPhaseProgressLine } from "../runtime/phase-progress.ts";
19
-
20
- const SPINNER = ["⠋", "⠙", "", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
21
- const TOOL_LABELS: Record<string, string> = {
22
- read: "reading",
23
- bash: "running command",
24
- edit: "editing",
25
- write: "writing",
26
- grep: "searching",
27
- find: "finding files",
28
- ls: "listing",
29
- };
30
- const LEGACY_WIDGET_KEY = "pi-crew";
31
- const WIDGET_KEY = "pi-crew-active";
32
- const STATUS_KEY = "pi-crew";
33
-
34
- const MAX_LINES_DEFAULT = DEFAULT_UI.widgetMaxLines;
35
- const MAX_AGENTS_DISPLAY = 3;
36
-
37
- type WidgetComponent = { render(width: number): string[]; invalidate(): void };
38
-
39
- interface CrewWidgetModel {
40
- cwd: string;
41
- frame: number;
42
- maxLines: number;
43
- notificationCount?: number;
44
- manifestCache?: ManifestCache;
45
- snapshotCache?: RunSnapshotCache;
46
- preloadManifests?: TeamRunManifest[];
47
- }
48
-
49
- export interface CrewWidgetState {
50
- frame: number;
51
- interval?: ReturnType<typeof setInterval>;
52
- lastPlacement?: string;
53
- lastVisibility?: "hidden" | "visible";
54
- lastKey?: string;
55
- lastMaxLines?: number;
56
- lastCwd?: string;
57
- legacyCleared?: boolean;
58
- model?: CrewWidgetModel;
59
- notificationCount?: number;
60
- }
61
-
62
- interface WidgetRun {
63
- run: TeamRunManifest;
64
- agents: CrewAgentRecord[];
65
- snapshot?: RunUiSnapshot;
66
- }
67
-
68
- function elapsed(iso: string | undefined, now = Date.now()): string | undefined {
69
- if (!iso) return undefined;
70
- const ms = Math.max(0, now - new Date(iso).getTime());
71
- if (!Number.isFinite(ms)) return undefined;
72
- if (ms < 1000) return "now";
73
- if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
74
- if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
75
- return `${Math.floor(ms / 3_600_000)}h`;
76
- }
77
-
78
- function agentActivity(agent: CrewAgentRecord): string {
79
- if (agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
80
- const recent = agent.progress?.recentOutput?.at(-1);
81
- if (recent) return recent.replace(/\s+/g, " ").trim();
82
- if (agent.progress?.activityState === "needs_attention") return "needs attention";
83
- if (agent.status === "queued") return "queued";
84
- if (agent.status === "running") {
85
- const age = agent.startedAt ? Date.now() - new Date(agent.startedAt).getTime() : Infinity;
86
- if (age < 5000 && !agent.progress?.currentTool) return "spawning…";
87
- return "thinking…";
88
- }
89
- if (agent.status === "failed") return agent.error ?? "failed";
90
- return "done";
91
- }
92
-
93
- function agentStats(agent: CrewAgentRecord): string {
94
- const parts: string[] = [];
95
- if (agent.toolUses) parts.push(`${agent.toolUses} tools`);
96
- if (agent.progress?.tokens) parts.push(`${agent.progress.tokens} tok`);
97
- if (agent.progress?.turns) parts.push(`⟳${agent.progress.turns}`);
98
- const age = elapsed(agent.completedAt ?? agent.startedAt);
99
- if (age) parts.push(agent.completedAt ? age : `${age} ago`);
100
- return parts.join(" · ");
101
- }
102
-
103
- function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
104
- try {
105
- return readCrewAgents(run);
106
- } catch {
107
- return [];
108
- }
109
- }
110
-
111
- export function activeWidgetRuns(cwd: string, manifestCache?: ManifestCache, snapshotCache?: RunSnapshotCache, preloadedManifests?: TeamRunManifest[]): WidgetRun[] {
112
- const runs = preloadedManifests ?? (manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20));
113
- return runs
114
- .map((run) => {
115
- try {
116
- const snapshot = snapshotCache?.get(run.runId) ?? snapshotCache?.refreshIfStale(run.runId);
117
- return snapshot ? { run: snapshot.manifest, agents: snapshot.agents, snapshot } : { run, agents: agentsFor(run) };
118
- } catch {
119
- return { run, agents: agentsFor(run) };
120
- }
121
- })
122
- .filter((item) => isDisplayActiveRun(item.run, item.agents));
123
- }
124
-
125
- function statusSummary(runs: WidgetRun[]): string {
126
- const agents = runs.flatMap((item) => item.agents);
127
- const runningAgents = agents.filter((agent) => agent.status === "running").length;
128
- const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
129
- const waitingAgents = agents.filter((agent) => agent.status === "waiting").length;
130
- const completedAgents = agents.filter((agent) => agent.status === "completed").length;
131
- const parts = [`${runningAgents} running`];
132
- if (queuedAgents) parts.push(`${queuedAgents} queued`);
133
- if (waitingAgents) parts.push(`${waitingAgents} waiting`);
134
- if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
135
- return `Crew: ${parts.join(", ")}`;
136
- }
137
-
138
- export function notificationBadge(count: number | undefined, env: NodeJS.ProcessEnv = process.env): string {
139
- if (!count || count <= 0) return "";
140
- const term = `${env.TERM ?? ""} ${env.WT_SESSION ?? ""} ${env.TERM_PROGRAM ?? ""}`.toLowerCase();
141
- const supportsEmoji = !term.includes("dumb") && env.NO_COLOR !== "1";
142
- return supportsEmoji ? ` 🔔${count}` : ` [!${count}]`;
143
- }
144
-
145
- export function widgetHeader(runs: WidgetRun[], runningGlyph: string, maxLines = 20, notificationCount = 0): string {
146
- const agents = runs.flatMap((item) => item.agents);
147
- const runningAgents = agents.filter((agent) => agent.status === "running").length;
148
- const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
149
- const waitingAgents = agents.filter((agent) => agent.status === "waiting").length;
150
- const completedAgents = agents.filter((agent) => agent.status === "completed").length;
151
- const parts = [`${runningAgents} running`];
152
- if (queuedAgents) parts.push(`${queuedAgents} queued`);
153
- if (waitingAgents) parts.push(`${waitingAgents} waiting`);
154
- if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
155
- return `${runningGlyph} Crew agents${notificationBadge(notificationCount)} · ${parts.join(" · ")} · /team-dashboard`;
156
- }
157
-
158
- function shortRunLabel(run: TeamRunManifest): string {
159
- return `${run.team}/${run.workflow ?? "none"}`;
160
- }
161
-
162
- export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8, providedRuns?: WidgetRun[], notificationCount = 0): string[] {
163
- const runs = providedRuns ?? activeWidgetRuns(cwd);
164
- if (!runs.length) return [];
165
- const runningGlyph = SPINNER[frame % SPINNER.length] ?? SPINNER[0];
166
- const lines: string[] = [widgetHeader(runs, runningGlyph, maxLines, notificationCount)];
167
- for (const { run, agents, snapshot } of runs) {
168
- const activeAgents = agents.filter((item) => item.status === "running" || item.status === "queued" || item.status === "waiting");
169
- const completed = agents.filter((agent) => agent.status === "completed").length;
170
- const runGlyph = iconForStatus(run.status, { runningGlyph });
171
- const phaseLine = snapshot ? formatPhaseProgressLine(computePhaseProgress(snapshot.tasks)) : "";
172
- const progressPart = phaseLine ? `${phaseLine}` : `${completed}/${agents.length} done`;
173
- lines.push(`├─ ${runGlyph} ${shortRunLabel(run)} · ${progressPart} · ${run.runId.slice(-8)}`);
174
- const visibleAgents = activeAgents.slice(0, MAX_AGENTS_DISPLAY);
175
- for (const [index, agent] of visibleAgents.entries()) {
176
- const last = index === visibleAgents.length - 1 && activeAgents.length <= MAX_AGENTS_DISPLAY;
177
- const branch = last ? "└─" : "├─";
178
- const agentGlyph = iconForStatus(agent.status, { runningGlyph });
179
- const stats = agentStats(agent);
180
- lines.push(`│ ${branch} ${agentGlyph} ${agent.agent} · ${agent.role}`);
181
- lines.push(`│ ⎿ ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
182
- }
183
- if (activeAgents.length > MAX_AGENTS_DISPLAY) lines.push(`│ └─ … +${activeAgents.length - MAX_AGENTS_DISPLAY} more agents`);
184
- if (lines.length >= maxLines) break;
185
- }
186
- return lines.slice(0, maxLines);
187
- }
188
-
189
- function statusGlyphColor(icon: string): Parameters<CrewTheme["fg"]>[0] {
190
- const mapping: Record<string, Parameters<CrewTheme["fg"]>[0]> = {
191
- "✓": "success",
192
- "✗": "error",
193
- "■": "warning",
194
- "⏸": "warning",
195
- "◦": "dim",
196
- "·": "dim",
197
- "▶": "accent",
198
- };
199
- return mapping[icon] ?? "accent";
200
- }
201
-
202
- function colorWidgetLine(line: string, index: number, theme: CrewTheme): string {
203
- let result = line;
204
- if (index === 0) {
205
- result = result.replace("Crew agents", theme.bold(theme.fg("accent", "Crew agents")));
206
- }
207
- result = result.replace(/[✓✗■⏸◦·▶]/g, (icon) => theme.fg(statusGlyphColor(icon), icon));
208
- if (index === 0) {
209
- result = theme.fg("accent", result);
210
- }
211
- return result;
212
- }
213
-
214
- function renderLines(lines: string[], width: number): string[] {
215
- const box = new Box(0, 0);
216
- for (const line of lines) {
217
- box.addChild(new Text(line));
218
- }
219
- return box.render(width);
220
- }
221
-
222
- class CrewWidgetComponent implements WidgetComponent {
223
- private readonly model: CrewWidgetModel;
224
- private theme: CrewTheme;
225
- private cacheSignature: string;
226
- private cachedWidth = 0;
227
- private cachedLines: string[] = [];
228
- private cachedBaseLines: string[] = [];
229
- private cachedTheme: CrewTheme;
230
- private readonly unsubscribeTheme: () => void;
231
- private readonly unsubscribeEventBus: () => void;
232
-
233
- constructor(model: CrewWidgetModel, themeLike: unknown) {
234
- this.model = model;
235
- this.theme = asCrewTheme(themeLike);
236
- this.cachedTheme = this.theme;
237
- this.cacheSignature = "";
238
- this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
239
- this.unsubscribeEventBus = runEventBus.onAny(() => this.invalidate());
240
- }
241
-
242
- private buildSignature(runs: WidgetRun[]): string {
243
- return runs
244
- .map((entry) => entry.snapshot?.signature ?? `${entry.run.runId}:${entry.run.status}:${entry.run.updatedAt}:` + entry.agents.map((agent) => {
245
- const recentOutput = agent.progress?.recentOutput.at(-1) ?? "";
246
- const progress = [agent.progress?.currentTool ?? "", agent.progress?.toolCount ?? 0, agent.progress?.tokens ?? 0, agent.progress?.turns ?? 0, agent.progress?.lastActivityAt ?? "", recentOutput].join(":");
247
- return `${agent.status}:${agent.startedAt}:${agent.completedAt ?? ""}:${agent.toolUses ?? 0}:${progress}`;
248
- }).join(","))
249
- .join("|");
250
- }
251
-
252
- private colorize(lines: string[], width: number): string[] {
253
- return renderLines(lines.map((line, index) => colorWidgetLine(line, index, this.theme)), width);
254
- }
255
-
256
- invalidate(): void {
257
- this.cacheSignature = "";
258
- this.cachedBaseLines = [];
259
- this.cachedLines = [];
260
- }
261
-
262
- dispose(): void {
263
- this.unsubscribeTheme();
264
- this.unsubscribeEventBus();
265
- }
266
-
267
- render(width: number): string[] {
268
- const runs = activeWidgetRuns(this.model.cwd, this.model.manifestCache, this.model.snapshotCache, this.model.preloadManifests);
269
- const signature = `${this.buildSignature(runs)}:${this.model.notificationCount ?? 0}`;
270
- const runningGlyph = SPINNER[this.model.frame % SPINNER.length] ?? SPINNER[0];
271
- const headerGlyph = runs.length ? SPINNER[0] : " ";
272
-
273
- if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
274
- this.cachedBaseLines = buildCrewWidgetLines(this.model.cwd, 0, this.model.maxLines, runs, this.model.notificationCount ?? 0).map((line, index) => {
275
- if (index === 0 && line.length > 0) return `${headerGlyph}${line.slice(1)}`;
276
- return line;
277
- });
278
- this.cachedLines = this.colorize(this.cachedBaseLines, width);
279
- this.cachedWidth = width;
280
- this.cachedTheme = this.theme;
281
- this.cacheSignature = signature;
282
- }
283
-
284
- if (runs.length === 0) return [];
285
-
286
- // Update only spinner and command icon on header line to avoid full re-color for every frame.
287
- const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
288
- this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
289
- // Safety: ensure all lines fit within terminal width (handles emoji/CJK width mismatch)
290
- return this.cachedLines.map((line) => truncate(line, width));
291
- }
292
- }
293
-
294
- export function updateCrewWidget(
295
- ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">,
296
- state: CrewWidgetState,
297
- config?: CrewUiConfig,
298
- manifestCache?: ManifestCache,
299
- snapshotCache?: RunSnapshotCache,
300
- preloadedManifests?: TeamRunManifest[],
301
- ): void {
302
- if (!ctx.hasUI) return;
303
- state.frame += 1;
304
- const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
305
- const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests);
306
- const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0);
307
- const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
308
- ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
309
- const shouldClearLegacy = state.legacyCleared !== true || state.lastPlacement !== placement;
310
- if (shouldClearLegacy) {
311
- setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
312
- state.legacyCleared = true;
313
- }
314
- if (!lines.length) {
315
- if (state.lastVisibility !== "hidden" || state.lastPlacement !== placement) {
316
- setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
317
- state.lastVisibility = "hidden";
318
- state.lastPlacement = placement;
319
- state.lastKey = WIDGET_KEY;
320
- state.lastMaxLines = maxLines;
321
- state.lastCwd = ctx.cwd;
322
- state.model = undefined;
323
- }
324
- requestRender(ctx);
325
- return;
326
- }
327
- const needsWidgetInstall = state.lastVisibility !== "visible" || state.lastPlacement !== placement || state.lastKey !== WIDGET_KEY || state.lastMaxLines !== maxLines || state.lastCwd !== ctx.cwd || !state.model;
328
- if (!state.model) state.model = { cwd: ctx.cwd, frame: state.frame, maxLines, notificationCount: state.notificationCount ?? 0, manifestCache, snapshotCache, preloadManifests: preloadedManifests };
329
- else {
330
- state.model.cwd = ctx.cwd;
331
- state.model.frame = state.frame;
332
- state.model.maxLines = maxLines;
333
- state.model.notificationCount = state.notificationCount ?? 0;
334
- state.model.manifestCache = manifestCache;
335
- state.model.snapshotCache = snapshotCache;
336
- state.model.preloadManifests = preloadedManifests;
337
- }
338
- if (needsWidgetInstall) {
339
- const model = state.model;
340
- setExtensionWidget(
341
- ctx,
342
- WIDGET_KEY,
343
- ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme)) as never,
344
- { placement, persist: true },
345
- );
346
- state.lastVisibility = "visible";
347
- state.lastPlacement = placement;
348
- state.lastKey = WIDGET_KEY;
349
- state.lastMaxLines = maxLines;
350
- state.lastCwd = ctx.cwd;
351
- }
352
- requestRender(ctx);
353
- }
354
-
355
- export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState, config?: CrewUiConfig): void {
356
- if (state.interval) clearInterval(state.interval);
357
- state.interval = undefined;
358
- if (ctx?.hasUI) {
359
- const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
360
- ctx.ui.setStatus(STATUS_KEY, undefined);
361
- setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
362
- setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
363
- state.lastVisibility = "hidden";
364
- state.lastPlacement = placement;
365
- state.lastKey = WIDGET_KEY;
366
- state.model = undefined;
367
- state.legacyCleared = true;
368
- requestRender(ctx);
369
- }
370
- }
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { CrewUiConfig } from "../config/config.ts";
3
+ import { listRecentRuns } from "../extension/run-index.ts";
4
+ import { readCrewAgents } from "../runtime/crew-agent-records.ts";
5
+ import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
6
+ import { isDisplayActiveRun } from "../runtime/process-status.ts";
7
+ import { listLiveAgents, evictStaleLiveAgentHandles, type LiveAgentHandle } from "../runtime/live-agent-manager.ts";
8
+ import { getTaskUsage } from "../runtime/usage-tracker.ts";
9
+ import type { TeamRunManifest } from "../state/types.ts";
10
+ import type { ManifestCache } from "../runtime/manifest-cache.ts";
11
+ import { reconcileAllStaleRuns } from "../runtime/crash-recovery.ts";
12
+ import { colorForStatus, iconForStatus, type RunStatus } from "./status-colors.ts";
13
+ import { pad, truncate } from "../utils/visual.ts";
14
+ import type { CrewTheme } from "./theme-adapter.ts";
15
+ import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
16
+ import { Box, Text } from "./layout-primitives.ts";
17
+ import { requestRender, setExtensionWidget } from "./pi-ui-compat.ts";
18
+ import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
19
+ import { runEventBus } from "./run-event-bus.ts";
20
+ import { DEFAULT_UI } from "../config/defaults.ts";
21
+ import { computePhaseProgress, formatPhaseProgressLine } from "../runtime/phase-progress.ts";
22
+ import { SUBAGENT_SPINNER_FRAMES, spinnerBucket, spinnerFrame } from "./spinner.ts";
23
+
24
+ const SPINNER = SUBAGENT_SPINNER_FRAMES;
25
+ const TOOL_LABELS: Record<string, string> = {
26
+ read: "reading",
27
+ bash: "running command",
28
+ edit: "editing",
29
+ write: "writing",
30
+ grep: "searching",
31
+ find: "finding files",
32
+ ls: "listing",
33
+ };
34
+ const LEGACY_WIDGET_KEY = "pi-crew";
35
+ const WIDGET_KEY = "pi-crew-active";
36
+ const STATUS_KEY = "pi-crew";
37
+
38
+ const MAX_LINES_DEFAULT = DEFAULT_UI.widgetMaxLines;
39
+ const MAX_AGENTS_DISPLAY = 3;
40
+ /** R1: How many turns finished agents linger before disappearing. */
41
+ const FINISHED_LINGER_MAX_AGE = 1;
42
+ const ERROR_LINGER_MAX_AGE = 2;
43
+ const ERROR_STATUSES = new Set(["failed", "cancelled", "stopped"]);
44
+ /** R3: Faster refresh when live agents are running. Aligned with spinner frame. */
45
+ const LIVE_REFRESH_MS = 160;
46
+
47
+ type WidgetComponent = { render(width: number): string[]; invalidate(): void };
48
+
49
+ interface CrewWidgetModel {
50
+ cwd: string;
51
+ frame: number;
52
+ maxLines: number;
53
+ notificationCount?: number;
54
+ manifestCache?: ManifestCache;
55
+ snapshotCache?: RunSnapshotCache;
56
+ preloadManifests?: TeamRunManifest[];
57
+ }
58
+
59
+ export interface CrewWidgetState {
60
+ frame: number;
61
+ interval?: ReturnType<typeof setInterval>;
62
+ lastPlacement?: string;
63
+ lastVisibility?: "hidden" | "visible";
64
+ lastKey?: string;
65
+ lastMaxLines?: number;
66
+ lastCwd?: string;
67
+ legacyCleared?: boolean;
68
+ model?: CrewWidgetModel;
69
+ notificationCount?: number;
70
+ }
71
+
72
+ interface WidgetRun {
73
+ run: TeamRunManifest;
74
+ agents: CrewAgentRecord[];
75
+ snapshot?: RunUiSnapshot;
76
+ }
77
+
78
+ function elapsed(iso: string | undefined, now = Date.now()): string | undefined {
79
+ if (!iso) return undefined;
80
+ const ms = Math.max(0, now - new Date(iso).getTime());
81
+ if (!Number.isFinite(ms)) return undefined;
82
+ if (ms < 1000) return "now";
83
+ if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
84
+ if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
85
+ return `${Math.floor(ms / 3_600_000)}h`;
86
+ }
87
+
88
+ function describeLiveActivity(handle: LiveAgentHandle): string {
89
+ const act = handle.activity;
90
+ if (act.activeTools.size > 0) {
91
+ const groups = new Map<string, number>();
92
+ for (const toolName of act.activeTools.values()) {
93
+ const label = TOOL_LABELS[toolName] ?? toolName;
94
+ groups.set(label, (groups.get(label) ?? 0) + 1);
95
+ }
96
+ const parts: string[] = [];
97
+ for (const [label, count] of groups) {
98
+ if (count > 1) {
99
+ const noun = label === "searching" ? "patterns" : label === "listing" ? "entries" : "files";
100
+ parts.push(`${label} ${count} ${noun}`);
101
+ } else {
102
+ parts.push(label);
103
+ }
104
+ }
105
+ return parts.join(", ") + "…";
106
+ }
107
+ if (act.responseText?.trim()) {
108
+ const line = act.responseText.split("\n").find((l) => l.trim())?.trim() ?? "";
109
+ return line.length > 60 ? line.slice(0, 60) + "…" : line;
110
+ }
111
+ return "thinking…";
112
+ }
113
+
114
+ function agentActivity(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle): string {
115
+ if (liveHandle && liveHandle.status === "running") {
116
+ const live = describeLiveActivity(liveHandle);
117
+ // If live activity is just the fallback, prefer richer agent.progress data
118
+ // (persistLiveProgress writes tool events to agentProgress before live tracking picks them up)
119
+ if (live === "thinking…" && agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
120
+ return live;
121
+ }
122
+ if (agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
123
+ const recent = agent.progress?.recentOutput?.at(-1);
124
+ if (recent) return recent.replace(/\s+/g, " ").trim();
125
+ if (agent.progress?.activityState === "needs_attention") return "needs attention";
126
+ if (agent.status === "queued") return "queued";
127
+ if (agent.status === "running") {
128
+ const age = agent.startedAt ? Date.now() - new Date(agent.startedAt).getTime() : Infinity;
129
+ if (age < 5000 && !agent.progress?.currentTool) return "spawning…";
130
+ return "thinking…";
131
+ }
132
+ if (agent.status === "failed") return agent.error ?? "failed";
133
+ return "done";
134
+ }
135
+
136
+ function formatTokensCompact(count: number): string {
137
+ if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M tok`;
138
+ if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k tok`;
139
+ return `${count} tok`;
140
+ }
141
+
142
+ function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle): string {
143
+ const parts: string[] = [];
144
+ if (liveHandle) {
145
+ const act = liveHandle.activity;
146
+ if (act.toolUses > 0) parts.push(`${act.toolUses} tools`);
147
+ const usage = getTaskUsage(liveHandle.taskId);
148
+ const total = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheWrite ?? 0);
149
+ if (total > 0) parts.push(formatTokensCompact(total));
150
+ try {
151
+ const stats = liveHandle.session.getSessionStats?.();
152
+ const ctxPct = stats?.contextUsage?.percent;
153
+ if (ctxPct != null) parts.push(`${Math.round(ctxPct)}% ctx`);
154
+ } catch { /* ignore */ }
155
+ const ms = (act.completedAtMs ?? Date.now()) - act.startedAtMs;
156
+ parts.push(`${(ms / 1000).toFixed(1)}s`);
157
+ } else {
158
+ if (agent.toolUses) parts.push(`${agent.toolUses} tools`);
159
+ if (agent.progress?.tokens) parts.push(formatTokensCompact(agent.progress.tokens));
160
+ const age = elapsed(agent.completedAt ?? agent.startedAt);
161
+ if (age) parts.push(age);
162
+ }
163
+ return parts.join(" · ");
164
+ }
165
+
166
+ function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
167
+ try {
168
+ return readCrewAgents(run);
169
+ } catch {
170
+ return [];
171
+ }
172
+ }
173
+
174
+ /** Timestamp of the last periodic stale-reconciliation. Throttled to 60s to avoid excessive disk I/O. */
175
+ let lastStaleReconcileAt = 0;
176
+ const STALE_RECONCILE_INTERVAL_MS = 60_000;
177
+
178
+ export function activeWidgetRuns(cwd: string, manifestCache?: ManifestCache, snapshotCache?: RunSnapshotCache, preloadedManifests?: TeamRunManifest[]): WidgetRun[] {
179
+ // Evict stale live-agent handles (terminal status >10min, or running >30min with no update)
180
+ evictStaleLiveAgentHandles();
181
+ // Periodic stale reconciliation: detect ghost runs on disk with dead PIDs
182
+ // and repair them to terminal status. Throttled to once per 60 seconds.
183
+ const now = Date.now();
184
+ if (now - lastStaleReconcileAt > STALE_RECONCILE_INTERVAL_MS && manifestCache) {
185
+ lastStaleReconcileAt = now;
186
+ try { reconcileAllStaleRuns(cwd, manifestCache); } catch { /* non-critical background maintenance */ }
187
+ }
188
+ const runs = preloadedManifests ?? (manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20));
189
+ return runs
190
+ .map((run) => {
191
+ try {
192
+ // 1.2: render path is read-only. Use cache.get() only; if miss
193
+ // the background preload loop will populate on the next tick.
194
+ // Fall back to manifest-only data so the widget still renders
195
+ // without blocking on disk.
196
+ const snapshot = snapshotCache?.get(run.runId);
197
+ return snapshot ? { run: snapshot.manifest, agents: snapshot.agents, snapshot } : { run, agents: agentsFor(run) };
198
+ } catch {
199
+ return { run, agents: agentsFor(run) };
200
+ }
201
+ })
202
+ .filter((item) => isDisplayActiveRun(item.run, item.agents));
203
+ }
204
+
205
+ function statusSummary(runs: WidgetRun[]): string {
206
+ const agents = runs.flatMap((item) => item.agents);
207
+ const runningAgents = agents.filter((agent) => agent.status === "running").length;
208
+ const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
209
+ const waitingAgents = agents.filter((agent) => agent.status === "waiting").length;
210
+ const completedAgents = agents.filter((agent) => agent.status === "completed").length;
211
+ const parts = [`${runningAgents} running`];
212
+ if (queuedAgents) parts.push(`${queuedAgents} queued`);
213
+ if (waitingAgents) parts.push(`${waitingAgents} waiting`);
214
+ if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
215
+ return `Crew: ${parts.join(", ")}`;
216
+ }
217
+
218
+ export function notificationBadge(count: number | undefined, env: NodeJS.ProcessEnv = process.env): string {
219
+ if (!count || count <= 0) return "";
220
+ const term = `${env.TERM ?? ""} ${env.WT_SESSION ?? ""} ${env.TERM_PROGRAM ?? ""}`.toLowerCase();
221
+ const supportsEmoji = !term.includes("dumb") && env.NO_COLOR !== "1";
222
+ return supportsEmoji ? ` 🔔${count}` : ` [!${count}]`;
223
+ }
224
+
225
+ export function widgetHeader(runs: WidgetRun[], runningGlyph: string, maxLines = 20, notificationCount = 0): string {
226
+ const agents = runs.flatMap((item) => item.agents);
227
+ const runningAgents = agents.filter((agent) => agent.status === "running").length;
228
+ const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
229
+ const waitingAgents = agents.filter((agent) => agent.status === "waiting").length;
230
+ const completedAgents = agents.filter((agent) => agent.status === "completed").length;
231
+ const parts = [`${runningAgents} running`];
232
+ if (queuedAgents) parts.push(`${queuedAgents} queued`);
233
+ if (waitingAgents) parts.push(`${waitingAgents} waiting`);
234
+ if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
235
+ return `${runningGlyph} Crew agents${notificationBadge(notificationCount)} · ${parts.join(" · ")} · /team-dashboard`;
236
+ }
237
+
238
+ function shortRunLabel(run: TeamRunManifest): string {
239
+ return `${run.team}/${run.workflow ?? "none"}`;
240
+ }
241
+
242
+ export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8, providedRuns?: WidgetRun[], notificationCount = 0): string[] {
243
+ const runs = providedRuns ?? activeWidgetRuns(cwd);
244
+ if (!runs.length) return [];
245
+ // Time-based spinner glyph so animation stays smooth at the renderer's
246
+ // natural cadence independent of how often `frame` counter advances.
247
+ const runningGlyph = spinnerFrame("widget-header");
248
+ const lines: string[] = [widgetHeader(runs, runningGlyph, maxLines, notificationCount)];
249
+ for (const { run, agents, snapshot } of runs) {
250
+ const activeAgents = agents.filter((item) => item.status === "running" || item.status === "queued" || item.status === "waiting");
251
+ // R1: Include recently finished agents (linger 1-2 min)
252
+ const now = Date.now();
253
+ const finishedAgents = agents.filter((item) => {
254
+ if (item.status === "running" || item.status === "queued" || item.status === "waiting") return false;
255
+ if (!item.completedAt) return false;
256
+ const maxAgeMs = (ERROR_STATUSES.has(item.status) ? ERROR_LINGER_MAX_AGE : FINISHED_LINGER_MAX_AGE) * 60_000;
257
+ const age = now - new Date(item.completedAt).getTime();
258
+ return Number.isFinite(age) && age < maxAgeMs;
259
+ });
260
+ const completed = agents.filter((agent) => agent.status === "completed").length;
261
+ const runGlyph = iconForStatus(run.status, { runningGlyph });
262
+ const phaseLine = snapshot ? formatPhaseProgressLine(computePhaseProgress(snapshot.tasks)) : "";
263
+ const progressPart = phaseLine ? `${phaseLine}` : `${completed}/${agents.length} done`;
264
+ lines.push(`\u251C\u2500 ${runGlyph} ${shortRunLabel(run)} \u00B7 ${progressPart} \u00B7 ${run.runId.slice(-8)}`);
265
+ const liveForRun = listLiveAgents().filter((a) => a.runId === run.runId);
266
+ // Render finished agents first (compact 1-line format)
267
+ for (const agent of finishedAgents.slice(0, 2)) {
268
+ const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
269
+ const name = liveHandle?.agent ?? agent.agent;
270
+ const icon = agent.status === "completed" ? "\u2713" : agent.status === "failed" ? "\u2717" : "\u25AA";
271
+ const stats = agentStats(agent, liveHandle);
272
+ const desc = liveHandle?.description ?? agent.role;
273
+ lines.push(`\u2502 \u251C\u2500 ${icon} ${name} \u00B7 ${desc}${stats ? ` \u00B7 ${stats}` : ""}`);
274
+ }
275
+ // Render active agents
276
+ const visibleAgents = activeAgents.slice(0, MAX_AGENTS_DISPLAY);
277
+ for (const [index, agent] of visibleAgents.entries()) {
278
+ const last = index === visibleAgents.length - 1 && activeAgents.length <= MAX_AGENTS_DISPLAY;
279
+ const branch = last ? "\u2514\u2500" : "\u251C\u2500";
280
+ const agentGlyph = iconForStatus(agent.status, { runningGlyph });
281
+ const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
282
+ const stats = agentStats(agent, liveHandle);
283
+ const name = liveHandle?.agent ?? agent.agent;
284
+ const desc = liveHandle?.description ?? agent.role;
285
+ lines.push(`\u2502 ${branch} ${agentGlyph} ${name}${desc ? ` \u00B7 ${desc}` : ` \u00B7 ${agent.role}`}`);
286
+ lines.push(`\u2502 \u23B7 ${agentActivity(agent, liveHandle)}${stats ? ` \u00B7 ${stats}` : ""}`);
287
+ }
288
+ if (activeAgents.length > MAX_AGENTS_DISPLAY) lines.push(`\u2502 \u2514\u2500 \u2026 +${activeAgents.length - MAX_AGENTS_DISPLAY} more agents`);
289
+ if (lines.length >= maxLines) break;
290
+ }
291
+ return lines.slice(0, maxLines);
292
+ }
293
+
294
+ function statusGlyphColor(icon: string): Parameters<CrewTheme["fg"]>[0] {
295
+ const mapping: Record<string, Parameters<CrewTheme["fg"]>[0]> = {
296
+ "✓": "success",
297
+ "✗": "error",
298
+ "■": "warning",
299
+ "⏸": "warning",
300
+ "◦": "dim",
301
+ "·": "dim",
302
+ "▶": "accent",
303
+ };
304
+ return mapping[icon] ?? "accent";
305
+ }
306
+
307
+ function colorWidgetLine(line: string, index: number, theme: CrewTheme): string {
308
+ let result = line;
309
+ if (index === 0) {
310
+ result = result.replace("Crew agents", theme.bold(theme.fg("accent", "Crew agents")));
311
+ }
312
+ result = result.replace(/[✓✗■⏸◦·▶]/g, (icon) => theme.fg(statusGlyphColor(icon), icon));
313
+ if (index === 0) {
314
+ result = theme.fg("accent", result);
315
+ }
316
+ return result;
317
+ }
318
+
319
+ function renderLines(lines: string[], width: number): string[] {
320
+ const box = new Box(0, 0);
321
+ for (const line of lines) {
322
+ box.addChild(new Text(line));
323
+ }
324
+ return box.render(width);
325
+ }
326
+
327
+ class CrewWidgetComponent implements WidgetComponent {
328
+ private readonly model: CrewWidgetModel;
329
+ private theme: CrewTheme;
330
+ private cacheSignature: string;
331
+ private cachedWidth = 0;
332
+ private cachedLines: string[] = [];
333
+ private cachedBaseLines: string[] = [];
334
+ private cachedTheme: CrewTheme;
335
+ private readonly unsubscribeTheme: () => void;
336
+ private readonly unsubscribeEventBus: () => void;
337
+
338
+ constructor(model: CrewWidgetModel, themeLike: unknown) {
339
+ this.model = model;
340
+ this.theme = asCrewTheme(themeLike);
341
+ this.cachedTheme = this.theme;
342
+ this.cacheSignature = "";
343
+ this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
344
+ this.unsubscribeEventBus = runEventBus.onAny(() => this.invalidate());
345
+ }
346
+
347
+ private buildSignature(runs: WidgetRun[]): string {
348
+ const liveSig = listLiveAgents().map((h) => `${h.agentId}:${h.status}:${h.activity.turnCount}:${h.activity.toolUses}:${[...h.activity.activeTools.values()].join(",")}:${h.activity.responseText.slice(-30)}`).join("|");
349
+ // When any agent is running we want per-agent runningGlyph (baked into
350
+ // cachedBaseLines) to re-rotate in step with the spinner bucket. Without
351
+ // this, finished+running rows would freeze between data updates.
352
+ const hasRunning = runs.some((entry) => entry.agents.some((agent) => agent.status === "running"))
353
+ || listLiveAgents().some((h) => h.status === "running");
354
+ const animation = hasRunning ? `:spin=${spinnerBucket()}` : "";
355
+ return runs
356
+ .map((entry) => entry.snapshot?.signature ?? `${entry.run.runId}:${entry.run.status}:${entry.run.updatedAt}:` + entry.agents.map((agent) => {
357
+ const recentOutput = agent.progress?.recentOutput.at(-1) ?? "";
358
+ const progress = [agent.progress?.currentTool ?? "", agent.progress?.toolCount ?? 0, agent.progress?.tokens ?? 0, agent.progress?.turns ?? 0, agent.progress?.lastActivityAt ?? "", recentOutput].join(":");
359
+ return `${agent.status}:${agent.startedAt}:${agent.completedAt ?? ""}:${agent.toolUses ?? 0}:${progress}`;
360
+ }).join(","))
361
+ .join("|") + `|live:${liveSig}${animation}`;
362
+ }
363
+
364
+ private colorize(lines: string[], width: number): string[] {
365
+ return renderLines(lines.map((line, index) => colorWidgetLine(line, index, this.theme)), width);
366
+ }
367
+
368
+ invalidate(): void {
369
+ this.cacheSignature = "";
370
+ this.cachedBaseLines = [];
371
+ this.cachedLines = [];
372
+ }
373
+
374
+ dispose(): void {
375
+ this.unsubscribeTheme();
376
+ this.unsubscribeEventBus();
377
+ }
378
+
379
+ render(width: number): string[] {
380
+ const runs = activeWidgetRuns(this.model.cwd, this.model.manifestCache, this.model.snapshotCache, this.model.preloadManifests);
381
+ const signature = `${this.buildSignature(runs)}:${this.model.notificationCount ?? 0}`;
382
+ // Time-based glyphs so the spinner animates every render call rather than
383
+ // only when state.frame is bumped (which happens at the slower data refresh).
384
+ const runningGlyph = spinnerFrame("widget-header");
385
+ const headerGlyph = runs.length ? spinnerFrame("widget-header") : " ";
386
+
387
+ if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
388
+ this.cachedBaseLines = buildCrewWidgetLines(this.model.cwd, 0, this.model.maxLines, runs, this.model.notificationCount ?? 0).map((line, index) => {
389
+ if (index === 0 && line.length > 0) return `${headerGlyph}${line.slice(1)}`;
390
+ return line;
391
+ });
392
+ this.cachedLines = this.colorize(this.cachedBaseLines, width);
393
+ this.cachedWidth = width;
394
+ this.cachedTheme = this.theme;
395
+ this.cacheSignature = signature;
396
+ }
397
+
398
+ if (runs.length === 0) {
399
+ this.invalidate();
400
+ return [];
401
+ }
402
+
403
+ // Update only spinner and command icon on header line to avoid full re-color for every frame.
404
+ const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
405
+ this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
406
+ // Safety: ensure all lines fit within terminal width (handles emoji/CJK width mismatch)
407
+ return this.cachedLines.map((line) => truncate(line, width));
408
+ }
409
+ }
410
+
411
+ export function updateCrewWidget(
412
+ ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">,
413
+ state: CrewWidgetState,
414
+ config?: CrewUiConfig,
415
+ manifestCache?: ManifestCache,
416
+ snapshotCache?: RunSnapshotCache,
417
+ preloadedManifests?: TeamRunManifest[],
418
+ ): void {
419
+ if (!ctx.hasUI) return;
420
+ state.frame += 1;
421
+ const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
422
+ const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests);
423
+ const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0);
424
+ const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
425
+ ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
426
+ const shouldClearLegacy = state.legacyCleared !== true || state.lastPlacement !== placement;
427
+ if (shouldClearLegacy) {
428
+ setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
429
+ state.legacyCleared = true;
430
+ }
431
+ if (!lines.length) {
432
+ if (state.lastVisibility !== "hidden" || state.lastPlacement !== placement) {
433
+ setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
434
+ state.lastVisibility = "hidden";
435
+ state.lastPlacement = placement;
436
+ state.lastKey = WIDGET_KEY;
437
+ state.lastMaxLines = maxLines;
438
+ state.lastCwd = ctx.cwd;
439
+ state.model = undefined;
440
+ }
441
+ requestRender(ctx);
442
+ return;
443
+ }
444
+ const needsWidgetInstall = state.lastVisibility !== "visible" || state.lastPlacement !== placement || state.lastKey !== WIDGET_KEY || state.lastMaxLines !== maxLines || state.lastCwd !== ctx.cwd || !state.model;
445
+ if (!state.model) state.model = { cwd: ctx.cwd, frame: state.frame, maxLines, notificationCount: state.notificationCount ?? 0, manifestCache, snapshotCache, preloadManifests: preloadedManifests };
446
+ else {
447
+ state.model.cwd = ctx.cwd;
448
+ state.model.frame = state.frame;
449
+ state.model.maxLines = maxLines;
450
+ state.model.notificationCount = state.notificationCount ?? 0;
451
+ state.model.manifestCache = manifestCache;
452
+ state.model.snapshotCache = snapshotCache;
453
+ state.model.preloadManifests = preloadedManifests;
454
+ }
455
+ if (needsWidgetInstall) {
456
+ const model = state.model;
457
+ setExtensionWidget(
458
+ ctx,
459
+ WIDGET_KEY,
460
+ ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme)) as never,
461
+ { placement, persist: true },
462
+ );
463
+ state.lastVisibility = "visible";
464
+ state.lastPlacement = placement;
465
+ state.lastKey = WIDGET_KEY;
466
+ state.lastMaxLines = maxLines;
467
+ state.lastCwd = ctx.cwd;
468
+ }
469
+ requestRender(ctx);
470
+ }
471
+
472
+ export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState, config?: CrewUiConfig): void {
473
+ if (state.interval) clearInterval(state.interval);
474
+ state.interval = undefined;
475
+ if (ctx?.hasUI) {
476
+ const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
477
+ ctx.ui.setStatus(STATUS_KEY, undefined);
478
+ setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
479
+ setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
480
+ state.lastVisibility = "hidden";
481
+ state.lastPlacement = placement;
482
+ state.lastKey = WIDGET_KEY;
483
+ state.model = undefined;
484
+ state.legacyCleared = true;
485
+ requestRender(ctx);
486
+ }
487
+ }