pi-crew 0.9.55 → 0.9.57

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 (309) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +53 -5
  4. package/dist/index.mjs +5666 -4370
  5. package/docs/README.md +54 -0
  6. package/package.json +12 -13
  7. package/skills/real-test-pi-crew/SKILL.md +93 -3
  8. package/src/agents/discover-agents.ts +68 -10
  9. package/src/benchmark/feedback-loop.ts +1 -1
  10. package/src/config/config.ts +2 -1
  11. package/src/config/types.ts +12 -0
  12. package/src/extension/action-suggestions.ts +16 -63
  13. package/src/extension/async-notifier.ts +3 -3
  14. package/src/extension/command-completions.ts +2 -2
  15. package/src/extension/crew-cleanup.ts +3 -3
  16. package/src/extension/crew-vibes/footer.ts +4 -2
  17. package/src/extension/cross-extension-rpc.ts +10 -5
  18. package/src/extension/knowledge-injection.ts +57 -10
  19. package/src/extension/register.ts +2 -2
  20. package/src/extension/registration/artifact-cleanup.ts +41 -3
  21. package/src/extension/registration/commands.ts +4 -4
  22. package/src/extension/registration/context-builder.ts +10 -10
  23. package/src/extension/registration/crash-recovery-cache.ts +3 -3
  24. package/src/extension/registration/foreground-run-controller.ts +1 -1
  25. package/src/extension/registration/lazy-configurers.ts +2 -2
  26. package/src/extension/registration/lifecycle-handlers.ts +12 -12
  27. package/src/extension/registration/lifecycle.ts +2 -2
  28. package/src/extension/registration/observability.ts +2 -2
  29. package/src/extension/registration/registration-types.ts +3 -3
  30. package/src/extension/registration/runtime-cleanup.ts +1 -1
  31. package/src/extension/registration/subagent-helpers.ts +2 -2
  32. package/src/extension/registration/subagent-manager-setup.ts +2 -2
  33. package/src/extension/registration/subagent-tools.ts +21 -16
  34. package/src/extension/registration/team-tool.ts +109 -17
  35. package/src/extension/registration/viewers.ts +3 -4
  36. package/src/extension/registration/wire-cross-extension.ts +7 -7
  37. package/src/extension/rpc-hmac.ts +3 -2
  38. package/src/extension/run-bundle-schema.ts +13 -4
  39. package/src/extension/run-export.ts +2 -2
  40. package/src/extension/run-index.ts +2 -3
  41. package/src/extension/run-maintenance.ts +57 -1
  42. package/src/extension/team-manager-command.ts +3 -1
  43. package/src/extension/team-tool/anchor.ts +8 -2
  44. package/src/extension/team-tool/api.ts +117 -39
  45. package/src/extension/team-tool/cancel.ts +23 -8
  46. package/src/extension/team-tool/chain-executor.ts +1 -1
  47. package/src/extension/team-tool/dispatch/automate.ts +17 -2
  48. package/src/extension/team-tool/dispatch/control.ts +17 -2
  49. package/src/extension/team-tool/dispatch/manage.ts +34 -2
  50. package/src/extension/team-tool/dispatch/run.ts +17 -2
  51. package/src/extension/team-tool/dispatch/status.ts +37 -5
  52. package/src/extension/team-tool/doctor.ts +2 -2
  53. package/src/extension/team-tool/explain.ts +1 -1
  54. package/src/extension/team-tool/goal-wrap.ts +5 -5
  55. package/src/extension/team-tool/goal.ts +30 -9
  56. package/src/extension/team-tool/handle-schedule.ts +37 -19
  57. package/src/extension/team-tool/inspect.ts +21 -5
  58. package/src/extension/team-tool/lifecycle-actions.ts +53 -10
  59. package/src/extension/team-tool/parallel-dispatch.ts +14 -5
  60. package/src/extension/team-tool/param-error.ts +138 -0
  61. package/src/extension/team-tool/respond.ts +20 -6
  62. package/src/extension/team-tool/run.ts +27 -17
  63. package/src/extension/team-tool/status.ts +17 -6
  64. package/src/extension/team-tool.ts +26 -19
  65. package/src/hooks/registry.ts +1 -1
  66. package/src/i18n.ts +22 -0
  67. package/src/observability/event-bus.ts +1 -1
  68. package/src/prompt/prompt-runtime.ts +2 -2
  69. package/src/runtime/README.md +35 -0
  70. package/src/runtime/agent-control.ts +1 -1
  71. package/src/runtime/anchor-manager.ts +0 -2
  72. package/src/runtime/async-runner.ts +1 -1
  73. package/src/runtime/attention-events.ts +3 -2
  74. package/src/runtime/auto-summarize.ts +0 -2
  75. package/src/runtime/background-runner.ts +103 -30
  76. package/src/runtime/{crew-broker-child.ts → broker/crew-broker-child.ts} +2 -2
  77. package/src/runtime/{crew-broker-client.ts → broker/crew-broker-client.ts} +4 -7
  78. package/src/runtime/{crew-broker.ts → broker/crew-broker.ts} +13 -21
  79. package/src/runtime/chain-runner.ts +50 -5
  80. package/src/runtime/{child-pi-constants.ts → child-pi/child-pi-constants.ts} +1 -1
  81. package/src/runtime/{child-pi-kill.ts → child-pi/child-pi-kill.ts} +3 -3
  82. package/src/runtime/{child-pi-spawn.ts → child-pi/child-pi-spawn.ts} +28 -12
  83. package/src/runtime/{child-pi-steering.ts → child-pi/child-pi-steering.ts} +1 -1
  84. package/src/runtime/{child-pi-streams.ts → child-pi/child-pi-streams.ts} +23 -4
  85. package/src/runtime/{child-pi-transcript.ts → child-pi/child-pi-transcript.ts} +6 -6
  86. package/src/runtime/{child-pi.ts → child-pi/child-pi.ts} +47 -78
  87. package/src/runtime/compaction/compact-stages/bounded-tail.ts +84 -0
  88. package/src/runtime/{compact-stages → compaction/compact-stages}/index.ts +2 -2
  89. package/src/runtime/{compaction-summary.ts → compaction/compaction-summary.ts} +3 -3
  90. package/src/runtime/{tool-output-pruner.ts → compaction/tool-output-pruner.ts} +1 -1
  91. package/src/runtime/crew-agent-records.ts +48 -2
  92. package/src/runtime/custom-tools/irc-tool.ts +2 -2
  93. package/src/runtime/delivery-coordinator.ts +0 -3
  94. package/src/runtime/diagnostic-export.ts +5 -4
  95. package/src/runtime/foreground-control.ts +1 -1
  96. package/src/runtime/foreground-watchdog.ts +1 -1
  97. package/src/runtime/{adaptive-plan.ts → goal-workflow/adaptive-plan.ts} +8 -8
  98. package/src/runtime/{dynamic-workflow-context.ts → goal-workflow/dynamic-workflow-context.ts} +16 -16
  99. package/src/runtime/{dynamic-workflow-runner.ts → goal-workflow/dynamic-workflow-runner.ts} +10 -10
  100. package/src/runtime/{goal-achievement.ts → goal-workflow/goal-achievement.ts} +3 -3
  101. package/src/runtime/{goal-evaluator.ts → goal-workflow/goal-evaluator.ts} +8 -8
  102. package/src/runtime/{goal-loop-runner.ts → goal-workflow/goal-loop-runner.ts} +27 -25
  103. package/src/runtime/{goal-state-store.ts → goal-workflow/goal-state-store.ts} +8 -8
  104. package/src/runtime/{plan-templates.ts → goal-workflow/plan-templates.ts} +1 -1
  105. package/src/runtime/group-join.ts +3 -3
  106. package/src/runtime/handoff-manager.ts +0 -2
  107. package/src/runtime/{heartbeat-watcher.ts → heartbeat/heartbeat-watcher.ts} +7 -7
  108. package/src/runtime/iteration-hooks.ts +1 -1
  109. package/src/runtime/{live-agent-control.ts → live-session/live-agent-control.ts} +2 -2
  110. package/src/runtime/{live-agent-manager.ts → live-session/live-agent-manager.ts} +4 -4
  111. package/src/runtime/{live-session-runtime.ts → live-session/live-session-runtime.ts} +39 -41
  112. package/src/runtime/manifest-cache.ts +6 -2
  113. package/src/runtime/{model-fallback.ts → model/model-fallback.ts} +1 -1
  114. package/src/runtime/{pi-args.ts → model/pi-args.ts} +20 -5
  115. package/src/runtime/model/provider-extensions.ts +116 -0
  116. package/src/runtime/{runtime-policy.ts → model/runtime-policy.ts} +2 -2
  117. package/src/runtime/{runtime-resolver.ts → model/runtime-resolver.ts} +3 -3
  118. package/src/runtime/{runtime-warmup.ts → model/runtime-warmup.ts} +8 -8
  119. package/src/runtime/orphan-worker-registry.ts +1 -1
  120. package/src/runtime/{progress-tracker.ts → output/progress-tracker.ts} +1 -1
  121. package/src/runtime/{result-extractor.ts → output/result-extractor.ts} +4 -4
  122. package/src/runtime/{sidechain-output.ts → output/sidechain-output.ts} +2 -2
  123. package/src/runtime/{stream-preview.ts → output/stream-preview.ts} +1 -1
  124. package/src/runtime/{streaming-output.ts → output/streaming-output.ts} +2 -2
  125. package/src/runtime/{tool-progress.ts → output/tool-progress.ts} +1 -1
  126. package/src/runtime/parent-guard.ts +30 -10
  127. package/src/runtime/pipeline-runner.ts +2 -2
  128. package/src/runtime/policy-engine.ts +2 -2
  129. package/src/runtime/{cancellation.ts → process/cancellation.ts} +1 -1
  130. package/src/runtime/{post-exit-stdio-guard.ts → process/post-exit-stdio-guard.ts} +1 -1
  131. package/src/runtime/{process-lifecycle.ts → process/process-lifecycle.ts} +3 -3
  132. package/src/runtime/{checkpoint.ts → recovery/checkpoint.ts} +4 -4
  133. package/src/runtime/{crash-recovery.ts → recovery/crash-recovery.ts} +93 -21
  134. package/src/runtime/{overflow-recovery.ts → recovery/overflow-recovery.ts} +1 -1
  135. package/src/runtime/{recovery-recipes.ts → recovery/recovery-recipes.ts} +1 -1
  136. package/src/runtime/{retry-executor.ts → recovery/retry-executor.ts} +2 -2
  137. package/src/runtime/{retry-runner.ts → recovery/retry-runner.ts} +2 -8
  138. package/src/runtime/run-tracker.ts +1 -1
  139. package/src/runtime/run-worker.ts +3 -3
  140. package/src/runtime/{coalesce-tasks.ts → scheduling/coalesce-tasks.ts} +3 -3
  141. package/src/runtime/{concurrency.ts → scheduling/concurrency.ts} +9 -3
  142. package/src/runtime/{parallel-research.ts → scheduling/parallel-research.ts} +4 -2
  143. package/src/runtime/{run-coalesced-task-group.ts → scheduling/run-coalesced-task-group.ts} +95 -36
  144. package/src/runtime/{semaphore.ts → scheduling/semaphore.ts} +4 -1
  145. package/src/runtime/{task-graph-scheduler.ts → scheduling/task-graph-scheduler.ts} +1 -1
  146. package/src/runtime/session-resources.ts +1 -1
  147. package/src/runtime/settings-store.ts +1 -1
  148. package/src/runtime/single-agent-compose.ts +1 -0
  149. package/src/runtime/stale-reconciler.ts +1 -1
  150. package/src/runtime/subagent-manager.ts +1 -1
  151. package/src/runtime/supervisor-contact.ts +28 -13
  152. package/src/runtime/task-output-context.ts +7 -5
  153. package/src/runtime/task-runner/child-executor.ts +67 -31
  154. package/src/runtime/task-runner/live-executor.ts +8 -8
  155. package/src/runtime/task-runner/post-execution.ts +12 -12
  156. package/src/runtime/task-runner/pre-execution.ts +6 -6
  157. package/src/runtime/task-runner/progress.ts +1 -1
  158. package/src/runtime/task-runner/run-projection.ts +1 -1
  159. package/src/runtime/task-runner/scaffold-executor.ts +1 -1
  160. package/src/runtime/task-runner/state-helpers.ts +2 -2
  161. package/src/runtime/task-runner.ts +4 -4
  162. package/src/runtime/team-runner.ts +441 -175
  163. package/src/runtime/{completion-guard.ts → verification/completion-guard.ts} +1 -1
  164. package/src/runtime/{green-contract.ts → verification/green-contract.ts} +1 -1
  165. package/src/runtime/{post-checks.ts → verification/post-checks.ts} +4 -4
  166. package/src/runtime/{verification-gates.ts → verification/verification-gates.ts} +5 -5
  167. package/src/runtime/{verification-integrity.ts → verification/verification-integrity.ts} +1 -1
  168. package/src/schema/config-schema.ts +1 -0
  169. package/src/schema/team-tool-schema.ts +149 -185
  170. package/src/state/README.md +44 -0
  171. package/src/state/atomic-write.ts +24 -3
  172. package/src/state/{locks.ts → coordination/locks.ts} +118 -38
  173. package/src/state/{mailbox.ts → coordination/mailbox.ts} +14 -8
  174. package/src/state/{schedule.ts → coordination/schedule.ts} +3 -3
  175. package/src/state/{task-claims.ts → coordination/task-claims.ts} +1 -1
  176. package/src/state/decision-ledger.ts +1 -1
  177. package/src/state/{event-log-rotation.ts → event-log/event-log-rotation.ts} +189 -31
  178. package/src/state/{event-log.ts → event-log/event-log.ts} +77 -39
  179. package/src/state/{jsonl-writer.ts → event-log/jsonl-writer.ts} +14 -6
  180. package/src/state/hook-instinct-bridge.ts +2 -2
  181. package/src/state/{active-run-registry.ts → stores/active-run-registry.ts} +7 -7
  182. package/src/state/{artifact-store.ts → stores/artifact-store.ts} +62 -4
  183. package/src/state/{blob-store.ts → stores/blob-store.ts} +3 -3
  184. package/src/state/{health-store.ts → stores/health-store.ts} +3 -3
  185. package/src/state/{instinct-store.ts → stores/instinct-store.ts} +3 -3
  186. package/src/state/{observation-store.ts → stores/observation-store.ts} +2 -2
  187. package/src/state/{run-cache.ts → stores/run-cache.ts} +4 -4
  188. package/src/state/{run-graph.ts → stores/run-graph.ts} +4 -4
  189. package/src/state/{run-metrics.ts → stores/run-metrics.ts} +3 -3
  190. package/src/state/{state-store.ts → stores/state-store.ts} +254 -21
  191. package/src/state/types.ts +27 -3
  192. package/src/ui/component.ts +55 -0
  193. package/src/ui/crew-select-list.ts +3 -1
  194. package/src/ui/dashboard-panes/agents-pane.ts +1 -1
  195. package/src/ui/dwf-phase-display.ts +1 -1
  196. package/src/ui/heartbeat-aggregator.ts +1 -1
  197. package/src/ui/keybinding-map.ts +179 -6
  198. package/src/ui/layout-primitives.ts +9 -5
  199. package/src/ui/live-conversation-overlay.ts +1 -1
  200. package/src/ui/live-run-sidebar.ts +1 -1
  201. package/src/ui/overlay-stack.ts +148 -0
  202. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  203. package/src/ui/overlays/mailbox-compose-overlay.ts +1 -1
  204. package/src/ui/overlays/mailbox-detail-overlay.ts +2 -2
  205. package/src/ui/powerbar-publisher.ts +1 -1
  206. package/src/ui/render-scheduler.ts +1 -1
  207. package/src/ui/run-action-dispatcher.ts +3 -3
  208. package/src/ui/run-dashboard.ts +5 -33
  209. package/src/ui/run-event-bus.ts +3 -3
  210. package/src/ui/run-snapshot-cache.ts +37 -18
  211. package/src/ui/settings-overlay.ts +13 -21
  212. package/src/ui/snapshot-types.ts +1 -1
  213. package/src/ui/status-colors.ts +0 -2
  214. package/src/ui/terminal-status.ts +1 -1
  215. package/src/ui/theme-adapter.ts +33 -13
  216. package/src/ui/tool-renderers/index.ts +8 -0
  217. package/src/ui/transcript-viewer.ts +37 -13
  218. package/src/ui/widget/index.ts +32 -9
  219. package/src/ui/widget/widget-formatters.ts +62 -4
  220. package/src/ui/widget/widget-model.ts +2 -2
  221. package/src/ui/widget/widget-renderer.ts +1 -1
  222. package/src/utils/conflict-detect.ts +58 -5
  223. package/src/utils/ndjson.ts +2 -2
  224. package/src/utils/redaction.ts +39 -0
  225. package/src/utils/socket-path.ts +1 -1
  226. package/src/workflows/workflow-config.ts +1 -1
  227. package/src/worktree/cleanup.ts +1 -1
  228. package/src/worktree/worktree-manager.ts +111 -47
  229. package/workflows/chain.workflow.md +2 -2
  230. package/dist/build-meta.json +0 -16807
  231. package/index.bundle.ts +0 -25
  232. package/scripts/bench-check.mjs +0 -96
  233. package/scripts/bench-cold-start.mjs +0 -216
  234. package/scripts/build-bundle.mjs +0 -92
  235. package/scripts/check-all-skills.ts +0 -294
  236. package/scripts/check-bundle-size.mjs +0 -38
  237. package/scripts/check-bundle-staleness.mjs +0 -97
  238. package/scripts/check-conflict-markers.mjs +0 -91
  239. package/scripts/check-lazy-imports.mjs +0 -28
  240. package/scripts/dev-runner.mjs +0 -106
  241. package/scripts/profile-startup.mjs +0 -125
  242. package/scripts/release-smoke.mjs +0 -74
  243. package/scripts/run-bench.mjs +0 -47
  244. package/scripts/run-real-chain.ts +0 -41
  245. package/scripts/test-issue-29-crash.ts +0 -111
  246. package/scripts/test-issue-29-e2e.ts +0 -183
  247. package/scripts/test-issue-29-real-runtime.ts +0 -330
  248. package/scripts/test-issue-29-real-tasks.ts +0 -387
  249. package/scripts/test-issue-29-team-tool.ts +0 -105
  250. package/scripts/test-runner.mjs +0 -84
  251. package/scripts/verify-flicker-fix.ts +0 -109
  252. package/scripts/verify-skill.ts +0 -550
  253. package/scripts/watch-bundle.mjs +0 -259
  254. package/scripts/watchdog-harness.ts +0 -119
  255. package/src/adapters/claude-adapter.ts +0 -23
  256. package/src/adapters/codex-adapter.ts +0 -21
  257. package/src/adapters/cursor-adapter.ts +0 -17
  258. package/src/adapters/export-util.ts +0 -143
  259. package/src/adapters/index.ts +0 -15
  260. package/src/adapters/registry.ts +0 -18
  261. package/src/adapters/types.ts +0 -23
  262. package/src/runtime/code-summary.ts +0 -292
  263. package/src/runtime/cross-extension-rpc.ts +0 -143
  264. package/src/runtime/hidden-handoff.ts +0 -407
  265. package/src/runtime/prose-compressor.ts +0 -162
  266. package/src/subagents/async-entry.ts +0 -1
  267. package/src/subagents/index.ts +0 -3
  268. package/src/subagents/live/control.ts +0 -1
  269. package/src/subagents/live/manager.ts +0 -1
  270. package/src/subagents/live/realtime.ts +0 -1
  271. package/src/subagents/live/session-runtime.ts +0 -1
  272. package/src/subagents/manager.ts +0 -1
  273. package/src/subagents/spawn.ts +0 -1
  274. package/test-integration-check.ts +0 -118
  275. /package/src/runtime/{broker-issuer.ts → broker/broker-issuer.ts} +0 -0
  276. /package/src/runtime/{crew-broker-tokens.ts → broker/crew-broker-tokens.ts} +0 -0
  277. /package/src/runtime/{child-pi-pool.ts → child-pi/child-pi-pool.ts} +0 -0
  278. /package/src/runtime/{compact-pipeline.ts → compaction/compact-pipeline.ts} +0 -0
  279. /package/src/runtime/{compact-stages → compaction/compact-stages}/ansi-strip-stage.ts +0 -0
  280. /package/src/runtime/{compact-stages → compaction/compact-stages}/blank-collapse-stage.ts +0 -0
  281. /package/src/runtime/{compact-stages → compaction/compact-stages}/deduplicate-stage.ts +0 -0
  282. /package/src/runtime/{compact-stages → compaction/compact-stages}/head-snap-stage.ts +0 -0
  283. /package/src/runtime/{compact-stages → compaction/compact-stages}/tail-capture-stage.ts +0 -0
  284. /package/src/runtime/{compact-stages → compaction/compact-stages}/truncation-stage.ts +0 -0
  285. /package/src/runtime/{important-line-classifier.ts → compaction/important-line-classifier.ts} +0 -0
  286. /package/src/runtime/{heartbeat-gradient.ts → heartbeat/heartbeat-gradient.ts} +0 -0
  287. /package/src/runtime/{worker-heartbeat.ts → heartbeat/worker-heartbeat.ts} +0 -0
  288. /package/src/runtime/{worker-startup.ts → heartbeat/worker-startup.ts} +0 -0
  289. /package/src/runtime/{intercom-bridge.ts → live-session/intercom-bridge.ts} +0 -0
  290. /package/src/runtime/{live-control-realtime.ts → live-session/live-control-realtime.ts} +0 -0
  291. /package/src/runtime/{live-extension-bridge.ts → live-session/live-extension-bridge.ts} +0 -0
  292. /package/src/runtime/{live-irc.ts → live-session/live-irc.ts} +0 -0
  293. /package/src/runtime/{live-session-health.ts → live-session/live-session-health.ts} +0 -0
  294. /package/src/runtime/{model-resolver.ts → model/model-resolver.ts} +0 -0
  295. /package/src/runtime/{model-scope.ts → model/model-scope.ts} +0 -0
  296. /package/src/runtime/{output-validator.ts → output/output-validator.ts} +0 -0
  297. /package/src/runtime/{pi-json-output.ts → output/pi-json-output.ts} +0 -0
  298. /package/src/runtime/{progress-event-coalescer.ts → output/progress-event-coalescer.ts} +0 -0
  299. /package/src/runtime/{cancellation-token.ts → process/cancellation-token.ts} +0 -0
  300. /package/src/runtime/{zombie-scanner.ts → process/zombie-scanner.ts} +0 -0
  301. /package/src/runtime/{crash-classification.ts → recovery/crash-classification.ts} +0 -0
  302. /package/src/runtime/{batch-barrier.ts → scheduling/batch-barrier.ts} +0 -0
  303. /package/src/runtime/{global-worker-cap.ts → scheduling/global-worker-cap.ts} +0 -0
  304. /package/src/runtime/{parallel-utils.ts → scheduling/parallel-utils.ts} +0 -0
  305. /package/src/runtime/{scheduler.ts → scheduling/scheduler.ts} +0 -0
  306. /package/src/runtime/{task-graph.ts → scheduling/task-graph.ts} +0 -0
  307. /package/src/runtime/{verification-worktree.ts → verification/verification-worktree.ts} +0 -0
  308. /package/src/state/{event-reconstructor.ts → event-log/event-reconstructor.ts} +0 -0
  309. /package/src/state/{worker-atomic-writer.ts → event-log/worker-atomic-writer.ts} +0 -0
