pi-crew 0.9.53 → 0.9.55

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 (493) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +6 -3
  3. package/scripts/clean-strip-types.mjs +60 -0
  4. package/src/adapters/claude-adapter.ts +23 -0
  5. package/src/adapters/codex-adapter.ts +21 -0
  6. package/src/adapters/cursor-adapter.ts +17 -0
  7. package/src/adapters/export-util.ts +143 -0
  8. package/src/adapters/index.ts +15 -0
  9. package/src/adapters/registry.ts +18 -0
  10. package/src/adapters/types.ts +23 -0
  11. package/src/agents/agent-config.ts +194 -0
  12. package/src/agents/agent-search.ts +98 -0
  13. package/src/agents/agent-serializer.ts +38 -0
  14. package/src/agents/discover-agents.ts +623 -0
  15. package/src/benchmark/benchmark-runner.ts +313 -0
  16. package/src/benchmark/feedback-loop.ts +73 -0
  17. package/src/config/config.ts +1270 -0
  18. package/src/config/defaults.ts +193 -0
  19. package/src/config/drift-detector.ts +290 -0
  20. package/src/config/markers.ts +330 -0
  21. package/src/config/resilient-parser.ts +117 -0
  22. package/src/config/role-tools.ts +118 -0
  23. package/src/config/suggestions.ts +75 -0
  24. package/src/config/types.ts +280 -0
  25. package/src/errors.ts +194 -0
  26. package/src/extension/action-suggestions.ts +117 -0
  27. package/src/extension/async-notifier.ts +179 -0
  28. package/src/extension/autonomous-policy.ts +209 -0
  29. package/src/extension/command-completions.ts +120 -0
  30. package/src/extension/context-status-injection.ts +185 -0
  31. package/src/extension/crew-autocomplete.ts +131 -0
  32. package/src/extension/crew-cleanup.ts +168 -0
  33. package/src/extension/crew-input-router.ts +93 -0
  34. package/src/extension/crew-shortcuts.ts +87 -0
  35. package/src/extension/crew-vibes/cat-frames.ts +18 -0
  36. package/src/extension/crew-vibes/config.ts +195 -0
  37. package/src/extension/crew-vibes/figures.ts +101 -0
  38. package/src/extension/crew-vibes/font-detect.ts +71 -0
  39. package/src/extension/crew-vibes/footer.ts +292 -0
  40. package/src/extension/crew-vibes/index.ts +432 -0
  41. package/src/extension/crew-vibes/provider-usage.ts +396 -0
  42. package/src/extension/crew-vibes/render.ts +206 -0
  43. package/src/extension/crew-vibes/speed.ts +286 -0
  44. package/src/extension/cross-extension-rpc.ts +350 -0
  45. package/src/extension/help.ts +62 -0
  46. package/src/extension/import-index.ts +74 -0
  47. package/src/extension/knowledge-injection.ts +402 -0
  48. package/src/extension/management.ts +571 -0
  49. package/src/extension/message-renderers.ts +108 -0
  50. package/src/extension/notification-router.ts +152 -0
  51. package/src/extension/notification-sink.ts +54 -0
  52. package/src/extension/pi-api.ts +56 -0
  53. package/src/extension/plan-orchestrate.ts +302 -0
  54. package/src/extension/project-init.ts +170 -0
  55. package/src/extension/register.ts +113 -0
  56. package/src/extension/registration/artifact-cleanup.ts +20 -0
  57. package/src/extension/registration/command-registration.ts +58 -0
  58. package/src/extension/registration/command-utils.ts +58 -0
  59. package/src/extension/registration/commands.ts +1228 -0
  60. package/src/extension/registration/compaction-guard.ts +333 -0
  61. package/src/extension/registration/context-builder.ts +135 -0
  62. package/src/extension/registration/crash-recovery-cache.ts +54 -0
  63. package/src/extension/registration/foreground-run-controller.ts +238 -0
  64. package/src/extension/registration/hook-registration.ts +119 -0
  65. package/src/extension/registration/lazy-configurers.ts +124 -0
  66. package/src/extension/registration/lifecycle-handlers.ts +923 -0
  67. package/src/extension/registration/lifecycle.ts +259 -0
  68. package/src/extension/registration/observability.ts +322 -0
  69. package/src/extension/registration/registration-types.ts +158 -0
  70. package/src/extension/registration/runtime-cleanup.ts +230 -0
  71. package/src/extension/registration/subagent-helpers.ts +125 -0
  72. package/src/extension/registration/subagent-manager-setup.ts +330 -0
  73. package/src/extension/registration/subagent-tools.ts +456 -0
  74. package/src/extension/registration/team-tool.ts +225 -0
  75. package/src/extension/registration/tool-registration.ts +50 -0
  76. package/src/extension/registration/ui.ts +172 -0
  77. package/src/extension/registration/viewers.ts +121 -0
  78. package/src/extension/registration/wire-cross-extension.ts +37 -0
  79. package/src/extension/result-watcher.ts +139 -0
  80. package/src/extension/rpc-hmac.ts +256 -0
  81. package/src/extension/run-bundle-schema.ts +104 -0
  82. package/src/extension/run-export.ts +97 -0
  83. package/src/extension/run-import.ts +168 -0
  84. package/src/extension/run-index.ts +131 -0
  85. package/src/extension/run-maintenance.ts +190 -0
  86. package/src/extension/session-summary.ts +18 -0
  87. package/src/extension/team-manager-command.ts +153 -0
  88. package/src/extension/team-onboard.ts +180 -0
  89. package/src/extension/team-recommendation.ts +288 -0
  90. package/src/extension/team-tool/anchor.ts +172 -0
  91. package/src/extension/team-tool/api.ts +1244 -0
  92. package/src/extension/team-tool/auto-summarize.ts +142 -0
  93. package/src/extension/team-tool/cache-control.ts +19 -0
  94. package/src/extension/team-tool/cancel.ts +337 -0
  95. package/src/extension/team-tool/chain-dispatch.ts +95 -0
  96. package/src/extension/team-tool/chain-executor.ts +379 -0
  97. package/src/extension/team-tool/config-patch.ts +50 -0
  98. package/src/extension/team-tool/context.ts +149 -0
  99. package/src/extension/team-tool/destructive-gate.ts +52 -0
  100. package/src/extension/team-tool/dispatch/automate.ts +80 -0
  101. package/src/extension/team-tool/dispatch/control.ts +42 -0
  102. package/src/extension/team-tool/dispatch/index.ts +95 -0
  103. package/src/extension/team-tool/dispatch/manage.ts +161 -0
  104. package/src/extension/team-tool/dispatch/run.ts +53 -0
  105. package/src/extension/team-tool/dispatch/status.ts +187 -0
  106. package/src/extension/team-tool/doctor.ts +431 -0
  107. package/src/extension/team-tool/explain.ts +282 -0
  108. package/src/extension/team-tool/failure-patterns.ts +118 -0
  109. package/src/extension/team-tool/goal-wrap.ts +330 -0
  110. package/src/extension/team-tool/goal.ts +556 -0
  111. package/src/extension/team-tool/handle-schedule.ts +332 -0
  112. package/src/extension/team-tool/handle-settings.ts +520 -0
  113. package/src/extension/team-tool/health-monitor.ts +481 -0
  114. package/src/extension/team-tool/inspect.ts +88 -0
  115. package/src/extension/team-tool/intent-policy.ts +45 -0
  116. package/src/extension/team-tool/lifecycle-actions.ts +615 -0
  117. package/src/extension/team-tool/orchestrate.ts +98 -0
  118. package/src/extension/team-tool/parallel-dispatch.ts +183 -0
  119. package/src/extension/team-tool/plan.ts +50 -0
  120. package/src/extension/team-tool/respond.ts +150 -0
  121. package/src/extension/team-tool/run-deadline.ts +66 -0
  122. package/src/extension/team-tool/run-not-found.ts +49 -0
  123. package/src/extension/team-tool/run.ts +1051 -0
  124. package/src/extension/team-tool/status.ts +260 -0
  125. package/src/extension/team-tool/workflow-manage.ts +261 -0
  126. package/src/extension/team-tool-types.ts +26 -0
  127. package/src/extension/team-tool.ts +750 -0
  128. package/src/extension/tool-result.ts +16 -0
  129. package/src/extension/validate-resources.ts +108 -0
  130. package/src/hooks/registry.ts +200 -0
  131. package/src/hooks/types.ts +69 -0
  132. package/src/i18n.ts +212 -0
  133. package/src/observability/correlation.ts +50 -0
  134. package/src/observability/event-bus.ts +86 -0
  135. package/src/observability/event-to-metric.ts +170 -0
  136. package/src/observability/exporters/adapter.ts +27 -0
  137. package/src/observability/exporters/otlp-exporter.ts +356 -0
  138. package/src/observability/exporters/prometheus-exporter.ts +54 -0
  139. package/src/observability/metric-registry.ts +100 -0
  140. package/src/observability/metric-retention.ts +64 -0
  141. package/src/observability/metric-sink.ts +99 -0
  142. package/src/observability/metrics-primitives.ts +219 -0
  143. package/src/plugins/plugin-define.ts +6 -0
  144. package/src/plugins/plugin-registry.ts +32 -0
  145. package/src/plugins/plugins/index.ts +3 -0
  146. package/src/plugins/plugins/nextjs.ts +19 -0
  147. package/src/plugins/plugins/vite.ts +10 -0
  148. package/src/plugins/plugins/vitest.ts +9 -0
  149. package/src/prompt/prompt-runtime.ts +380 -0
  150. package/src/runtime/adaptive-plan.ts +563 -0
  151. package/src/runtime/agent-control.ts +215 -0
  152. package/src/runtime/agent-memory.ts +81 -0
  153. package/src/runtime/agent-observability.ts +122 -0
  154. package/src/runtime/anchor-manager.ts +475 -0
  155. package/src/runtime/async-marker.ts +32 -0
  156. package/src/runtime/async-runner.ts +381 -0
  157. package/src/runtime/attention-events.ts +28 -0
  158. package/src/runtime/auto-summarize.ts +344 -0
  159. package/src/runtime/background-runner.ts +840 -0
  160. package/src/runtime/batch-barrier.ts +147 -0
  161. package/src/runtime/broker-issuer.ts +39 -0
  162. package/src/runtime/cancellation-token.ts +99 -0
  163. package/src/runtime/cancellation.ts +99 -0
  164. package/src/runtime/capability-inventory.ts +137 -0
  165. package/src/runtime/chain-parser.ts +237 -0
  166. package/src/runtime/chain-runner.ts +606 -0
  167. package/src/runtime/checkpoint.ts +291 -0
  168. package/src/runtime/child-pi-constants.ts +42 -0
  169. package/src/runtime/child-pi-kill.ts +180 -0
  170. package/src/runtime/child-pi-pool.ts +68 -0
  171. package/src/runtime/child-pi-spawn.ts +287 -0
  172. package/src/runtime/child-pi-steering.ts +128 -0
  173. package/src/runtime/child-pi-streams.ts +296 -0
  174. package/src/runtime/child-pi-transcript.ts +169 -0
  175. package/src/runtime/child-pi.ts +1105 -0
  176. package/src/runtime/coalesce-tasks.ts +268 -0
  177. package/src/runtime/code-summary.ts +292 -0
  178. package/src/runtime/command-trace.ts +105 -0
  179. package/src/runtime/compact-pipeline.ts +56 -0
  180. package/src/runtime/compact-stages/ansi-strip-stage.ts +25 -0
  181. package/src/runtime/compact-stages/blank-collapse-stage.ts +31 -0
  182. package/src/runtime/compact-stages/deduplicate-stage.ts +34 -0
  183. package/src/runtime/compact-stages/head-snap-stage.ts +57 -0
  184. package/src/runtime/compact-stages/index.ts +23 -0
  185. package/src/runtime/compact-stages/tail-capture-stage.ts +77 -0
  186. package/src/runtime/compact-stages/truncation-stage.ts +74 -0
  187. package/src/runtime/compaction-summary.ts +278 -0
  188. package/src/runtime/completion-guard.ts +207 -0
  189. package/src/runtime/concurrency.ts +58 -0
  190. package/src/runtime/crash-classification.ts +229 -0
  191. package/src/runtime/crash-recovery.ts +594 -0
  192. package/src/runtime/crew-agent-records.ts +595 -0
  193. package/src/runtime/crew-agent-runtime.ts +62 -0
  194. package/src/runtime/crew-broker-child.ts +88 -0
  195. package/src/runtime/crew-broker-client.ts +673 -0
  196. package/src/runtime/crew-broker-tokens.ts +159 -0
  197. package/src/runtime/crew-broker.ts +1304 -0
  198. package/src/runtime/crew-hooks.ts +227 -0
  199. package/src/runtime/cross-extension-rpc.ts +143 -0
  200. package/src/runtime/custom-tools/irc-tool.ts +282 -0
  201. package/src/runtime/custom-tools/submit-result-tool.ts +102 -0
  202. package/src/runtime/deadletter.ts +47 -0
  203. package/src/runtime/delivery-coordinator.ts +211 -0
  204. package/src/runtime/delta-conflict.ts +348 -0
  205. package/src/runtime/deterministic-ast.ts +161 -0
  206. package/src/runtime/diagnostic-export.ts +171 -0
  207. package/src/runtime/direct-run.ts +45 -0
  208. package/src/runtime/dwf-state-store.ts +102 -0
  209. package/src/runtime/dynamic-workflow-context.ts +1013 -0
  210. package/src/runtime/dynamic-workflow-runner.ts +325 -0
  211. package/src/runtime/effectiveness.ts +90 -0
  212. package/src/runtime/errors/crew-errors.ts +162 -0
  213. package/src/runtime/event-stream-bridge.ts +98 -0
  214. package/src/runtime/foreground-control.ts +193 -0
  215. package/src/runtime/foreground-watchdog.ts +132 -0
  216. package/src/runtime/global-worker-cap.ts +96 -0
  217. package/src/runtime/goal-achievement.ts +148 -0
  218. package/src/runtime/goal-evaluator.ts +365 -0
  219. package/src/runtime/goal-loop-runner.ts +825 -0
  220. package/src/runtime/goal-state-store.ts +219 -0
  221. package/src/runtime/green-contract.ts +55 -0
  222. package/src/runtime/group-join.ts +267 -0
  223. package/src/runtime/handoff-manager.ts +598 -0
  224. package/src/runtime/heartbeat-gradient.ts +36 -0
  225. package/src/runtime/heartbeat-watcher.ts +239 -0
  226. package/src/runtime/hidden-handoff.ts +407 -0
  227. package/src/runtime/important-line-classifier.ts +140 -0
  228. package/src/runtime/intercom-bridge.ts +187 -0
  229. package/src/runtime/iteration-hooks.ts +305 -0
  230. package/src/runtime/live-agent-control.ts +136 -0
  231. package/src/runtime/live-agent-manager.ts +704 -0
  232. package/src/runtime/live-control-realtime.ts +56 -0
  233. package/src/runtime/live-extension-bridge.ts +148 -0
  234. package/src/runtime/live-irc.ts +97 -0
  235. package/src/runtime/live-session-health.ts +108 -0
  236. package/src/runtime/live-session-runtime.ts +1146 -0
  237. package/src/runtime/loop-gates.ts +128 -0
  238. package/src/runtime/manifest-cache.ts +375 -0
  239. package/src/runtime/mcp-proxy.ts +105 -0
  240. package/src/runtime/metric-parser.ts +36 -0
  241. package/src/runtime/model-fallback.ts +409 -0
  242. package/src/runtime/model-resolver.ts +126 -0
  243. package/src/runtime/model-scope.ts +158 -0
  244. package/src/runtime/orphan-worker-registry.ts +465 -0
  245. package/src/runtime/output-validator.ts +202 -0
  246. package/src/runtime/overflow-recovery.ts +206 -0
  247. package/src/runtime/parallel-research.ts +68 -0
  248. package/src/runtime/parallel-utils.ts +160 -0
  249. package/src/runtime/parent-guard.ts +134 -0
  250. package/src/runtime/path-overlap.ts +150 -0
  251. package/src/runtime/peer-dep.ts +292 -0
  252. package/src/runtime/per-write-validator.ts +181 -0
  253. package/src/runtime/phase-progress.ts +217 -0
  254. package/src/runtime/phase-tracker.ts +385 -0
  255. package/src/runtime/pi-args.ts +655 -0
  256. package/src/runtime/pi-json-output.ts +170 -0
  257. package/src/runtime/pi-spawn.ts +273 -0
  258. package/src/runtime/pipeline-runner.ts +523 -0
  259. package/src/runtime/plan-templates.ts +201 -0
  260. package/src/runtime/policy-engine.ts +115 -0
  261. package/src/runtime/post-checks.ts +142 -0
  262. package/src/runtime/post-exit-stdio-guard.ts +109 -0
  263. package/src/runtime/process-lifecycle.ts +491 -0
  264. package/src/runtime/process-status.ts +154 -0
  265. package/src/runtime/progress-event-coalescer.ts +44 -0
  266. package/src/runtime/progress-tracker.ts +124 -0
  267. package/src/runtime/prose-compressor.ts +162 -0
  268. package/src/runtime/recovery-recipes.ts +203 -0
  269. package/src/runtime/replace.ts +570 -0
  270. package/src/runtime/resilient-edit.ts +153 -0
  271. package/src/runtime/result-extractor.ts +217 -0
  272. package/src/runtime/retry-executor.ts +109 -0
  273. package/src/runtime/retry-runner.ts +336 -0
  274. package/src/runtime/role-permission.ts +59 -0
  275. package/src/runtime/run-coalesced-task-group.ts +308 -0
  276. package/src/runtime/run-drift.ts +219 -0
  277. package/src/runtime/run-tracker.ts +116 -0
  278. package/src/runtime/run-worker.ts +78 -0
  279. package/src/runtime/runtime-policy.ts +29 -0
  280. package/src/runtime/runtime-resolver.ts +172 -0
  281. package/src/runtime/runtime-warmup.ts +160 -0
  282. package/src/runtime/scheduler.ts +353 -0
  283. package/src/runtime/semaphore.ts +141 -0
  284. package/src/runtime/sensitive-paths.ts +93 -0
  285. package/src/runtime/session-resources.ts +25 -0
  286. package/src/runtime/session-snapshot.ts +59 -0
  287. package/src/runtime/session-usage.ts +79 -0
  288. package/src/runtime/settings-store.ts +155 -0
  289. package/src/runtime/sidechain-output.ts +35 -0
  290. package/src/runtime/single-agent-compose.ts +84 -0
  291. package/src/runtime/skill-effectiveness.ts +524 -0
  292. package/src/runtime/skill-instructions.ts +394 -0
  293. package/src/runtime/stale-reconciler.ts +680 -0
  294. package/src/runtime/stream-preview.ts +184 -0
  295. package/src/runtime/streaming-output.ts +50 -0
  296. package/src/runtime/subagent-manager.ts +561 -0
  297. package/src/runtime/subprocess-tool-registry.ts +70 -0
  298. package/src/runtime/supervisor-contact.ts +64 -0
  299. package/src/runtime/task-display.ts +52 -0
  300. package/src/runtime/task-graph-scheduler.ts +227 -0
  301. package/src/runtime/task-graph.ts +290 -0
  302. package/src/runtime/task-health.ts +83 -0
  303. package/src/runtime/task-id.ts +161 -0
  304. package/src/runtime/task-output-context.ts +602 -0
  305. package/src/runtime/task-packet.ts +268 -0
  306. package/src/runtime/task-quality.ts +199 -0
  307. package/src/runtime/task-runner/capabilities.ts +78 -0
  308. package/src/runtime/task-runner/child-executor.ts +790 -0
  309. package/src/runtime/task-runner/context-retrieval.ts +163 -0
  310. package/src/runtime/task-runner/live-executor.ts +227 -0
  311. package/src/runtime/task-runner/output-splitter.ts +152 -0
  312. package/src/runtime/task-runner/post-execution.ts +480 -0
  313. package/src/runtime/task-runner/pre-execution.ts +365 -0
  314. package/src/runtime/task-runner/progress.ts +157 -0
  315. package/src/runtime/task-runner/prompt-builder.ts +278 -0
  316. package/src/runtime/task-runner/prompt-pipeline.ts +88 -0
  317. package/src/runtime/task-runner/result-utils.ts +16 -0
  318. package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
  319. package/src/runtime/task-runner/run-projection.ts +125 -0
  320. package/src/runtime/task-runner/scaffold-executor.ts +25 -0
  321. package/src/runtime/task-runner/state-helpers.ts +176 -0
  322. package/src/runtime/task-runner/tail-read.ts +34 -0
  323. package/src/runtime/task-runner.ts +216 -0
  324. package/src/runtime/team-runner-artifacts.ts +13 -0
  325. package/src/runtime/team-runner.ts +2396 -0
  326. package/src/runtime/tool-output-pruner.ts +333 -0
  327. package/src/runtime/tool-progress.ts +278 -0
  328. package/src/runtime/usage-tracker.ts +73 -0
  329. package/src/runtime/verification-gates.ts +579 -0
  330. package/src/runtime/verification-integrity.ts +108 -0
  331. package/src/runtime/verification-worktree.ts +186 -0
  332. package/src/runtime/worker-heartbeat.ts +25 -0
  333. package/src/runtime/worker-startup.ts +83 -0
  334. package/src/runtime/workflow-state.ts +195 -0
  335. package/src/runtime/workspace-lock.ts +443 -0
  336. package/src/runtime/workspace-tree.ts +312 -0
  337. package/src/runtime/yield-handler.ts +224 -0
  338. package/src/runtime/zombie-scanner.ts +298 -0
  339. package/src/schema/config-schema.ts +327 -0
  340. package/src/schema/team-tool-schema.ts +492 -0
  341. package/src/schema/validation-types.ts +159 -0
  342. package/src/skills/discover-skills.ts +203 -0
  343. package/src/skills/skill-templates.ts +456 -0
  344. package/src/skills/validate.ts +285 -0
  345. package/src/state/active-run-registry.ts +439 -0
  346. package/src/state/artifact-store.ts +176 -0
  347. package/src/state/atomic-write.ts +970 -0
  348. package/src/state/blob-store.ts +308 -0
  349. package/src/state/contracts.ts +160 -0
  350. package/src/state/crew-init.ts +269 -0
  351. package/src/state/decision-ledger.ts +372 -0
  352. package/src/state/event-log-rotation.ts +399 -0
  353. package/src/state/event-log.ts +1465 -0
  354. package/src/state/event-reconstructor.ts +229 -0
  355. package/src/state/gitignore-manager.ts +46 -0
  356. package/src/state/health-store.ts +79 -0
  357. package/src/state/hook-instinct-bridge.ts +94 -0
  358. package/src/state/hook-integrations.ts +51 -0
  359. package/src/state/instinct-store.ts +275 -0
  360. package/src/state/jsonl-writer.ts +107 -0
  361. package/src/state/locks.ts +562 -0
  362. package/src/state/mailbox.ts +916 -0
  363. package/src/state/observation-store.ts +176 -0
  364. package/src/state/run-cache.ts +193 -0
  365. package/src/state/run-graph.ts +183 -0
  366. package/src/state/run-metrics.ts +165 -0
  367. package/src/state/schedule.ts +166 -0
  368. package/src/state/session-state-map.ts +51 -0
  369. package/src/state/state-store.ts +973 -0
  370. package/src/state/task-claims.ts +61 -0
  371. package/src/state/tiered-eval.ts +480 -0
  372. package/src/state/types-eval.ts +58 -0
  373. package/src/state/types.ts +447 -0
  374. package/src/state/usage.ts +138 -0
  375. package/src/state/worker-atomic-writer.ts +198 -0
  376. package/src/subagents/async-entry.ts +1 -0
  377. package/src/subagents/index.ts +3 -0
  378. package/src/subagents/live/control.ts +1 -0
  379. package/src/subagents/live/manager.ts +1 -0
  380. package/src/subagents/live/realtime.ts +1 -0
  381. package/src/subagents/live/session-runtime.ts +1 -0
  382. package/src/subagents/manager.ts +1 -0
  383. package/src/subagents/spawn.ts +1 -0
  384. package/src/teams/discover-teams.ts +187 -0
  385. package/src/teams/team-config.ts +27 -0
  386. package/src/teams/team-serializer.ts +38 -0
  387. package/src/tools/safe-bash-extension.ts +54 -0
  388. package/src/tools/safe-bash.ts +505 -0
  389. package/src/types/diff.d.ts +18 -0
  390. package/src/types/new-api-types.ts +35 -0
  391. package/src/ui/agent-management-overlay.ts +160 -0
  392. package/src/ui/card-colors.ts +139 -0
  393. package/src/ui/crew-footer.ts +102 -0
  394. package/src/ui/crew-select-list.ts +112 -0
  395. package/src/ui/dashboard-panes/agents-pane.ts +164 -0
  396. package/src/ui/dashboard-panes/cancellation-pane.ts +66 -0
  397. package/src/ui/dashboard-panes/capability-pane.ts +77 -0
  398. package/src/ui/dashboard-panes/health-pane.ts +31 -0
  399. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -0
  400. package/src/ui/dashboard-panes/metrics-pane.ts +37 -0
  401. package/src/ui/dashboard-panes/progress-pane.ts +38 -0
  402. package/src/ui/dashboard-panes/transcript-pane.ts +10 -0
  403. package/src/ui/deploy-bundled-themes.ts +71 -0
  404. package/src/ui/dwf-phase-display.ts +151 -0
  405. package/src/ui/dynamic-border.ts +35 -0
  406. package/src/ui/format-helpers.ts +31 -0
  407. package/src/ui/heartbeat-aggregator.ts +74 -0
  408. package/src/ui/key-utils.ts +42 -0
  409. package/src/ui/keybinding-map.ts +248 -0
  410. package/src/ui/layout-primitives.ts +106 -0
  411. package/src/ui/live-conversation-overlay.ts +183 -0
  412. package/src/ui/live-duration.ts +55 -0
  413. package/src/ui/live-run-sidebar.ts +293 -0
  414. package/src/ui/loaders.ts +6 -0
  415. package/src/ui/mascot.ts +449 -0
  416. package/src/ui/overlays/agent-picker-overlay.ts +66 -0
  417. package/src/ui/overlays/confirm-overlay.ts +60 -0
  418. package/src/ui/overlays/help-overlay.ts +177 -0
  419. package/src/ui/overlays/mailbox-compose-overlay.ts +181 -0
  420. package/src/ui/overlays/mailbox-compose-preview.ts +78 -0
  421. package/src/ui/overlays/mailbox-detail-overlay.ts +155 -0
  422. package/src/ui/pi-ui-compat.ts +68 -0
  423. package/src/ui/powerbar-publisher.ts +459 -0
  424. package/src/ui/render-coalescer.ts +101 -0
  425. package/src/ui/render-diff.ts +160 -0
  426. package/src/ui/render-scheduler.ts +278 -0
  427. package/src/ui/run-action-dispatcher.ts +182 -0
  428. package/src/ui/run-dashboard.ts +910 -0
  429. package/src/ui/run-event-bus.ts +381 -0
  430. package/src/ui/run-snapshot-cache.ts +1057 -0
  431. package/src/ui/settings-overlay.ts +1067 -0
  432. package/src/ui/shared-overlay-scheduler.ts +96 -0
  433. package/src/ui/snapshot-types.ts +90 -0
  434. package/src/ui/spinner.ts +17 -0
  435. package/src/ui/status-colors.ts +143 -0
  436. package/src/ui/syntax-highlight.ts +136 -0
  437. package/src/ui/terminal-status.ts +269 -0
  438. package/src/ui/theme-adapter.ts +271 -0
  439. package/src/ui/theme-discovery.ts +199 -0
  440. package/src/ui/tool-progress-formatter.ts +93 -0
  441. package/src/ui/tool-renderers/brief-mode.ts +225 -0
  442. package/src/ui/tool-renderers/index.ts +726 -0
  443. package/src/ui/transcript-cache.ts +128 -0
  444. package/src/ui/transcript-entries.ts +256 -0
  445. package/src/ui/transcript-viewer.ts +424 -0
  446. package/src/ui/widget/index.ts +418 -0
  447. package/src/ui/widget/widget-formatters.ts +182 -0
  448. package/src/ui/widget/widget-model.ts +110 -0
  449. package/src/ui/widget/widget-renderer.ts +182 -0
  450. package/src/ui/widget/widget-types.ts +36 -0
  451. package/src/utils/bm25-search.ts +235 -0
  452. package/src/utils/completion-dedupe.ts +63 -0
  453. package/src/utils/conflict-detect.ts +668 -0
  454. package/src/utils/env-allowlist.ts +30 -0
  455. package/src/utils/env-filter.ts +155 -0
  456. package/src/utils/file-coalescer.ts +98 -0
  457. package/src/utils/fingerprint.ts +180 -0
  458. package/src/utils/frontmatter.ts +70 -0
  459. package/src/utils/fs-watch.ts +37 -0
  460. package/src/utils/gh-protocol.ts +556 -0
  461. package/src/utils/git.ts +260 -0
  462. package/src/utils/guards.ts +107 -0
  463. package/src/utils/ids.ts +24 -0
  464. package/src/utils/incremental-reader.ts +220 -0
  465. package/src/utils/internal-error.ts +10 -0
  466. package/src/utils/names.ts +36 -0
  467. package/src/utils/ndjson.ts +115 -0
  468. package/src/utils/paths.ts +227 -0
  469. package/src/utils/project-detector.ts +160 -0
  470. package/src/utils/redaction.ts +370 -0
  471. package/src/utils/resolve-shell.ts +36 -0
  472. package/src/utils/run-watcher-registry.ts +152 -0
  473. package/src/utils/safe-paths.ts +393 -0
  474. package/src/utils/scan-cache.ts +144 -0
  475. package/src/utils/session-utils.ts +110 -0
  476. package/src/utils/sleep.ts +54 -0
  477. package/src/utils/socket-path.ts +164 -0
  478. package/src/utils/sse-parser.ts +131 -0
  479. package/src/utils/task-name-generator.ts +337 -0
  480. package/src/utils/timings.ts +33 -0
  481. package/src/utils/token-counter.ts +200 -0
  482. package/src/utils/visual.ts +207 -0
  483. package/src/workflows/cost-estimator.ts +34 -0
  484. package/src/workflows/discover-workflows.ts +308 -0
  485. package/src/workflows/intermediate-store.ts +166 -0
  486. package/src/workflows/preflight-validator.ts +168 -0
  487. package/src/workflows/topology-analyzer.ts +203 -0
  488. package/src/workflows/validate-workflow.ts +50 -0
  489. package/src/workflows/workflow-config.ts +75 -0
  490. package/src/workflows/workflow-serializer.ts +32 -0
  491. package/src/worktree/branch-freshness.ts +112 -0
  492. package/src/worktree/cleanup.ts +341 -0
  493. package/src/worktree/worktree-manager.ts +1204 -0