@@ -23,6 +23,8 @@
23
23
  * `use-global-shortcuts.ts:38-61`.
24
24
  */
25
25
 
26
+ import * as fs from "node:fs";
27
+ import * as path from "node:path";
26
28
  import { type KeyId, matchesKey } from "@earendil-works/pi-tui";
27
29
  import { keyOf } from "./key-utils.ts";
28
30
 
@@ -113,7 +115,10 @@ export type DashboardKeyAction =
113
115
  | "notifications-dismiss";
114
116
 
115
117
  /**
116
- * The dispatch table. ORDER MATTERS — first match wins.
118
+ * The default dispatch table. ORDER MATTERS — first match wins. These
119
+ * hardcoded defaults may be overridden per-action via `.crew/config.json`
120
+ * (`keybindings` section) and/or the `PI_CREW_KEYBINDINGS` env var — see
121
+ * `getEffectiveBindings` below.
117
122
  *
118
123
  * Precedence notes (must match the pre-L2 if-chain exactly):
119
124
  * 1. `close` always wins (q / Esc).
@@ -129,7 +134,7 @@ export type DashboardKeyAction =
129
134
  * reservation but are handled by the mailbox overlay's own `handleInput`,
130
135
  * not by the dashboard dispatch. Adding them here would change behavior.
131
136
  */