@@ -0,0 +1,910 @@
1
+ import * as fs from "node:fs";
2
+ import type { MetricRegistry } from "../observability/metric-registry.ts";
3
+ import { readCrewAgents } from "../runtime/crew-agent-records.ts";
4
+ import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
5
+ import { getLiveAgentContextPercent } from "../runtime/live-agent-manager.ts";
6
+ import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
7
+ import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
8
+ import { aggregateUsage } from "../state/usage.ts";
9
+ import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
10
+ import { logInternalError } from "../utils/internal-error.ts";
11
+ import { resolveRealContainedPath } from "../utils/safe-paths.ts";
12
+ import { pad, sanitizeLine, truncate, visibleWidth } from "../utils/visual.ts";
13
+ import { renderAgentsPane } from "./dashboard-panes/agents-pane.ts";
14
+ import { summarizeTerminalReason } from "./dashboard-panes/cancellation-pane.ts";
15
+ import { renderHealthPane } from "./dashboard-panes/health-pane.ts";
16
+ import { renderMailboxPane } from "./dashboard-panes/mailbox-pane.ts";
17
+ import { renderMetricsPane } from "./dashboard-panes/metrics-pane.ts";
18
+ import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
19
+ import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
20
+ import { DynamicCrewBorder } from "./dynamic-border.ts";
21
+ import { dashboardActionForKey } from "./keybinding-map.ts";
22
+ import { HelpOverlay } from "./overlays/help-overlay.ts";
23
+ import type { OverlaySchedulerHandle } from "./shared-overlay-scheduler.ts";
24
+ import { registerOverlayScheduler } from "./shared-overlay-scheduler.ts";
25
+ import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
26
+ import { spinnerBucket, spinnerFrame } from "./spinner.ts";
27
+ import { applyStatusColor, colorizeStatusGlyphs, iconForStatus, type RunStatus } from "./status-colors.ts";
28
+ import type { CrewTheme } from "./theme-adapter.ts";
29
+ import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
30
+ import { renderLines } from "./widget/widget-renderer.ts";
31
+
32
+ /** S05 — wrap a pane render in try/catch so a single pane crash does not bring down the whole dashboard. */
33
+ function safeRenderPane(name: string, fn: () => string[]): string[] {
34
+ try {
35
+ return fn();
36
+ } catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ logInternalError("run-dashboard", new Error(`Dashboard pane '${name}' render failed: ${message}`));
39
+ return [`<error: ${name}>`];
40
+ }
41
+ }
42
+
43
+ interface DashboardComponent {
44
+ invalidate(): void;
45
+ render(width: number): string[];
46
+ handleInput(data: string): void;
47
+ }
48
+
49
+ export interface RunDashboardOptions {
50
+ placement?: "center" | "right";
51
+ showModel?: boolean;
52
+ showTokens?: boolean;
53
+ showTools?: boolean;
54
+ snapshotCache?: RunSnapshotCache;
55
+ runProvider?: () => TeamRunManifest[];
56
+ registry?: MetricRegistry;
57
+ /**
58
+ * Workspace/session ID for filtering runs and live agents. When provided,
59
+ * only runs with matching ownerSessionId and live agents with matching
60
+ * workspaceId are shown. This ensures session isolation in the UI.
61
+ */
62
+ workspaceId?: string;
63
+ /**
64
+ * Poke the host TUI to repaint after a state change. Must be wired from
65
+ * `commands.ts` (`() => requestRenderTarget(tui)`) so keypresses and event-bus
66
+ * updates immediately refresh the overlay instead of waiting on the next
67
+ * host tick. Without this the overlay can desync and base content (chat,
68
+ * status line) can paint through stale cells.
69
+ */
70
+ requestRender?: () => void;
71
+ }
72
+
73
+ /**
74
+ * Persisted per-process so that pressing `r` (reload) or closing+reopening the
75
+ * dashboard within the same Pi session keeps the user on the pane they were
76
+ * looking at. Resetting to "agents" on every `new RunDashboard(...)` was a
77
+ * UX regression.
78
+ */
79
+ let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = "agents";
80
+
81
+ export type RunDashboardAction =
82
+ | "status"
83
+ | "summary"
84
+ | "artifacts"
85
+ | "api"
86
+ | "events"
87
+ | "agents"
88
+ | "agent-events"
89
+ | "agent-output"
90
+ | "agent-transcript"
91
+ | "agent-live"
92
+ | "mailbox"
93
+ | "reload"
94
+ | "mailbox-detail"
95
+ | "health-recovery"
96
+ | "health-kill-stale"
97
+ | "health-diagnostic-export"
98
+ | "notifications-dismiss";
99
+ export interface RunDashboardSelection {
100
+ runId: string;
101
+ action: RunDashboardAction;
102
+ }
103
+
104
+ const TASK_READ_TTL_MS = 1000;
105
+
106
+ /** Max run rows rendered in the dashboard run-list block (L-1 window budget). */
107
+ const RUN_LIST_MAX = 8;
108
+
109
+ /**
110
+ * 1.10 (UI-P1-2) — short TTL for the buildSignature cache. `buildSignature`
111
+ * iterates every run and calls `snapshotFor → refreshIfStale`, which can
112
+ * stat multiple files per run when the TTL expires. Rendering fires several
113
+ * times per second (spinner + event-bus), so we cache the computed signature
114
+ * for 100ms: enough to coalesce one render burst without holding the cache
115
+ * across genuinely new data (>100ms idle means a fresh signature will be
116
+ * computed on the next render anyway).
117
+ */
118
+ const SIGNATURE_CACHE_TTL_MS = 100;
119
+
120
+ /**
121
+ * F-4 — a live run's snapshot is considered "possibly stale" once its
122
+ * `fetchedAt` is this old. A progressing run rebuilds on every stamp change,
123
+ * so an old `fetchedAt` means the manifest read has been repeatedly flaky
124
+ * (the cache silently returned the previous entry).
125
+ */
126
+ const STALE_SNAPSHOT_MS = 15_000;
127
+
128
+ /** Left-pad `value` to a fixed VISIBLE width (ANSI-aware). Used by V-1. */
129
+ function padVis(value: string, width: number): string {
130
+ const current = visibleWidth(value);
131
+ return current >= width ? value : `${" ".repeat(width - current)}${value}`;
132
+ }
133
+
134
+ /**
135
+ * L-1 — compute the run-list window (visible slots + which scroll indicators
136
+ * render) for a given offset. Shared by `ensureRunListWindow()` and the
137
+ * render loop so they ALWAYS agree and the selection can never land off-screen.
138
+ */
139
+ interface RunListWindow {
140
+ slots: number;
141
+ hasTop: boolean;
142
+ hasBottom: boolean;
143
+ }
144
+ function runListWindow(scrollOffset: number, count: number): RunListWindow {
145
+ const hasTop = scrollOffset > 0;
146
+ const availNoBottom = Math.max(1, RUN_LIST_MAX - (hasTop ? 1 : 0));
147
+ const hasBottom = scrollOffset + availNoBottom < count;
148
+ const slots = Math.max(1, RUN_LIST_MAX - (hasTop ? 1 : 0) - (hasBottom ? 1 : 0));
149
+ return { slots, hasTop, hasBottom };
150
+ }
151
+
152
+ function formatAge(iso: string | undefined): string | undefined {
153
+ if (!iso) return undefined;
154
+ const ms = Math.max(0, Date.now() - new Date(iso).getTime());
155
+ if (!Number.isFinite(ms)) return undefined;
156
+ if (ms < 1000) return "now";
157
+ if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
158
+ if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
159
+ return `${Math.floor(ms / 3_600_000)}h`;
160
+ }
161
+
162
+ function readProgressPreview(run: TeamRunManifest, maxLines = 5, snapshotCache?: RunSnapshotCache): string[] {
163
+ // P0-6: prefer the snapshot's `recentOutputLines` (no disk I/O) over reading the
164
+ // progress artifact on every render. The progress artifact content is captured
165
+ // into the snapshot's recent events / output pipeline upstream.
166
+ const snapshot = snapshotFor(run, snapshotCache);
167
+ if (snapshot?.recentOutputLines?.length) {
168
+ return ["Progress:", ...snapshot.recentOutputLines.slice(0, maxLines)];
169
+ }
170
+ if (snapshotCache) return ["Progress: (loading…)"];
171
+ // Legacy fallback: tests/dev paths without a snapshot cache still read the
172
+ // progress artifact directly so existing assertions keep working.
173
+ const progress = [...run.artifacts].reverse().find((artifact) => artifact.kind === "progress");
174
+ if (!progress) return ["Progress: (none)"];
175
+ try {
176
+ const progressPath = resolveRealContainedPath(run.artifactsRoot, progress.path);
177
+ if (!fs.existsSync(progressPath)) return ["Progress: (none)"];
178
+ return ["Progress:", ...fs.readFileSync(progressPath, "utf-8").split(/\r?\n/).filter(Boolean).slice(0, maxLines)];
179
+ } catch (error) {
180
+ const message = error instanceof Error ? error.message : String(error);
181
+ return [`Progress: failed to read (${message})`];
182
+ }
183
+ }
184
+
185
+ function formatTokens(usage: UsageState | undefined): string | undefined {
186
+ if (!usage) return undefined;
187
+ const total = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
188
+ if (!total) return undefined;
189
+ const compact = total >= 1000 ? `${(total / 1000).toFixed(total >= 10_000 ? 0 : 1)}k` : `${total}`;
190
+ const parts = [`tok=${compact}`];
191
+ if (usage.input) parts.push(`in=${usage.input}`);
192
+ if (usage.output) parts.push(`out=${usage.output}`);
193
+ if (usage.cacheRead) parts.push(`cache=${usage.cacheRead}`);
194
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
195
+ return parts.join("/");
196
+ }
197
+
198
+ function snapshotFor(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): RunUiSnapshot | undefined {
199
+ try {
200
+ return snapshotCache?.refreshIfStale(run.runId);
201
+ } catch {
202
+ return snapshotCache?.get(run.runId);
203
+ }
204
+ }
205
+
206
+ function readRunTasks(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): TeamTaskState[] {
207
+ const snapshot = snapshotFor(run, snapshotCache);
208
+ if (snapshot) return snapshot.tasks;
209
+ // P0-6: when a snapshot cache is provided but hasn't populated yet, return
210
+ // empty (the render loop falls back to the empty pane placeholder) instead
211
+ // of reading the tasks.json file synchronously every render tick.
212
+ if (snapshotCache) return [];
213
+ const parse = () => {
214
+ if (!fs.existsSync(run.tasksPath)) return [];
215
+ const parsed = JSON.parse(fs.readFileSync(run.tasksPath, "utf-8"));
216
+ return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
217
+ };
218
+ try {
219
+ return readJsonFileCoalesced(run.tasksPath, TASK_READ_TTL_MS, parse);
220
+ } catch {
221
+ return [];
222
+ }
223
+ }
224
+
225
+ function taskForAgent(tasks: TeamTaskState[], agent: CrewAgentRecord): TeamTaskState | undefined {
226
+ return tasks.find((task) => task.id === agent.taskId);
227
+ }
228
+
229
+ function modelForTask(task: TeamTaskState | undefined): string | undefined {
230
+ const attempts = task?.modelAttempts;
231
+ if (!attempts?.length) return undefined;
232
+ return attempts.find((attempt) => attempt.success)?.model ?? attempts.at(-1)?.model;
233
+ }
234
+
235
+ function modelForAgent(agent: CrewAgentRecord, task: TeamTaskState | undefined): string | undefined {
236
+ return modelForTask(task) ?? agent.model;
237
+ }
238
+
239
+ function usageForAgent(agent: CrewAgentRecord, task: TeamTaskState | undefined): UsageState | undefined {
240
+ return task?.usage ?? agent.usage;
241
+ }
242
+
243
+ function agentPreviewLine(agent: CrewAgentRecord, task: TeamTaskState | undefined, options: RunDashboardOptions): string {
244
+ const stats = [
245
+ agent.progress?.activityState,
246
+ options.showModel !== false && modelForAgent(agent, task) ? `model=${modelForAgent(agent, task)}` : undefined,
247
+ options.showTokens !== false
248
+ ? (formatTokens(usageForAgent(agent, task)) ??
249
+ (agent.progress?.tokens !== undefined ? `tok=${agent.progress.tokens}` : undefined))
250
+ : undefined,
251
+ options.showTools !== false && agent.progress?.currentTool ? `tool=${agent.progress.currentTool}` : undefined,
252
+ options.showTools !== false && agent.toolUses !== undefined ? `${agent.toolUses} tools` : undefined,
253
+ agent.progress?.turns !== undefined ? `${agent.progress.turns} turns` : undefined,
254
+ agent.progress?.failedTool ? `failedTool=${agent.progress.failedTool}` : undefined,
255
+ agent.startedAt ? `age=${formatAge(agent.completedAt ?? agent.startedAt)}` : undefined,
256
+ ].filter((part): part is string => Boolean(part));
257
+ const recent = agent.progress?.recentOutput?.at(-1);
258
+ const icon = iconForStatus(agent.status, {
259
+ runningGlyph: spinnerFrame(agent.taskId),
260
+ });
261
+ return sanitizeLine(
262
+ `Agent: ${icon} ${agent.taskId} ${agent.role}->${agent.agent}${stats.length ? ` · ${stats.join(" · ")}` : ""}${recent ? ` ⎿ ${recent}` : ""}`,
263
+ );
264
+ }
265
+
266
+ function readAgentPreview(run: TeamRunManifest, maxLines = 5, options: RunDashboardOptions = {}): string[] {
267
+ try {
268
+ const snapshot = snapshotFor(run, options.snapshotCache);
269
+ // P0-6: when a snapshot cache is provided but hasn't populated yet, return
270
+ // the empty-pane placeholder instead of calling `readCrewAgents` (disk I/O)
271
+ // on every render tick. Legacy callers (no cache) keep the disk-read path
272
+ // so existing unit tests continue to assert against concrete agent data.
273
+ const agents = snapshot?.agents ?? (options.snapshotCache ? [] : readCrewAgents(run));
274
+ const tasks = snapshot?.tasks ?? readRunTasks(run, options.snapshotCache);
275
+ if (!agents.length) return ["Agents: (none)"];
276
+ const totals = tasks.reduce(
277
+ (acc, task) => {
278
+ acc.input += task.usage?.input ?? 0;
279
+ acc.output += task.usage?.output ?? 0;
280
+ acc.cacheRead += task.usage?.cacheRead ?? 0;
281
+ acc.cacheWrite += task.usage?.cacheWrite ?? 0;
282
+ acc.cost += task.usage?.cost ?? 0;
283
+ return acc;
284
+ },
285
+ { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 } as {
286
+ input: number;
287
+ output: number;
288
+ cacheRead: number;
289
+ cacheWrite: number;
290
+ cost: number;
291
+ },
292
+ );
293
+ const header = formatTokens(totals) ? `Agents: ${formatTokens(totals)}` : "Agents:";
294
+ return [
295
+ header,
296
+ ...agents.slice(0, maxLines).map((agent) => agentPreviewLine(agent, taskForAgent(tasks, agent), options)),
297
+ ...(agents.length > maxLines ? [`Agents: +${agents.length - maxLines} more`] : []),
298
+ ];
299
+ } catch (error) {
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ return [`Agents: failed to read (${message})`];
302
+ }
303
+ }
304
+
305
+ function agentsFor(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): CrewAgentRecord[] {
306
+ const snapshot = snapshotFor(run, snapshotCache);
307
+ if (snapshot) return snapshot.agents;
308
+ // P0-6: when a snapshot cache is provided but hasn't populated yet, return
309
+ // empty (callers handle the empty-state placeholder) instead of calling
310
+ // `readCrewAgents` synchronously on every render tick.
311
+ if (snapshotCache) return [];
312
+ try {
313
+ return readCrewAgents(run);
314
+ } catch {
315
+ return [];
316
+ }
317
+ }
318
+
319
+ function runLabel(run: TeamRunManifest, selected: boolean, snapshotCache?: RunSnapshotCache, maxW?: number): string {
320
+ const agents = agentsFor(run, snapshotCache);
321
+ const stale = isLikelyOrphanedActiveRun(run, agents);
322
+ const running = agents.find((agent) => agent.status === "running");
323
+ const queued = agents.find((agent) => agent.status === "queued");
324
+ const step = stale
325
+ ? "orphaned queued run"
326
+ : running
327
+ ? `step ${running.taskId}`
328
+ : queued
329
+ ? `queued ${queued.taskId}`
330
+ : `agents ${agents.length}`;
331
+ const status: RunStatus = stale ? "stale" : (run.status as RunStatus);
332
+ const marker = selected ? "›" : " ";
333
+ const icon = iconForStatus(status, {
334
+ runningGlyph: spinnerFrame(run.runId),
335
+ });
336
+ // L-5: unified " · " separator (was "|").
337
+ // L-3: keep the GOAL (the human identifier) visible on narrow terminals —
338
+ // when the full line overflows, sacrifice the meta prefix (runId/status
339
+ // survive; team/workflow/step clip) rather than the goal. Optional `maxW`
340
+ // enables the goal-aware truncation; legacy 3-arg callers (dev patch
341
+ // scripts) get the untruncated full label.
342
+ const head = `${marker} ${icon} ${run.runId.slice(-8)} ${status}`;
343
+ const meta = `${run.team}/${run.workflow ?? "none"} · ${step}`;
344
+ const goal = sanitizeLine(run.goal ?? "");
345
+ if (maxW === undefined) return sanitizeLine(`${head} · ${meta} · ${goal}`);
346
+ const sepW = 3; // " · "
347
+ if (visibleWidth(head) + sepW + visibleWidth(meta) + sepW + visibleWidth(goal) <= maxW) {
348
+ return sanitizeLine(`${head} · ${meta} · ${goal}`);
349
+ }
350
+ // Not enough room: keep head + goal; clip the goal only if head+goal alone
351
+ // cannot fit, then shrink the prefix (meta clips first) to match.
352
+ const goalFits = visibleWidth(head) + sepW + visibleWidth(goal) <= maxW;
353
+ const goalRender = goalFits ? goal : truncate(goal, Math.max(8, maxW - visibleWidth(head) - sepW));
354
+ const prefixBudget = Math.max(0, maxW - visibleWidth(goalRender) - sepW);
355
+ const prefixRender = truncate(`${head} · ${meta}`, prefixBudget);
356
+ return sanitizeLine(`${prefixRender} · ${goalRender}`);
357
+ }
358
+
359
+ interface ResolvedRun {
360
+ manifest: TeamRunManifest;
361
+ snapshot: RunUiSnapshot | undefined;
362
+ agents: CrewAgentRecord[];
363
+ status: RunStatus;
364
+ }
365
+
366
+ function resolveRuns(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): Map<string, ResolvedRun> {
367
+ const map = new Map<string, ResolvedRun>();
368
+ for (const run of runs) {
369
+ const snapshot = snapshotFor(run, snapshotCache);
370
+ const agents = snapshot?.agents ?? agentsFor(run, snapshotCache);
371
+ const displayRun = snapshot?.manifest ?? run;
372
+ const status: RunStatus = isLikelyOrphanedActiveRun(displayRun, agents) ? "stale" : (displayRun.status as RunStatus);
373
+ map.set(run.runId, { manifest: run, snapshot, agents, status });
374
+ }
375
+ return map;
376
+ }
377
+
378
+ function groupedRuns(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): Array<{ label: string; run?: TeamRunManifest }> {
379
+ const resolved = resolveRuns(runs, snapshotCache);
380
+ const rows: Array<{ label: string; run?: TeamRunManifest }> = [];
381
+ const active = runs.filter((run) =>
382
+ isDisplayActiveRun(resolved.get(run.runId)?.snapshot?.manifest ?? run, resolved.get(run.runId)?.agents ?? []),
383
+ );
384
+ const rest = runs.filter(
385
+ (run) => !isDisplayActiveRun(resolved.get(run.runId)?.snapshot?.manifest ?? run, resolved.get(run.runId)?.agents ?? []),
386
+ );
387
+ if (active.length) rows.push({ label: "Active" }, ...active.map((run) => ({ label: run.runId, run })));
388
+ if (rest.length) rows.push({ label: "Recent" }, ...rest.map((run) => ({ label: run.runId, run })));
389
+ return rows;
390
+ }
391
+
392
+ function selectedRunFromGrouped(runs: TeamRunManifest[], selected: number, snapshotCache?: RunSnapshotCache): TeamRunManifest | undefined {
393
+ return groupedRuns(runs, snapshotCache).filter((row) => row.run)[selected]?.run;
394
+ }
395
+
396
+ function countByStatus(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): string {
397
+ const resolved = resolveRuns(runs, snapshotCache);
398
+ const counts = new Map<RunStatus, number>();
399
+ for (const r of resolved.values()) counts.set(r.status, (counts.get(r.status) ?? 0) + 1);
400
+ return [...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none";
401
+ }
402
+
403
+ export class RunDashboard implements DashboardComponent {
404
+ // TEMP DIAGNOSTIC (remove after verifying keybind fix on Pi 0.81.1)
405
+ private static _instanceCounter = 0;
406
+ private readonly _instanceId: number;
407
+ // END TEMP DIAGNOSTIC
408
+
409
+ private selected = 0;
410
+ private runScrollOffset = 0;
411
+ private showFullProgress = false;
412
+ private showHelp = false;
413
+ private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = lastActivePane;
414
+ private runs: TeamRunManifest[];
415
+ private readonly done: (selection: RunDashboardSelection | undefined) => void;
416
+ private readonly theme: CrewTheme;
417
+ private readonly options: RunDashboardOptions;
418
+ private cachedWidth = 0;
419
+ private cachedVersion = "";
420
+ private cachedLines: string[] = [];
421
+ /** 1.10 (UI-P1-2) — short-TTL cache for `buildSignature()` so per-run
422
+ * `snapshotFor → refreshIfStale` calls don't fire on every render tick. */
423
+ private cachedSignatureAt = 0;
424
+ private cachedSignature = "";
425
+ private readonly unsubscribeTheme: () => void;
426
+ private readonly schedulerHandle: OverlaySchedulerHandle | undefined;
427
+
428
+ constructor(
429
+ runs: TeamRunManifest[],
430
+ done: (selection: RunDashboardSelection | undefined) => void,
431
+ theme: unknown = {},
432
+ options: RunDashboardOptions = {},
433
+ ) {
434
+ // TEMP DIAGNOSTIC: log every constructor + handleInput + focus change
435
+ this._instanceId = ++RunDashboard._instanceCounter;
436
+ if (process.env.PI_CREW_BROKER_DIAG_UI === "1") {
437
+ try {
438
+ process.stderr.write(
439
+ `[PI-CREW-DIAG] RunDashboard#${this._instanceId}.constructor runs=${runs.length} workspaceId=${options.workspaceId ?? "n/a"}\n`,
440
+ );
441
+ } catch (error) {
442
+ logInternalError("run-dashboard.constructor-diag", error, undefined, "debug");
443
+ }
444
+ }
445
+ // END TEMP DIAGNOSTIC
446
+ // Filter runs by workspaceId for session isolation
447
+ // If workspaceId is provided, only show runs owned by that session or runs with no owner (legacy)
448
+ const filteredRuns = options.workspaceId
449
+ ? runs.filter((run) => !run.ownerSessionId || run.ownerSessionId === options.workspaceId)
450
+ : runs;
451
+ this.runs = filteredRuns;
452
+ this.done = done;
453
+ this.theme = asCrewTheme(theme);
454
+ this.options = options;
455
+ // 1.10 (UI-P1-1): route run:state / worker:lifecycle / ui:invalidate
456
+ // through a RenderScheduler (debounce + fallback) instead of registering
457
+ // 3 raw runEventBus subscriptions that each schedule an immediate
458
+ // invalidate+requestRender. With 3 overlays subscribing independently,
459
+ // a single event triggered up to 9 callbacks and ~150 invalidates/sec.
460
+ // The scheduler keeps the per-channel subscriptions but collapses them
461
+ // into one debounced render + one coalesced invalidate per runId.
462
+ //
463
+ // FIND-07: scheduler must be constructed BEFORE the theme subscription
464
+ // and the input-handler paths so they can route their invalidate+render
465
+ // calls through `this.scheduleRender()` (debounced + coalesced) instead
466
+ // of bypassing it with direct `invalidateAndRender()` calls. Each direct
467
+ // call would otherwise skip the scheduler's debounce window and force
468
+ // a synchronous repaint, recreating the burst-storm that this scheduler
469
+ // was introduced to eliminate.
470
+ const renderTick = (): void => {
471
+ this.invalidateAndRender();
472
+ };
473
+ this.schedulerHandle = registerOverlayScheduler(renderTick, () => {
474
+ // Drop any cached signature — data underneath may have
475
+ // changed so the next buildSignature() needs to recompute.
476
+ this.cachedSignature = "";
477
+ this.cachedSignatureAt = 0;
478
+ });
479
+ // FIND-07: route theme changes through the scheduler so they coalesce
480
+ // with run:state / worker:lifecycle / ui:invalidate bursts instead of
481
+ // each firing their own immediate invalidate+requestRender.
482
+ this.unsubscribeTheme = subscribeThemeChange(theme, () => this.scheduleRender());
483
+ }
484
+
485
+ /**
486
+ * FIND-07: route an external invalidation through the RenderScheduler.
487
+ *
488
+ * Use this for any caller-driven invalidation (theme change, input
489
+ * handler action) instead of calling `invalidateAndRender()` directly.
490
+ * The scheduler debounces + coalesces these so a theme change arriving
491
+ * in the middle of a run:state burst doesn't force a separate synchronous
492
+ * repaint. Falls back to a direct render if the scheduler was not created
493
+ * (defensive — currently always created in the constructor).
494
+ */
495
+ private scheduleRender(): void {
496
+ if (this.schedulerHandle) {
497
+ this.schedulerHandle.schedule();
498
+ return;
499
+ }
500
+ this.invalidateAndRender();
501
+ }
502
+
503
+ /**
504
+ * Invalidate the layout cache AND poke the host TUI to repaint. Without
505
+ * the explicit `requestRender` call the host only repaints on its own
506
+ * tick / on keypress, so async events (subagent completed, mailbox
507
+ * updates, theme change) would leave the overlay showing stale data
508
+ * until the user pressed a key — which is exactly when the "cascading
509
+ * dashboard" symptom surfaces because the diff renderer was comparing
510
+ * against a stale `previousLines` snapshot.
511
+ */
512
+ private invalidateAndRender(): void {
513
+ this.invalidate();
514
+ try {
515
+ this.options.requestRender?.();
516
+ } catch {
517
+ /* host may not expose requestRender */
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Stable overlay height. The host pi-tui positions overlays based on the
523
+ * number of lines `render()` returns; if that number fluctuates between
524
+ * frames (empty state → full pane → fewer agents) the anchor row shifts
525
+ * up/down and the differential renderer cannot reliably erase the
526
+ * previous footprint, producing the "ghost dashboard below" bug.
527
+ *
528
+ * Locking the output to a single height per render eliminates that.
529
+ */
530
+ private targetHeight(): number {
531
+ const rows = Number.isFinite(process.stdout?.rows) ? Number(process.stdout?.rows) : 30;
532
+ return Math.max(12, Math.min(36, rows - 2));
533
+ }
534
+
535
+ private refreshRuns(): void {
536
+ if (!this.options.runProvider) return;
537
+ const selectedRunId = this.selectedRunId();
538
+ const next = this.options.runProvider();
539
+ this.runs = Array.isArray(next) ? next : this.runs;
540
+ if (selectedRunId) {
541
+ const nextIndex = groupedRuns(this.runs, this.options.snapshotCache)
542
+ .filter((row) => row.run)
543
+ .findIndex((row) => row.run?.runId === selectedRunId);
544
+ if (nextIndex >= 0) this.selected = nextIndex;
545
+ else this.selected = 0;
546
+ }
547
+ }
548
+
549
+ /**
550
+ * L-1 — keep the run-list selection inside the rendered window so the `›`
551
+ * marker can never scroll off-screen (and Enter can never act on an
552
+ * invisible run). Uses the SAME `runListWindow()` the render loop uses and
553
+ * iterates to a fixed point, because the slot count depends on whether the
554
+ * ↑/↓ indicators render (which depends on the offset). This makes the
555
+ * window correct on the SAME render that processed the keypress — there is
556
+ * no one-frame lag where the marker is off-screen.
557
+ */
558
+ private ensureRunListWindow(selectableCount: number): void {
559
+ if (selectableCount <= RUN_LIST_MAX) {
560
+ this.runScrollOffset = 0;
561
+ return;
562
+ }
563
+ for (let i = 0; i < 4; i++) {
564
+ const { slots } = runListWindow(this.runScrollOffset, selectableCount);
565
+ const prev = this.runScrollOffset;
566
+ if (this.selected < this.runScrollOffset) this.runScrollOffset = this.selected;
567
+ else if (this.selected >= this.runScrollOffset + slots) this.runScrollOffset = this.selected - slots + 1;
568
+ this.runScrollOffset = Math.max(0, Math.min(this.runScrollOffset, Math.max(0, selectableCount - 1)));
569
+ if (this.runScrollOffset === prev) break;
570
+ }
571
+ }
572
+
573
+ private buildSignature(): string {
574
+ // 1.10 (UI-P1-2) — short-TTL cache so we don't re-read every run's
575
+ // snapshot on every render tick. `snapshotFor → refreshIfStale` can
576
+ // stat multiple files per run when its own TTL expires, and the
577
+ // dashboard's render is called several times per second (spinner +
578
+ // event-bus). Cache the full computed signature for ~100ms: long
579
+ // enough to coalesce one render burst, short enough that idle pauses
580
+ // still pick up new data promptly.
581
+ const now = Date.now();
582
+ if (this.cachedSignature && now - this.cachedSignatureAt < SIGNATURE_CACHE_TTL_MS) {
583
+ return this.cachedSignature;
584
+ }
585
+ let hasRunning = false;
586
+ const statuses = this.runs
587
+ .map((run) => {
588
+ const snapshot = snapshotFor(run, this.options.snapshotCache);
589
+ const displayRun = snapshot?.manifest ?? run;
590
+ const agents = snapshot?.agents ?? agentsFor(run, this.options.snapshotCache);
591
+ const stale = isLikelyOrphanedActiveRun(displayRun, agents);
592
+ const status: RunStatus = stale ? "stale" : (displayRun.status as RunStatus);
593
+ if (status === "running" || agents.some((agent) => agent.status === "running")) hasRunning = true;
594
+ return snapshot?.signature ?? `${displayRun.runId}:${displayRun.status}:${displayRun.updatedAt}:${status}`;
595
+ })
596
+ .join("|");
597
+ const metricsSig =
598
+ this.activePane === "metrics" ? `:metrics=${this.options.registry?.snapshot().length ?? 0}:${spinnerBucket()}` : "";
599
+ const sig = `${this.selected}:${this.showHelp ? 1 : 0}:${this.showFullProgress ? 1 : 0}:${this.activePane}:${statuses}${hasRunning ? `:spin=${spinnerBucket()}` : ""}${metricsSig}`;
600
+ this.cachedSignature = sig;
601
+ this.cachedSignatureAt = now;
602
+ return sig;
603
+ }
604
+
605
+ invalidate(): void {
606
+ this.cachedVersion = "";
607
+ this.cachedLines = [];
608
+ // 1.10 (UI-P1-2): also drop the short-TTL signature cache so the next
609
+ // render can't return a stale snapshot-derived signature that was
610
+ // computed before the invalidating event arrived.
611
+ this.cachedSignature = "";
612
+ this.cachedSignatureAt = 0;
613
+ }
614
+
615
+ dispose(): void {
616
+ this.unsubscribeTheme();
617
+ this.schedulerHandle?.dispose();
618
+ }
619
+
620
+ private selectedRunId(): string | undefined {
621
+ return selectedRunFromGrouped(this.runs, this.selected, this.options.snapshotCache)?.runId;
622
+ }
623
+
624
+ render(width: number): string[] {
625
+ try {
626
+ return this.renderUnsafe(width);
627
+ } catch (error) {
628
+ logInternalError("run-dashboard.render", error);
629
+ return renderLines(["Dashboard error — see logs for details.", "Press r to reload · Esc to close."], width);
630
+ }
631
+ }
632
+
633
+ private renderUnsafe(width: number): string[] {
634
+ this.refreshRuns();
635
+ const signature = this.buildSignature();
636
+ if (signature !== this.cachedVersion || this.cachedWidth !== width) {
637
+ const innerWidth = Math.max(20, width - 4);
638
+ const borderWidth = Math.min(innerWidth, Math.max(0, width - 2));
639
+ const fg = (color: Parameters<CrewTheme["fg"]>[0], text: string) => this.theme.fg(color, text);
640
+ const borderFill = (count: number) => new DynamicCrewBorder(this.theme).render(count)[0];
641
+ const border = (left: string, right: string) => `${fg("border", left)}${borderFill(borderWidth)}${fg("border", right)}`;
642
+ const row = (text: string) => `│ ${pad(truncate(text, innerWidth - 1), innerWidth - 1)}│`;
643
+ const sep = () => border("├", "┤");
644
+
645
+ const lines: string[] = [];
646
+ if (this.showHelp) {
647
+ // K-1: help overlay replaces the dashboard body until dismissed.
648
+ lines.push(...new HelpOverlay(this.theme).render(width));
649
+ } else {
650
+ lines.push(
651
+ border("╭", "╮"),
652
+ row(
653
+ `${fg("accent", "▐")} ${this.theme.bold("pi-crew")} · ${this.runs.length} runs ${fg("dim", "1-6 pane · ↑↓ · Enter · ? help · Esc")}`,
654
+ ),
655
+ sep(),
656
+ );
657
+
658
+ if (this.runs.length === 0) {
659
+ // F-7: actionable empty state instead of a bare "No runs.".
660
+ lines.push(row(fg("dim", "No runs yet.")));
661
+ lines.push(row(fg("dim", "Start one: team action='run' · r reload · Esc close")));
662
+ } else {
663
+ // L-1: windowed run list so the selection can never scroll off-screen.
664
+ const allGrouped = groupedRuns(this.runs, this.options.snapshotCache);
665
+ const selectable = allGrouped.filter((rowItem) => rowItem.run);
666
+ const selectableCount = selectable.length;
667
+ if (this.selected > selectableCount - 1) this.selected = Math.max(0, selectableCount - 1);
668
+ this.ensureRunListWindow(selectableCount);
669
+ if (selectableCount <= RUN_LIST_MAX) {
670
+ // Common case (≤8 runs): keep the Active/Recent group headers.
671
+ for (const rowItem of allGrouped) {
672
+ if (!rowItem.run) {
673
+ lines.push(row(fg("dim", `── ${rowItem.label} ──`)));
674
+ continue;
675
+ }
676
+ const idx = selectable.findIndex((c) => c.run?.runId === rowItem.run?.runId);
677
+ const snap = snapshotFor(rowItem.run, this.options.snapshotCache);
678
+ const run = snap?.manifest ?? rowItem.run;
679
+ const agents = snap?.agents ?? agentsFor(rowItem.run, this.options.snapshotCache);
680
+ const status: RunStatus = isLikelyOrphanedActiveRun(run, agents) ? "stale" : (run.status as RunStatus);
681
+ const label = runLabel(run, idx === this.selected, this.options.snapshotCache, innerWidth - 2);
682
+ lines.push(row(applyStatusColor(this.theme, status, label)));
683
+ }
684
+ } else {
685
+ // >8 runs: windowed list (group headers omitted to maximise run rows).
686
+ const win = runListWindow(this.runScrollOffset, selectableCount);
687
+ if (win.hasTop) lines.push(row(fg("dim", `↑ ${this.runScrollOffset} more above`)));
688
+ for (let gi = this.runScrollOffset; gi < Math.min(this.runScrollOffset + win.slots, selectableCount); gi++) {
689
+ const rowItem = selectable[gi];
690
+ if (!rowItem?.run) continue;
691
+ const snap = snapshotFor(rowItem.run, this.options.snapshotCache);
692
+ const run = snap?.manifest ?? rowItem.run;
693
+ const agents = snap?.agents ?? agentsFor(rowItem.run, this.options.snapshotCache);
694
+ const status: RunStatus = isLikelyOrphanedActiveRun(run, agents) ? "stale" : (run.status as RunStatus);
695
+ const label = runLabel(run, gi === this.selected, this.options.snapshotCache, innerWidth - 2);
696
+ lines.push(row(applyStatusColor(this.theme, status, label)));
697
+ }
698
+ if (win.hasBottom)
699
+ lines.push(row(fg("dim", `↓ ${selectableCount - (this.runScrollOffset + win.slots)} more below`)));
700
+ }
701
+
702
+ // Selected run detail — compact
703
+ const selectedRun = selectedRunFromGrouped(this.runs, this.selected, this.options.snapshotCache);
704
+ if (selectedRun) {
705
+ const snap = snapshotFor(selectedRun, this.options.snapshotCache);
706
+ const r = snap?.manifest ?? selectedRun;
707
+ const agents = snap?.agents ?? agentsFor(selectedRun, this.options.snapshotCache);
708
+ const statusStr: RunStatus = isLikelyOrphanedActiveRun(r, agents) ? "stale" : (r.status as RunStatus);
709
+ const selectedTasks = snap?.tasks ?? readRunTasks(r, this.options.snapshotCache);
710
+ lines.push(sep());
711
+ lines.push(row(`${fg("accent", "▸")} ${truncate(sanitizeLine(r.goal), innerWidth - 6)}`));
712
+ // L-2: surface the failure/cancellation reason inline for terminal runs.
713
+ const isTerminal = statusStr === "failed" || statusStr === "cancelled" || statusStr === "stopped";
714
+ const reason = isTerminal ? summarizeTerminalReason(r, selectedTasks, snap?.cancellationReason) : undefined;
715
+ const reasonSuffix = reason ? ` · ${truncate(sanitizeLine(reason), 40)}` : "";
716
+ lines.push(
717
+ row(
718
+ fg(
719
+ "dim",
720
+ sanitizeLine(
721
+ ` ${r.team}/${r.workflow ?? "default"} · ${statusStr} · ${r.runId.slice(-10)}${reasonSuffix}`,
722
+ ),
723
+ ),
724
+ ),
725
+ );
726
+
727
+ // Pane content (max 8 lines) — F-2: colorize embedded status glyphs.
728
+ const paneLines = snap
729
+ ? this.activePane === "agents"
730
+ ? safeRenderPane("agents", () => renderAgentsPane(snap, this.options))
731
+ : this.activePane === "progress"
732
+ ? safeRenderPane("progress", () => renderProgressPane(snap))
733
+ : this.activePane === "mailbox"
734
+ ? safeRenderPane("mailbox", () => renderMailboxPane(snap))
735
+ : this.activePane === "health"
736
+ ? safeRenderPane("health", () =>
737
+ renderHealthPane(snap, {
738
+ isForeground: !r.async,
739
+ }),
740
+ )
741
+ : this.activePane === "metrics"
742
+ ? safeRenderPane("metrics", () =>
743
+ renderMetricsPane(snap, {
744
+ registry: this.options.registry,
745
+ }),
746
+ )
747
+ : safeRenderPane("transcript", () => renderTranscriptPane(snap))
748
+ : [...readAgentPreview(r, 4, this.options), ...readProgressPreview(r, 2, this.options.snapshotCache)];
749
+ const filteredPane = paneLines.filter((l) => l && !l.includes("(none)") && l.trim() !== "");
750
+ if (filteredPane.length > 0) {
751
+ lines.push(row(fg("dim", `── ${this.activePane} ──`)));
752
+ for (const line of filteredPane.slice(0, 8)) {
753
+ lines.push(colorizeStatusGlyphs(row(truncate(sanitizeLine(line), innerWidth - 2)), this.theme));
754
+ }
755
+ }
756
+
757
+ // F-4: warn when a live run's snapshot is likely stale (flaky read).
758
+ const isActiveRun = statusStr === "running" || statusStr === "queued" || statusStr === "waiting";
759
+ const staleData = isActiveRun && (snap ? Date.now() - snap.fetchedAt > STALE_SNAPSHOT_MS : true);
760
+ if (staleData) lines.push(row(fg("warning", "⚠ data may be stale (manifest read flaky)")));
761
+
762
+ // One-line footer — V-1: width-padded so the right edge doesn't jitter.
763
+ const usage = aggregateUsage(selectedTasks);
764
+ const u = usage ?? {
765
+ input: 0,
766
+ output: 0,
767
+ cacheRead: 0,
768
+ cacheWrite: 0,
769
+ cost: 0,
770
+ };
771
+ const tok = (u.input ?? 0) + (u.output ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0);
772
+ const tokStr = tok > 0 ? (tok >= 1000 ? `${(tok / 1000).toFixed(1)}k tok` : `${tok} tok`) : "";
773
+ let ctxPct: number | undefined;
774
+ for (const agent of agents) {
775
+ if (agent.status === "running" && agent.runtime === "live-session") {
776
+ const pct = getLiveAgentContextPercent(agent.taskId);
777
+ if (pct != null) {
778
+ ctxPct = pct;
779
+ break;
780
+ }
781
+ }
782
+ }
783
+ const ctxStr = ctxPct != null ? `${Math.round(ctxPct)}% ctx` : "";
784
+ const footerFields: string[] = [];
785
+ if (tokStr) footerFields.push(padVis(tokStr, 10));
786
+ if (ctxStr) footerFields.push(padVis(ctxStr, 9));
787
+ if (footerFields.length) lines.push(row(fg("dim", footerFields.join(" · "))));
788
+ }
789
+ }
790
+ lines.push(border("╰", "╯"));
791
+ }
792
+
793
+ const target = this.targetHeight();
794
+ if (lines.length < target) {
795
+ const innerWidth = Math.max(20, width - 4);
796
+ const fg = (color: Parameters<CrewTheme["fg"]>[0], text: string) => this.theme.fg(color, text);
797
+ const blankRow = `│ ${pad("", innerWidth - 1)}│`;
798
+ const bottom = lines.pop();
799
+ while (lines.length < target - 1) lines.push(fg("border", blankRow));
800
+ if (bottom) lines.push(bottom);
801
+ } else if (lines.length > target) {
802
+ const bottom = lines[lines.length - 1];
803
+ lines.length = target - 1;
804
+ lines.push(bottom);
805
+ }
806
+
807
+ this.cachedLines = renderLines(
808
+ lines.map((line) => truncate(line, width)),
809
+ width,
810
+ );
811
+ this.cachedVersion = signature;
812
+ this.cachedWidth = width;
813
+ }
814
+ return this.cachedLines;
815
+ }
816
+
817
+ // Pi 0.81+ requires the Focusable contract: a string-indexable `focused`
818
+ // marker that TUI toggles to track which component currently receives
819
+ // input. Without it, isFocusable() returns false and downstream
820
+ // dispatch may skip the component. Declared as an own property so
821
+ // `"focused" in component` is true.
822
+ public focused = false;
823
+
824
+ handleInput(data: string): void {
825
+ // TEMP DIAGNOSTIC (remove after verifying keybind fix on Pi 0.81.1)
826
+ if (process.env.PI_CREW_BROKER_DIAG_UI === "1") {
827
+ try {
828
+ process.stderr.write(
829
+ `[PI-CREW-DIAG] RunDashboard#${this._instanceId}.handleInput data=${JSON.stringify(data)} focused=${this.focused}\n`,
830
+ );
831
+ } catch (error) {
832
+ logInternalError("run-dashboard.handleInput-diag", error, undefined, "debug");
833
+ }
834
+ }
835
+ const action = dashboardActionForKey(data, this.activePane);
836
+ // K-1: "?" toggles the help overlay; while it is shown, any other key
837
+ // (including Esc) just dismisses it first instead of acting.
838
+ if (action === "help") {
839
+ this.showHelp = !this.showHelp;
840
+ this.scheduleRender();
841
+ return;
842
+ }
843
+ if (this.showHelp) {
844
+ this.showHelp = false;
845
+ this.scheduleRender();
846
+ return;
847
+ }
848
+ const selectedRunId = this.selectedRunId();
849
+ if (action === "close") {
850
+ this.done(undefined);
851
+ return;
852
+ }
853
+ if (action === "select") {
854
+ this.done(selectedRunId ? { runId: selectedRunId, action: "status" } : undefined);
855
+ return;
856
+ }
857
+ if (
858
+ action === "summary" ||
859
+ action === "artifacts" ||
860
+ action === "api" ||
861
+ action === "agents" ||
862
+ action === "mailbox" ||
863
+ action === "reload" ||
864
+ action === "mailbox-detail" ||
865
+ action === "health-recovery" ||
866
+ action === "health-kill-stale" ||
867
+ action === "health-diagnostic-export" ||
868
+ action === "notifications-dismiss"
869
+ ) {
870
+ this.done(selectedRunId ? { runId: selectedRunId, action } : action === "reload" ? { runId: "", action } : undefined);
871
+ return;
872
+ }
873
+ if (action === "events") {
874
+ this.done(selectedRunId ? { runId: selectedRunId, action: "agent-events" } : undefined);
875
+ return;
876
+ }
877
+ if (action === "output") {
878
+ this.done(selectedRunId ? { runId: selectedRunId, action: "agent-output" } : undefined);
879
+ return;
880
+ }
881
+ if (action === "transcript") {
882
+ this.done(selectedRunId ? { runId: selectedRunId, action: "agent-transcript" } : undefined);
883
+ return;
884
+ }
885
+ if (action === "live-conversation") {
886
+ this.done(selectedRunId ? { runId: selectedRunId, action: "agent-live" } : undefined);
887
+ return;
888
+ }
889
+ if (action === "progressToggle") {
890
+ this.showFullProgress = !this.showFullProgress;
891
+ this.invalidate();
892
+ return;
893
+ }
894
+ if (action === "pane-agents") this.activePane = "agents";
895
+ else if (action === "pane-progress") this.activePane = "progress";
896
+ else if (action === "pane-mailbox") this.activePane = "mailbox";
897
+ else if (action === "pane-output") this.activePane = "output";
898
+ else if (action === "pane-health") this.activePane = "health";
899
+ else if (action === "pane-metrics") this.activePane = "metrics";
900
+ else if (action === "up") this.selected = Math.max(0, this.selected - 1);
901
+ else if (action === "down") {
902
+ const selectableCount = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).length;
903
+ this.selected = Math.min(Math.max(0, selectableCount - 1), this.selected + 1);
904
+ }
905
+ if (action) {
906
+ lastActivePane = this.activePane;
907
+ this.scheduleRender();
908
+ }
909
+ }
910
+ }