132
- const BINDINGS: readonly KeyBinding[] = [
137
+ const DEFAULT_BINDINGS: readonly KeyBinding[] = [
133
138
  { keys: DASHBOARD_KEYS.close, action: "close" },
134
139
  { keys: DASHBOARD_KEYS.help, action: "help" },
135
140
  {
@@ -202,13 +207,178 @@ const KEY_RESERVED = new Set<string>([
202
207
 
203
208
  export { KEY_RESERVED };
204
209
 
210
+ // ─── Keybinding overrides (UI-2) ───────────────────────────────────────────
211
+ //
212
+ // The hardcoded DEFAULT_BINDINGS above can be overridden per-action via two
213
+ // layered sources (later wins):
214
+ // 1. `.crew/config.json` → top-level `keybindings` object, e.g.
215
+ // { "keybindings": { "reload": ["z"], "events": ["E"] } }
216
+ // 2. `PI_CREW_KEYBINDINGS` env var — a JSON object string of the same shape
217
+ // (highest precedence; handy for ad-hoc / test overrides).
218
+ //
219
+ // Each entry REPLACES the default key list for that action (the action keeps
220
+ // its original pane scope). Actions not listed keep their defaults, so the
221
+ // parity golden snapshot is unaffected when no override is configured.
222
+ //
223
+ // Collision validation: an overridden key that would clash with another
224
+ // (default or overridden) binding in a compatible pane scope is treated as a
225
+ // collision; the offending action's override is reverted to its default and a
226
+ // warning is recorded (getKeybindingOverrideWarnings). This keeps the
227
+ // first-match-wins dispatch unambiguous — a shadowed override never silently
228
+ // changes behaviour.
229
+
230
+ /** Override map: action → replacement keys. `Partial` ⇒ only listed actions. */
231
+ export type KeybindingOverride = Partial<Record<DashboardKeyAction, readonly string[]>>;
232
+
233
+ const KEYBINDINGS_ENV = "PI_CREW_KEYBINDINGS";
234
+
235
+ /** Every dispatched action is a valid override target. */
236
+ const VALID_OVERRIDE_ACTIONS: ReadonlySet<string> = new Set(DEFAULT_BINDINGS.map((b) => b.action));
237
+
238
+ /** Coerce an unknown parsed value into a safe {@link KeybindingOverride}. */
239
+ function parseKeybindingOverride(raw: unknown): KeybindingOverride {
240
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
241
+ const result: KeybindingOverride = {};
242
+ for (const [action, keys] of Object.entries(raw as Record<string, unknown>)) {
243
+ if (!VALID_OVERRIDE_ACTIONS.has(action)) continue;
244
+ if (!Array.isArray(keys)) continue;
245
+ const clean = keys.filter((k): k is string => typeof k === "string" && k.length > 0);
246
+ if (clean.length > 0) result[action as DashboardKeyAction] = clean;
247
+ }
248
+ return result;
249
+ }
250
+
251
+ /**
252
+ * Two pane scopes are "compatible" when some `activePane` could make both
253
+ * bindings fire at once (so a shared key is genuinely ambiguous). Global
254
+ * (`undefined`) matches anything; two different concrete panes never overlap.
255
+ */
256
+ function paneScopesCompatible(a: ActivePane | undefined, b: ActivePane | undefined): boolean {
257
+ if (a === undefined || b === undefined) return true;
258
+ return a === b;
259
+ }
260
+
261
+ interface EffectiveBindingsResult {
262
+ readonly bindings: readonly KeyBinding[];
263
+ /** Actions whose override was rejected due to a collision. */
264
+ readonly reverted: readonly DashboardKeyAction[];
265
+ }
266
+
267
+ /**
268
+ * Apply `overrides` onto {@link DEFAULT_BINDINGS} (replace keys per action,
269
+ * preserving each action's pane scope) and detect collisions. A colliding
270
+ * override is reverted to its default so the dispatch stays unambiguous.
271
+ */
272
+ function computeEffectiveBindings(overrides: KeybindingOverride): EffectiveBindingsResult {
273
+ const applied = new Map<DashboardKeyAction, KeyBinding>();
274
+ for (const def of DEFAULT_BINDINGS) {
275
+ const ov = overrides[def.action];
276
+ applied.set(def.action, ov && ov.length > 0 ? { keys: [...ov], action: def.action, pane: def.pane } : def);
277
+ }
278
+ const effective = [...applied.values()];
279
+ const reverted = new Set<DashboardKeyAction>();
280
+ for (const def of DEFAULT_BINDINGS) {
281
+ const ov = overrides[def.action];
282
+ if (!ov || ov.length === 0) continue; // not overridden
283
+ const ob = applied.get(def.action);
284
+ if (!ob) continue;
285
+ for (const other of effective) {
286
+ if (other.action === def.action) continue;
287
+ if (!paneScopesCompatible(ob.pane, other.pane)) continue;
288
+ if (ob.keys.some((k) => other.keys.includes(k))) {
289
+ reverted.add(def.action);
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ const bindings =
295
+ reverted.size > 0
296
+ ? effective.map((b) => (reverted.has(b.action) ? (DEFAULT_BINDINGS.find((d) => d.action === b.action) ?? b) : b))
297
+ : effective;
298
+ return { bindings, reverted: [...reverted] };
299
+ }
300
+
301
+ /** Read the `keybindings` section from `<cwd>/.crew/config.json`. */
302
+ function readConfigKeybindings(cwd: string): KeybindingOverride {
303
+ try {
304
+ const raw: unknown = JSON.parse(fs.readFileSync(path.join(cwd, ".crew", "config.json"), "utf-8"));
305
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
306
+ return parseKeybindingOverride((raw as Record<string, unknown>).keybindings);
307
+ } catch {
308
+ return {};
309
+ }
310
+ }
311
+
312
+ /** Read the `PI_CREW_KEYBINDINGS` env var (JSON object string). */
313
+ function readEnvKeybindings(): KeybindingOverride {
314
+ const raw = process.env[KEYBINDINGS_ENV];
315
+ if (!raw) return {};
316
+ try {
317
+ return parseKeybindingOverride(JSON.parse(raw));
318
+ } catch {
319
+ return {};
320
+ }
321
+ }
322
+
323
+ function configKeybindingsMtime(cwd: string): number | undefined {
324
+ try {
325
+ return fs.statSync(path.join(cwd, ".crew", "config.json")).mtimeMs;
326
+ } catch {
327
+ return undefined;
328
+ }
329
+ }
330
+
331
+ interface EffectiveCache {
332
+ readonly env: string | undefined;
333
+ readonly configMtime: number | undefined;
334
+ readonly cwd: string;
335
+ readonly bindings: readonly KeyBinding[];
336
+ }
337
+
338
+ let _effectiveCache: EffectiveCache | null = null;
339
+ let _overrideWarnings: readonly string[] = [];
340
+
341
+ /**
342
+ * Resolve the effective dispatch table: {@link DEFAULT_BINDINGS} with config +
343
+ * env overrides applied (env wins per action). Memoised on (env value, config
344
+ * mtime, cwd); a single `statSync` per call detects on-disk config changes.
345
+ */
346
+ function getEffectiveBindings(cwd: string = process.cwd()): readonly KeyBinding[] {
347
+ const envRaw = process.env[KEYBINDINGS_ENV];
348
+ const configMtime = configKeybindingsMtime(cwd);
349
+ if (_effectiveCache && _effectiveCache.env === envRaw && _effectiveCache.configMtime === configMtime && _effectiveCache.cwd === cwd) {
350
+ return _effectiveCache.bindings;
351
+ }
352
+ const merged: KeybindingOverride = { ...readConfigKeybindings(cwd), ...readEnvKeybindings() };
353
+ const { bindings, reverted } = computeEffectiveBindings(merged);
354
+ _overrideWarnings = reverted.map((a) => `keybinding override for '${a}' collides with another binding — reverting to default`);
355
+ _effectiveCache = { env: envRaw, configMtime, cwd, bindings };
356
+ return bindings;
357
+ }
358
+
359
+ /** Warnings from the most recent override resolution (e.g. collisions). */
360
+ export function getKeybindingOverrideWarnings(): readonly string[] {
361
+ // Ensure a resolution has run so warnings are populated.
362
+ getEffectiveBindings();
363
+ return _overrideWarnings;
364
+ }
365
+
366
+ /** @internal — drop the memoised effective-binding cache (tests). */
367
+ export function __test__resetKeybindingCache(): void {
368
+ _effectiveCache = null;
369
+ _overrideWarnings = [];
370
+ }
371
+
205
372
  /**
206
373
  * Resolve a raw input `data` string to a dashboard action.
207
374
  *
208
- * Data-driven dispatch: iterates `BINDINGS` in order and returns the action of
209
- * the first binding whose `keys` contain `data` and whose optional `pane`
210
- * restriction matches `activePane`. Behavior is identical to the pre-L2
211
- * if-chain (verified by `test/unit/keybinding-map.parity.test.ts`).
375
+ * Data-driven dispatch: iterates the effective binding table (hardcoded
376
+ * {@link DEFAULT_BINDINGS}, optionally overridden per-action via config/env —
377
+ * see {@link getEffectiveBindings}) in order and returns the action of the
378
+ * first binding whose `keys` contain `data` and whose optional `pane`
379
+ * restriction matches `activePane`. With no override configured the result is
380
+ * identical to the pre-L2 if-chain (verified by
381
+ * `test/unit/keybinding-map.parity.test.ts`).
212
382
  *
213
383
  * @param data Raw key input (single char or escape sequence).
214
384
  * @param activePane Currently focused pane; pane-scoped bindings only fire
@@ -217,6 +387,9 @@ export { KEY_RESERVED };
217
387
  * arg skipped the `activePane === ...` branches).
218
388
  */
219
389
  export function dashboardActionForKey(data: string, activePane?: ActivePane): DashboardKeyAction | undefined {
390
+ // Effective table = hardcoded DEFAULT_BINDINGS with optional config/env
391
+ // overrides applied (see getEffectiveBindings). Memoised; one statSync/call.
392
+ const BINDINGS = getEffectiveBindings();
220
393
  // Two-pass dispatch to preserve case-sensitivity for plain ASCII keys
221
394
  // while still normalizing escape sequences via matchesKey().
222
395
  //
@@ -1,9 +1,11 @@
1
1
  import { pad, wrapHard } from "../utils/visual.ts";
2
+ import type { CrewComponent } from "./component.ts";
2
3
 
3
- export interface RenderableComponent {
4
- invalidate(): void;
5
- render(width: number): string[];
6
- }
4
+ // PR-G3 (UI-3): the shared CrewComponent contract (render + invalidate).
5
+ // RenderableComponent now extends it so every layout primitive is usable
6
+ // wherever a CrewComponent is expected. Structurally identical — no behavior
7
+ // change.
8
+ export interface RenderableComponent extends CrewComponent {}
7
9
 
8
10
  export class Container implements RenderableComponent {
9
11
  private children: RenderableComponent[] = [];
@@ -102,5 +104,7 @@ export class Spacer implements RenderableComponent {
102
104
  return Array.from({ length: Math.max(0, this.rows) }, () => pad("", width));
103
105
  }
104
106
 
105
- invalidate(): void {}
107
+ invalidate(): void {
108
+ /* no-op */
109
+ }
106
110
  }
@@ -4,7 +4,7 @@
4
4
  * R8: Subscribes to session events for real-time streaming updates.
5
5
  * Falls back to polling LiveAgentHandle.activity when subscribe is unavailable.
6
6
  */
7
- import type { LiveAgentHandle } from "../runtime/live-agent-manager.ts";
7
+ import type { LiveAgentHandle } from "../runtime/live-session/live-agent-manager.ts";
8
8
  import { pad, truncate } from "../utils/visual.ts";
9
9
  import { computeLiveDurationMs } from "./live-duration.ts";
10
10
  import { spinnerFrame } from "./spinner.ts";
@@ -3,7 +3,7 @@ import type { CrewUiConfig } from "../config/config.ts";
3
3
  import { applyAttentionState, resolveCrewControlConfig } from "../runtime/agent-control.ts";
4
4
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
5
5
  import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
6
- import { loadRunManifestById } from "../state/state-store.ts";
6
+ import { loadRunManifestById } from "../state/stores/state-store.ts";
7
7
  import type { TeamTaskState } from "../state/types.ts";
8
8
  import { aggregateUsage, formatUsage } from "../state/usage.ts";
9
9
  import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
@@ -0,0 +1,148 @@
1
+ /**
2
+ * PR-G3 (UI-4) — minimal overlay stack for z-order, focus, and dismissal
3
+ * chaining.
4
+ *
5
+ * Before this, src/ui/ had no central router for overlay z-order/focus/
6
+ * dismissal. Overlays (ConfirmOverlay, MailboxDetailOverlay, AgentPickerOverlay,
7
+ * HelpOverlay, …) each manage their own `handleInput`, and a caller that wants
8
+ * two overlays up at once had no way to express "the top one gets input and
9
+ * ESC dismisses it, restoring focus to the one below".
10
+ *
11
+ * The `OverlayStack` is a LIFO stack of overlays. The top of the stack owns
12
+ * input focus and is rendered last (i.e. on top). Popping or dismissing the
13
+ * top restores focus to the next overlay down. Every overlay already exposes
14
+ * the `InteractiveComponent` contract (render + invalidate + handleInput), so
15
+ * existing overlays opt in automatically with zero rewrite.
16
+ *
17
+ * This is intentionally additive: no existing overlay is required to use it.
18
+ * Callers that want centralized z-order/focus management opt in by pushing
19
+ * overlays here.
20
+ */
21
+
22
+ import type { InteractiveComponent } from "./component.ts";
23
+
24
+ /**
25
+ * An overlay is an interactive component occupying one z-layer. Anything with
26
+ * `render` + `invalidate` + `handleInput` qualifies (ConfirmOverlay,
27
+ * MailboxDetailOverlay, AgentPickerOverlay, RunDashboard, …).
28
+ */
29
+ export type Overlay = InteractiveComponent;
30
+
31
+ export interface OverlayStackOptions {
32
+ /**
33
+ * Invoked after the top-of-stack changes (push / pop / dismissTop / clear).
34
+ * Use it to (re)wire host focus or request a repaint. Optional.
35
+ */
36
+ onTopChanged?: () => void;
37
+ }
38
+
39
+ /**
40
+ * LIFO overlay stack providing z-order, focus, and dismissal chaining.
41
+ *
42
+ * - `push(o)` → `o` becomes the new focus/top layer.
43
+ * - `top()` → the focused overlay (or undefined when empty).
44
+ * - `handleInput(data)` → routed to the top overlay only; returns false when
45
+ * the stack is empty so callers can fall back to base input handling.
46
+ * - `dismissTop()` / `pop()` → removes the top overlay, restoring focus below.
47
+ * - `render(width)` → renders the top overlay by default; `{ composite: true }`
48
+ * returns every layer bottom-to-top for custom compositing.
49
+ */
50
+ export class OverlayStack {
51
+ private readonly stack: Overlay[] = [];
52
+ private readonly onTopChanged?: () => void;
53
+
54
+ constructor(options: OverlayStackOptions = {}) {
55
+ this.onTopChanged = options.onTopChanged;
56
+ }
57
+
58
+ /** Number of overlays currently on the stack. */
59
+ get size(): number {
60
+ return this.stack.length;
61
+ }
62
+
63
+ /** Whether the stack currently holds any overlays. */
64
+ isEmpty(): boolean {
65
+ return this.stack.length === 0;
66
+ }
67
+
68
+ /** Push a new overlay on top. It becomes the input/render focus. */
69
+ push(overlay: Overlay): void {
70
+ this.stack.push(overlay);
71
+ this.notifyTopChanged();
72
+ }
73
+
74
+ /** Remove and return the top overlay, or undefined if the stack is empty. */
75
+ pop(): Overlay | undefined {
76
+ const overlay = this.stack.pop();
77
+ this.notifyTopChanged();
78
+ return overlay;
79
+ }
80
+
81
+ /** The top-of-stack overlay (the focus/render target), or undefined when empty. */
82
+ top(): Overlay | undefined {
83
+ return this.stack.at(-1);
84
+ }
85
+
86
+ /** Peek at the overlay at a given depth (0 = bottom). */
87
+ at(index: number): Overlay | undefined {
88
+ return this.stack[index];
89
+ }
90
+
91
+ /**
92
+ * Dismiss the top overlay. Equivalent to `pop()` but returns a boolean so
93
+ * callers can chain dismissal (e.g. ESC closes the topmost, then the next).
94
+ * Returns true when an overlay was dismissed, false when the stack is empty.
95
+ */
96
+ dismissTop(): boolean {
97
+ if (this.stack.length === 0) return false;
98
+ this.stack.pop();
99
+ this.notifyTopChanged();
100
+ return true;
101
+ }
102
+
103
+ /**
104
+ * Route a raw input chunk to the top-of-stack overlay.
105
+ * @returns true if an overlay handled it (stack non-empty); false if the
106
+ * stack is empty so the caller may fall back to base input handling.
107
+ */
108
+ handleInput(data: string): boolean {
109
+ const overlay = this.top();
110
+ if (!overlay) return false;
111
+ overlay.handleInput(data);
112
+ return true;
113
+ }
114
+
115
+ /**
116
+ * Render the stack. By default returns the top overlay's lines (the common
117
+ * modal case where the top covers what is below). Pass `{ composite: true }`
118
+ * to receive every layer's lines in bottom-to-top order for custom
119
+ * compositing. Returns `[]` when the stack is empty.
120
+ */
121
+ render(width: number, options: { composite?: boolean } = {}): string[] {
122
+ if (this.stack.length === 0) return [];
123
+ if (options.composite) {
124
+ const lines: string[] = [];
125
+ for (const overlay of this.stack) {
126
+ lines.push(...overlay.render(width));
127
+ }
128
+ return lines;
129
+ }
130
+ return this.stack.at(-1)!.render(width);
131
+ }
132
+
133
+ /** Invalidate every overlay on the stack (e.g. on theme change). */
134
+ invalidateAll(): void {
135
+ for (const overlay of this.stack) overlay.invalidate();
136
+ }
137
+
138
+ /** Remove every overlay from the stack. */
139
+ clear(): void {
140
+ const hadOverlays = this.stack.length > 0;
141
+ this.stack.length = 0;
142
+ if (hadOverlays) this.notifyTopChanged();
143
+ }
144
+
145
+ private notifyTopChanged(): void {
146
+ this.onTopChanged?.();
147
+ }
148
+ }
@@ -1,6 +1,6 @@
1
1
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
2
2
  import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
3
- import { loadRunManifestById } from "../../state/state-store.ts";
3
+ import { loadRunManifestById } from "../../state/stores/state-store.ts";
4
4
  import { pad, truncate } from "../../utils/visual.ts";
5
5
  import { keyOf } from "../key-utils.ts";
6
6
  import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
@@ -1,4 +1,4 @@
1
- import type { MailboxDirection } from "../../state/mailbox.ts";
1
+ import type { MailboxDirection } from "../../state/coordination/mailbox.ts";
2
2
  import { pad, truncate } from "../../utils/visual.ts";
3
3
  import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
4
4
  import { ConfirmOverlay } from "./confirm-overlay.ts";
@@ -1,5 +1,5 @@
1
- import { type MailboxMessage, readDeliveryState, readMailbox } from "../../state/mailbox.ts";
2
- import { loadRunManifestById } from "../../state/state-store.ts";
1
+ import { type MailboxMessage, readDeliveryState, readMailbox } from "../../state/coordination/mailbox.ts";
2
+ import { loadRunManifestById } from "../../state/stores/state-store.ts";
3
3
  import { pad, truncate } from "../../utils/visual.ts";
4
4
  import { keyOf } from "../key-utils.ts";
5
5
  import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import type { CrewUiConfig } from "../config/config.ts";
3
3
  import { listRecentRuns } from "../extension/run-index.ts";
4
4
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
5
- import { listLiveAgents } from "../runtime/live-agent-manager.ts";
5
+ import { listLiveAgents } from "../runtime/live-session/live-agent-manager.ts";
6
6
  import type { ManifestCache } from "../runtime/manifest-cache.ts";
7
7
  import { isDisplayActiveRun } from "../runtime/process-status.ts";
8
8
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
@@ -1,7 +1,7 @@
1
1
  import { logInternalError } from "../utils/internal-error.ts";
2
2
 
3
3
  export interface RenderSchedulerEventBus {
4
- on?: (event: string, handler: (payload: unknown) => void) => (() => void) | void;
4
+ on?: (event: string, handler: (payload: unknown) => void) => (() => void) | undefined;
5
5
  }
6
6
 
7
7
  export interface RenderSchedulerOptions {
@@ -19,9 +19,9 @@ async function handleTeamTool(
19
19
  import { isToolError, textFromToolResult } from "../extension/tool-result.ts";
20
20
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
21
21
  import { exportDiagnostic } from "../runtime/diagnostic-export.ts";
22
- import { appendEvent } from "../state/event-log.ts";
23
- import type { MailboxDirection, MailboxMessage } from "../state/mailbox.ts";
24
- import { loadRunManifestById, saveRunTasks } from "../state/state-store.ts";
22
+ import type { MailboxDirection, MailboxMessage } from "../state/coordination/mailbox.ts";
23
+ import { appendEvent } from "../state/event-log/event-log.ts";
24
+ import { loadRunManifestById, saveRunTasks } from "../state/stores/state-store.ts";
25
25
 
26
26
  export interface RunActionResult {
27
27
  ok: boolean;
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import type { MetricRegistry } from "../observability/metric-registry.ts";
3
3
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
4
4
  import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
5
- import { getLiveAgentContextPercent } from "../runtime/live-agent-manager.ts";
5
+ import { getLiveAgentContextPercent } from "../runtime/live-session/live-agent-manager.ts";
6
6
  import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
7
7
  import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
8
8
  import { aggregateUsage } from "../state/usage.ts";
@@ -10,6 +10,7 @@ import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
10
10
  import { logInternalError } from "../utils/internal-error.ts";
11
11
  import { resolveRealContainedPath } from "../utils/safe-paths.ts";
12
12
  import { pad, sanitizeLine, truncate, visibleWidth } from "../utils/visual.ts";
13
+ import type { InteractiveComponent } from "./component.ts";
13
14
  import { renderAgentsPane } from "./dashboard-panes/agents-pane.ts";
14
15
  import { summarizeTerminalReason } from "./dashboard-panes/cancellation-pane.ts";
15
16
  import { renderHealthPane } from "./dashboard-panes/health-pane.ts";
@@ -40,11 +41,9 @@ function safeRenderPane(name: string, fn: () => string[]): string[] {
40
41
  }
41
42
  }
42
43
 
43
- interface DashboardComponent {
44
- invalidate(): void;
45
- render(width: number): string[];
46
- handleInput(data: string): void;
47
- }
44
+ // PR-G3 (UI-3): extend the shared InteractiveComponent contract (render +
45
+ // invalidate + handleInput). Structurally identical — no behavior change.
46
+ interface DashboardComponent extends InteractiveComponent {}
48
47
 
49
48
  export interface RunDashboardOptions {
50
49
  placement?: "center" | "right";
@@ -401,11 +400,6 @@ function countByStatus(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache
401
400
  }
402
401
 
403
402
  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
403
  private selected = 0;
410
404
  private runScrollOffset = 0;
411
405
  private showFullProgress = false;
@@ -431,18 +425,6 @@ export class RunDashboard implements DashboardComponent {
431
425
  theme: unknown = {},
432
426
  options: RunDashboardOptions = {},
433
427
  ) {
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
428
  // Filter runs by workspaceId for session isolation
447
429
  // If workspaceId is provided, only show runs owned by that session or runs with no owner (legacy)
448
430
  const filteredRuns = options.workspaceId
@@ -822,16 +804,6 @@ export class RunDashboard implements DashboardComponent {
822
804
  public focused = false;
823
805
 
824
806
  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
807
  const action = dashboardActionForKey(data, this.activePane);
836
808
  // K-1: "?" toggles the help overlay; while it is shown, any other key
837
809
  // (including Esc) just dismisses it first instead of acting.
@@ -1,5 +1,5 @@
1
- import type { TeamEvent } from "../state/event-log.ts";
2
- import { readEventsCursor } from "../state/event-log.ts";
1
+ import type { TeamEvent } from "../state/event-log/event-log.ts";
2
+ import { readEventsCursor } from "../state/event-log/event-log.ts";
3
3
  import type { RenderSchedulerEventBus } from "./render-scheduler.ts";
4
4
 
5
5
  export type RunEventType =
@@ -342,7 +342,7 @@ export const runEventBus = new RunEventBus();
342
342
  export function runEventBusAsRenderScheduler(channels: readonly EventChannel[]): RenderSchedulerEventBus {
343
343
  const allowed = new Set<EventChannel>(channels);
344
344
  return {
345
- on: (event: string, handler: (payload: unknown) => void): (() => void) | void => {
345
+ on: (event: string, handler: (payload: unknown) => void): (() => void) | undefined => {
346
346
  if (!allowed.has(event as EventChannel)) return undefined;
347
347
  return runEventBus.onChannel(event as EventChannel, handler);
348
348
  },
@@ -4,10 +4,10 @@ import * as path from "node:path";
4
4
  import { agentOutputPath, agentsPath, readCrewAgents, readCrewAgentsAsync } from "../runtime/crew-agent-records.ts";
5
5
  import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
6
6
  import { isActiveRunStatus } from "../runtime/process-status.ts";
7
- import type { TeamEvent } from "../state/event-log.ts";
8
- import { sequencePath } from "../state/event-log.ts";
9
- import type { MailboxMessageStatus } from "../state/mailbox.ts";
10
- import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/state-store.ts";
7
+ import type { MailboxMessageStatus } from "../state/coordination/mailbox.ts";
8
+ import type { TeamEvent } from "../state/event-log/event-log.ts";
9
+ import { sequencePath } from "../state/event-log/event-log.ts";
10
+ import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/stores/state-store.ts";
11
11
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
12
12
  import { extractDwfPhaseState } from "./dwf-phase-display.ts";
13
13
  import { runEventBus } from "./run-event-bus.ts";
@@ -629,15 +629,19 @@ async function mailboxFromAsync(manifest: TeamRunManifest, agents: CrewAgentReco
629
629
  let outbox = await readMailboxCountsAsync(path.join(root, "outbox.jsonl"), delivery);
630
630
  const tasksRoot = path.join(root, "tasks");
631
631
  try {
632
- for (const entry of await fs.promises.readdir(tasksRoot, {
633
- withFileTypes: true,
634
- })) {
635
- if (!entry.isDirectory()) continue;
636
- const taskInbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
637
- const taskOutbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
638
- inbox = mergeKindCounts(inbox, taskInbox);
639
- outbox = mergeKindCounts(outbox, taskOutbox);
640
- }
632
+ // PR-F2 / UI-5 — batch the O(tasks) per-task inbox/outbox reads in
633
+ // parallel instead of sequential awaits on the async render path.
634
+ const taskDirs = (
635
+ await fs.promises.readdir(tasksRoot, {
636
+ withFileTypes: true,
637
+ })
638
+ ).filter((entry) => entry.isDirectory());
639
+ const [taskInboxes, taskOutboxes] = await Promise.all([
640
+ Promise.all(taskDirs.map((entry) => readMailboxCountsAsync(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery))),
641
+ Promise.all(taskDirs.map((entry) => readMailboxCountsAsync(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery))),
642
+ ]);
643
+ for (const ti of taskInboxes) inbox = mergeKindCounts(inbox, ti);
644
+ for (const to of taskOutboxes) outbox = mergeKindCounts(outbox, to);
641
645
  } catch {
642
646
  // No task mailboxes yet.
643
647
  }
@@ -991,6 +995,21 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
991
995
  evictIfNeeded();
992
996
  return entry.snapshot;
993
997
  }
998
+ // PR-F2 / UI-1 — in-flight async refresh dedup. Prevents multiple
999
+ // concurrent `preloadStale` calls for the same runId when the render
1000
+ // path (refreshIfStale) and event-bus handler fire close together.
1001
+ const inFlightRefreshes = new Set<string>();
1002
+ function triggerAsyncRefresh(runId: string): void {
1003
+ if (inFlightRefreshes.has(runId)) return;
1004
+ inFlightRefreshes.add(runId);
1005
+ void preloadStale(runId)
1006
+ .catch(() => {
1007
+ /* best-effort; widget falls back to cached snapshot */
1008
+ })
1009
+ .finally(() => {
1010
+ inFlightRefreshes.delete(runId);
1011
+ });
1012
+ }
994
1013
  function localRefreshIfStale(runId: string): RunUiSnapshot {
995
1014
  const previous = entries.get(runId);
996
1015
  if (!previous) return localRefresh(runId);
@@ -1007,11 +1026,10 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
1007
1026
  if (existing) clearTimeout(existing);
1008
1027
  const timer = setTimeout(() => {
1009
1028
  pendingRefreshes.delete(runId);
1010
- try {
1011
- localRefreshIfStale(runId);
1012
- } catch {
1013
- /* best-effort; widget falls back gracefully */
1014
- }
1029
+ // PR-F2 / UI-1 — use the async refresh path (no sync fs on the
1030
+ // event-bus hot path). triggerAsyncRefresh deduplicates
1031
+ // concurrent refreshes.
1032
+ triggerAsyncRefresh(runId);
1015
1033
  }, INVAL_COALESCE_MS);
1016
1034
  timer.unref();
1017
1035
  pendingRefreshes.set(runId, timer);
@@ -1051,6 +1069,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
1051
1069
  },
1052
1070
  dispose(): void {
1053
1071
  unsubscribe();
1072
+ inFlightRefreshes.clear();
1054
1073
  entries.clear();
1055
1074
  },
1056
1075
  };