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.
- package/CHANGELOG.md +45 -0
- package/package.json +6 -3
- package/scripts/clean-strip-types.mjs +60 -0
- package/src/adapters/claude-adapter.ts +23 -0
- package/src/adapters/codex-adapter.ts +21 -0
- package/src/adapters/cursor-adapter.ts +17 -0
- package/src/adapters/export-util.ts +143 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +18 -0
- package/src/adapters/types.ts +23 -0
- package/src/agents/agent-config.ts +194 -0
- package/src/agents/agent-search.ts +98 -0
- package/src/agents/agent-serializer.ts +38 -0
- package/src/agents/discover-agents.ts +623 -0
- package/src/benchmark/benchmark-runner.ts +313 -0
- package/src/benchmark/feedback-loop.ts +73 -0
- package/src/config/config.ts +1270 -0
- package/src/config/defaults.ts +193 -0
- package/src/config/drift-detector.ts +290 -0
- package/src/config/markers.ts +330 -0
- package/src/config/resilient-parser.ts +117 -0
- package/src/config/role-tools.ts +118 -0
- package/src/config/suggestions.ts +75 -0
- package/src/config/types.ts +280 -0
- package/src/errors.ts +194 -0
- package/src/extension/action-suggestions.ts +117 -0
- package/src/extension/async-notifier.ts +179 -0
- package/src/extension/autonomous-policy.ts +209 -0
- package/src/extension/command-completions.ts +120 -0
- package/src/extension/context-status-injection.ts +185 -0
- package/src/extension/crew-autocomplete.ts +131 -0
- package/src/extension/crew-cleanup.ts +168 -0
- package/src/extension/crew-input-router.ts +93 -0
- package/src/extension/crew-shortcuts.ts +87 -0
- package/src/extension/crew-vibes/cat-frames.ts +18 -0
- package/src/extension/crew-vibes/config.ts +195 -0
- package/src/extension/crew-vibes/figures.ts +101 -0
- package/src/extension/crew-vibes/font-detect.ts +71 -0
- package/src/extension/crew-vibes/footer.ts +292 -0
- package/src/extension/crew-vibes/index.ts +432 -0
- package/src/extension/crew-vibes/provider-usage.ts +396 -0
- package/src/extension/crew-vibes/render.ts +206 -0
- package/src/extension/crew-vibes/speed.ts +286 -0
- package/src/extension/cross-extension-rpc.ts +350 -0
- package/src/extension/help.ts +62 -0
- package/src/extension/import-index.ts +74 -0
- package/src/extension/knowledge-injection.ts +402 -0
- package/src/extension/management.ts +571 -0
- package/src/extension/message-renderers.ts +108 -0
- package/src/extension/notification-router.ts +152 -0
- package/src/extension/notification-sink.ts +54 -0
- package/src/extension/pi-api.ts +56 -0
- package/src/extension/plan-orchestrate.ts +302 -0
- package/src/extension/project-init.ts +170 -0
- package/src/extension/register.ts +113 -0
- package/src/extension/registration/artifact-cleanup.ts +20 -0
- package/src/extension/registration/command-registration.ts +58 -0
- package/src/extension/registration/command-utils.ts +58 -0
- package/src/extension/registration/commands.ts +1228 -0
- package/src/extension/registration/compaction-guard.ts +333 -0
- package/src/extension/registration/context-builder.ts +135 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +923 -0
- package/src/extension/registration/lifecycle.ts +259 -0
- package/src/extension/registration/observability.ts +322 -0
- package/src/extension/registration/registration-types.ts +158 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-helpers.ts +125 -0
- package/src/extension/registration/subagent-manager-setup.ts +330 -0
- package/src/extension/registration/subagent-tools.ts +456 -0
- package/src/extension/registration/team-tool.ts +225 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/ui.ts +172 -0
- package/src/extension/registration/viewers.ts +121 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/result-watcher.ts +139 -0
- package/src/extension/rpc-hmac.ts +256 -0
- package/src/extension/run-bundle-schema.ts +104 -0
- package/src/extension/run-export.ts +97 -0
- package/src/extension/run-import.ts +168 -0
- package/src/extension/run-index.ts +131 -0
- package/src/extension/run-maintenance.ts +190 -0
- package/src/extension/session-summary.ts +18 -0
- package/src/extension/team-manager-command.ts +153 -0
- package/src/extension/team-onboard.ts +180 -0
- package/src/extension/team-recommendation.ts +288 -0
- package/src/extension/team-tool/anchor.ts +172 -0
- package/src/extension/team-tool/api.ts +1244 -0
- package/src/extension/team-tool/auto-summarize.ts +142 -0
- package/src/extension/team-tool/cache-control.ts +19 -0
- package/src/extension/team-tool/cancel.ts +337 -0
- package/src/extension/team-tool/chain-dispatch.ts +95 -0
- package/src/extension/team-tool/chain-executor.ts +379 -0
- package/src/extension/team-tool/config-patch.ts +50 -0
- package/src/extension/team-tool/context.ts +149 -0
- package/src/extension/team-tool/destructive-gate.ts +52 -0
- package/src/extension/team-tool/dispatch/automate.ts +80 -0
- package/src/extension/team-tool/dispatch/control.ts +42 -0
- package/src/extension/team-tool/dispatch/index.ts +95 -0
- package/src/extension/team-tool/dispatch/manage.ts +161 -0
- package/src/extension/team-tool/dispatch/run.ts +53 -0
- package/src/extension/team-tool/dispatch/status.ts +187 -0
- package/src/extension/team-tool/doctor.ts +431 -0
- package/src/extension/team-tool/explain.ts +282 -0
- package/src/extension/team-tool/failure-patterns.ts +118 -0
- package/src/extension/team-tool/goal-wrap.ts +330 -0
- package/src/extension/team-tool/goal.ts +556 -0
- package/src/extension/team-tool/handle-schedule.ts +332 -0
- package/src/extension/team-tool/handle-settings.ts +520 -0
- package/src/extension/team-tool/health-monitor.ts +481 -0
- package/src/extension/team-tool/inspect.ts +88 -0
- package/src/extension/team-tool/intent-policy.ts +45 -0
- package/src/extension/team-tool/lifecycle-actions.ts +615 -0
- package/src/extension/team-tool/orchestrate.ts +98 -0
- package/src/extension/team-tool/parallel-dispatch.ts +183 -0
- package/src/extension/team-tool/plan.ts +50 -0
- package/src/extension/team-tool/respond.ts +150 -0
- package/src/extension/team-tool/run-deadline.ts +66 -0
- package/src/extension/team-tool/run-not-found.ts +49 -0
- package/src/extension/team-tool/run.ts +1051 -0
- package/src/extension/team-tool/status.ts +260 -0
- package/src/extension/team-tool/workflow-manage.ts +261 -0
- package/src/extension/team-tool-types.ts +26 -0
- package/src/extension/team-tool.ts +750 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/extension/validate-resources.ts +108 -0
- package/src/hooks/registry.ts +200 -0
- package/src/hooks/types.ts +69 -0
- package/src/i18n.ts +212 -0
- package/src/observability/correlation.ts +50 -0
- package/src/observability/event-bus.ts +86 -0
- package/src/observability/event-to-metric.ts +170 -0
- package/src/observability/exporters/adapter.ts +27 -0
- package/src/observability/exporters/otlp-exporter.ts +356 -0
- package/src/observability/exporters/prometheus-exporter.ts +54 -0
- package/src/observability/metric-registry.ts +100 -0
- package/src/observability/metric-retention.ts +64 -0
- package/src/observability/metric-sink.ts +99 -0
- package/src/observability/metrics-primitives.ts +219 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +10 -0
- package/src/plugins/plugins/vitest.ts +9 -0
- package/src/prompt/prompt-runtime.ts +380 -0
- package/src/runtime/adaptive-plan.ts +563 -0
- package/src/runtime/agent-control.ts +215 -0
- package/src/runtime/agent-memory.ts +81 -0
- package/src/runtime/agent-observability.ts +122 -0
- package/src/runtime/anchor-manager.ts +475 -0
- package/src/runtime/async-marker.ts +32 -0
- package/src/runtime/async-runner.ts +381 -0
- package/src/runtime/attention-events.ts +28 -0
- package/src/runtime/auto-summarize.ts +344 -0
- package/src/runtime/background-runner.ts +840 -0
- package/src/runtime/batch-barrier.ts +147 -0
- package/src/runtime/broker-issuer.ts +39 -0
- package/src/runtime/cancellation-token.ts +99 -0
- package/src/runtime/cancellation.ts +99 -0
- package/src/runtime/capability-inventory.ts +137 -0
- package/src/runtime/chain-parser.ts +237 -0
- package/src/runtime/chain-runner.ts +606 -0
- package/src/runtime/checkpoint.ts +291 -0
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi-spawn.ts +287 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +1105 -0
- package/src/runtime/coalesce-tasks.ts +268 -0
- package/src/runtime/code-summary.ts +292 -0
- package/src/runtime/command-trace.ts +105 -0
- package/src/runtime/compact-pipeline.ts +56 -0
- package/src/runtime/compact-stages/ansi-strip-stage.ts +25 -0
- package/src/runtime/compact-stages/blank-collapse-stage.ts +31 -0
- package/src/runtime/compact-stages/deduplicate-stage.ts +34 -0
- package/src/runtime/compact-stages/head-snap-stage.ts +57 -0
- package/src/runtime/compact-stages/index.ts +23 -0
- package/src/runtime/compact-stages/tail-capture-stage.ts +77 -0
- package/src/runtime/compact-stages/truncation-stage.ts +74 -0
- package/src/runtime/compaction-summary.ts +278 -0
- package/src/runtime/completion-guard.ts +207 -0
- package/src/runtime/concurrency.ts +58 -0
- package/src/runtime/crash-classification.ts +229 -0
- package/src/runtime/crash-recovery.ts +594 -0
- package/src/runtime/crew-agent-records.ts +595 -0
- package/src/runtime/crew-agent-runtime.ts +62 -0
- package/src/runtime/crew-broker-child.ts +88 -0
- package/src/runtime/crew-broker-client.ts +673 -0
- package/src/runtime/crew-broker-tokens.ts +159 -0
- package/src/runtime/crew-broker.ts +1304 -0
- package/src/runtime/crew-hooks.ts +227 -0
- package/src/runtime/cross-extension-rpc.ts +143 -0
- package/src/runtime/custom-tools/irc-tool.ts +282 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +102 -0
- package/src/runtime/deadletter.ts +47 -0
- package/src/runtime/delivery-coordinator.ts +211 -0
- package/src/runtime/delta-conflict.ts +348 -0
- package/src/runtime/deterministic-ast.ts +161 -0
- package/src/runtime/diagnostic-export.ts +171 -0
- package/src/runtime/direct-run.ts +45 -0
- package/src/runtime/dwf-state-store.ts +102 -0
- package/src/runtime/dynamic-workflow-context.ts +1013 -0
- package/src/runtime/dynamic-workflow-runner.ts +325 -0
- package/src/runtime/effectiveness.ts +90 -0
- package/src/runtime/errors/crew-errors.ts +162 -0
- package/src/runtime/event-stream-bridge.ts +98 -0
- package/src/runtime/foreground-control.ts +193 -0
- package/src/runtime/foreground-watchdog.ts +132 -0
- package/src/runtime/global-worker-cap.ts +96 -0
- package/src/runtime/goal-achievement.ts +148 -0
- package/src/runtime/goal-evaluator.ts +365 -0
- package/src/runtime/goal-loop-runner.ts +825 -0
- package/src/runtime/goal-state-store.ts +219 -0
- package/src/runtime/green-contract.ts +55 -0
- package/src/runtime/group-join.ts +267 -0
- package/src/runtime/handoff-manager.ts +598 -0
- package/src/runtime/heartbeat-gradient.ts +36 -0
- package/src/runtime/heartbeat-watcher.ts +239 -0
- package/src/runtime/hidden-handoff.ts +407 -0
- package/src/runtime/important-line-classifier.ts +140 -0
- package/src/runtime/intercom-bridge.ts +187 -0
- package/src/runtime/iteration-hooks.ts +305 -0
- package/src/runtime/live-agent-control.ts +136 -0
- package/src/runtime/live-agent-manager.ts +704 -0
- package/src/runtime/live-control-realtime.ts +56 -0
- package/src/runtime/live-extension-bridge.ts +148 -0
- package/src/runtime/live-irc.ts +97 -0
- package/src/runtime/live-session-health.ts +108 -0
- package/src/runtime/live-session-runtime.ts +1146 -0
- package/src/runtime/loop-gates.ts +128 -0
- package/src/runtime/manifest-cache.ts +375 -0
- package/src/runtime/mcp-proxy.ts +105 -0
- package/src/runtime/metric-parser.ts +36 -0
- package/src/runtime/model-fallback.ts +409 -0
- package/src/runtime/model-resolver.ts +126 -0
- package/src/runtime/model-scope.ts +158 -0
- package/src/runtime/orphan-worker-registry.ts +465 -0
- package/src/runtime/output-validator.ts +202 -0
- package/src/runtime/overflow-recovery.ts +206 -0
- package/src/runtime/parallel-research.ts +68 -0
- package/src/runtime/parallel-utils.ts +160 -0
- package/src/runtime/parent-guard.ts +134 -0
- package/src/runtime/path-overlap.ts +150 -0
- package/src/runtime/peer-dep.ts +292 -0
- package/src/runtime/per-write-validator.ts +181 -0
- package/src/runtime/phase-progress.ts +217 -0
- package/src/runtime/phase-tracker.ts +385 -0
- package/src/runtime/pi-args.ts +655 -0
- package/src/runtime/pi-json-output.ts +170 -0
- package/src/runtime/pi-spawn.ts +273 -0
- package/src/runtime/pipeline-runner.ts +523 -0
- package/src/runtime/plan-templates.ts +201 -0
- package/src/runtime/policy-engine.ts +115 -0
- package/src/runtime/post-checks.ts +142 -0
- package/src/runtime/post-exit-stdio-guard.ts +109 -0
- package/src/runtime/process-lifecycle.ts +491 -0
- package/src/runtime/process-status.ts +154 -0
- package/src/runtime/progress-event-coalescer.ts +44 -0
- package/src/runtime/progress-tracker.ts +124 -0
- package/src/runtime/prose-compressor.ts +162 -0
- package/src/runtime/recovery-recipes.ts +203 -0
- package/src/runtime/replace.ts +570 -0
- package/src/runtime/resilient-edit.ts +153 -0
- package/src/runtime/result-extractor.ts +217 -0
- package/src/runtime/retry-executor.ts +109 -0
- package/src/runtime/retry-runner.ts +336 -0
- package/src/runtime/role-permission.ts +59 -0
- package/src/runtime/run-coalesced-task-group.ts +308 -0
- package/src/runtime/run-drift.ts +219 -0
- package/src/runtime/run-tracker.ts +116 -0
- package/src/runtime/run-worker.ts +78 -0
- package/src/runtime/runtime-policy.ts +29 -0
- package/src/runtime/runtime-resolver.ts +172 -0
- package/src/runtime/runtime-warmup.ts +160 -0
- package/src/runtime/scheduler.ts +353 -0
- package/src/runtime/semaphore.ts +141 -0
- package/src/runtime/sensitive-paths.ts +93 -0
- package/src/runtime/session-resources.ts +25 -0
- package/src/runtime/session-snapshot.ts +59 -0
- package/src/runtime/session-usage.ts +79 -0
- package/src/runtime/settings-store.ts +155 -0
- package/src/runtime/sidechain-output.ts +35 -0
- package/src/runtime/single-agent-compose.ts +84 -0
- package/src/runtime/skill-effectiveness.ts +524 -0
- package/src/runtime/skill-instructions.ts +394 -0
- package/src/runtime/stale-reconciler.ts +680 -0
- package/src/runtime/stream-preview.ts +184 -0
- package/src/runtime/streaming-output.ts +50 -0
- package/src/runtime/subagent-manager.ts +561 -0
- package/src/runtime/subprocess-tool-registry.ts +70 -0
- package/src/runtime/supervisor-contact.ts +64 -0
- package/src/runtime/task-display.ts +52 -0
- package/src/runtime/task-graph-scheduler.ts +227 -0
- package/src/runtime/task-graph.ts +290 -0
- package/src/runtime/task-health.ts +83 -0
- package/src/runtime/task-id.ts +161 -0
- package/src/runtime/task-output-context.ts +602 -0
- package/src/runtime/task-packet.ts +268 -0
- package/src/runtime/task-quality.ts +199 -0
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/child-executor.ts +790 -0
- package/src/runtime/task-runner/context-retrieval.ts +163 -0
- package/src/runtime/task-runner/live-executor.ts +227 -0
- package/src/runtime/task-runner/output-splitter.ts +152 -0
- package/src/runtime/task-runner/post-execution.ts +480 -0
- package/src/runtime/task-runner/pre-execution.ts +365 -0
- package/src/runtime/task-runner/progress.ts +157 -0
- package/src/runtime/task-runner/prompt-builder.ts +278 -0
- package/src/runtime/task-runner/prompt-pipeline.ts +88 -0
- package/src/runtime/task-runner/result-utils.ts +16 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
- package/src/runtime/task-runner/run-projection.ts +125 -0
- package/src/runtime/task-runner/scaffold-executor.ts +25 -0
- package/src/runtime/task-runner/state-helpers.ts +176 -0
- package/src/runtime/task-runner/tail-read.ts +34 -0
- package/src/runtime/task-runner.ts +216 -0
- package/src/runtime/team-runner-artifacts.ts +13 -0
- package/src/runtime/team-runner.ts +2396 -0
- package/src/runtime/tool-output-pruner.ts +333 -0
- package/src/runtime/tool-progress.ts +278 -0
- package/src/runtime/usage-tracker.ts +73 -0
- package/src/runtime/verification-gates.ts +579 -0
- package/src/runtime/verification-integrity.ts +108 -0
- package/src/runtime/verification-worktree.ts +186 -0
- package/src/runtime/worker-heartbeat.ts +25 -0
- package/src/runtime/worker-startup.ts +83 -0
- package/src/runtime/workflow-state.ts +195 -0
- package/src/runtime/workspace-lock.ts +443 -0
- package/src/runtime/workspace-tree.ts +312 -0
- package/src/runtime/yield-handler.ts +224 -0
- package/src/runtime/zombie-scanner.ts +298 -0
- package/src/schema/config-schema.ts +327 -0
- package/src/schema/team-tool-schema.ts +492 -0
- package/src/schema/validation-types.ts +159 -0
- package/src/skills/discover-skills.ts +203 -0
- package/src/skills/skill-templates.ts +456 -0
- package/src/skills/validate.ts +285 -0
- package/src/state/active-run-registry.ts +439 -0
- package/src/state/artifact-store.ts +176 -0
- package/src/state/atomic-write.ts +970 -0
- package/src/state/blob-store.ts +308 -0
- package/src/state/contracts.ts +160 -0
- package/src/state/crew-init.ts +269 -0
- package/src/state/decision-ledger.ts +372 -0
- package/src/state/event-log-rotation.ts +399 -0
- package/src/state/event-log.ts +1465 -0
- package/src/state/event-reconstructor.ts +229 -0
- package/src/state/gitignore-manager.ts +46 -0
- package/src/state/health-store.ts +79 -0
- package/src/state/hook-instinct-bridge.ts +94 -0
- package/src/state/hook-integrations.ts +51 -0
- package/src/state/instinct-store.ts +275 -0
- package/src/state/jsonl-writer.ts +107 -0
- package/src/state/locks.ts +562 -0
- package/src/state/mailbox.ts +916 -0
- package/src/state/observation-store.ts +176 -0
- package/src/state/run-cache.ts +193 -0
- package/src/state/run-graph.ts +183 -0
- package/src/state/run-metrics.ts +165 -0
- package/src/state/schedule.ts +166 -0
- package/src/state/session-state-map.ts +51 -0
- package/src/state/state-store.ts +973 -0
- package/src/state/task-claims.ts +61 -0
- package/src/state/tiered-eval.ts +480 -0
- package/src/state/types-eval.ts +58 -0
- package/src/state/types.ts +447 -0
- package/src/state/usage.ts +138 -0
- package/src/state/worker-atomic-writer.ts +198 -0
- package/src/subagents/async-entry.ts +1 -0
- package/src/subagents/index.ts +3 -0
- package/src/subagents/live/control.ts +1 -0
- package/src/subagents/live/manager.ts +1 -0
- package/src/subagents/live/realtime.ts +1 -0
- package/src/subagents/live/session-runtime.ts +1 -0
- package/src/subagents/manager.ts +1 -0
- package/src/subagents/spawn.ts +1 -0
- package/src/teams/discover-teams.ts +187 -0
- package/src/teams/team-config.ts +27 -0
- package/src/teams/team-serializer.ts +38 -0
- package/src/tools/safe-bash-extension.ts +54 -0
- package/src/tools/safe-bash.ts +505 -0
- package/src/types/diff.d.ts +18 -0
- package/src/types/new-api-types.ts +35 -0
- package/src/ui/agent-management-overlay.ts +160 -0
- package/src/ui/card-colors.ts +139 -0
- package/src/ui/crew-footer.ts +102 -0
- package/src/ui/crew-select-list.ts +112 -0
- package/src/ui/dashboard-panes/agents-pane.ts +164 -0
- package/src/ui/dashboard-panes/cancellation-pane.ts +66 -0
- package/src/ui/dashboard-panes/capability-pane.ts +77 -0
- package/src/ui/dashboard-panes/health-pane.ts +31 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -0
- package/src/ui/dashboard-panes/metrics-pane.ts +37 -0
- package/src/ui/dashboard-panes/progress-pane.ts +38 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -0
- package/src/ui/deploy-bundled-themes.ts +71 -0
- package/src/ui/dwf-phase-display.ts +151 -0
- package/src/ui/dynamic-border.ts +35 -0
- package/src/ui/format-helpers.ts +31 -0
- package/src/ui/heartbeat-aggregator.ts +74 -0
- package/src/ui/key-utils.ts +42 -0
- package/src/ui/keybinding-map.ts +248 -0
- package/src/ui/layout-primitives.ts +106 -0
- package/src/ui/live-conversation-overlay.ts +183 -0
- package/src/ui/live-duration.ts +55 -0
- package/src/ui/live-run-sidebar.ts +293 -0
- package/src/ui/loaders.ts +6 -0
- package/src/ui/mascot.ts +449 -0
- package/src/ui/overlays/agent-picker-overlay.ts +66 -0
- package/src/ui/overlays/confirm-overlay.ts +60 -0
- package/src/ui/overlays/help-overlay.ts +177 -0
- package/src/ui/overlays/mailbox-compose-overlay.ts +181 -0
- package/src/ui/overlays/mailbox-compose-preview.ts +78 -0
- package/src/ui/overlays/mailbox-detail-overlay.ts +155 -0
- package/src/ui/pi-ui-compat.ts +68 -0
- package/src/ui/powerbar-publisher.ts +459 -0
- package/src/ui/render-coalescer.ts +101 -0
- package/src/ui/render-diff.ts +160 -0
- package/src/ui/render-scheduler.ts +278 -0
- package/src/ui/run-action-dispatcher.ts +182 -0
- package/src/ui/run-dashboard.ts +910 -0
- package/src/ui/run-event-bus.ts +381 -0
- package/src/ui/run-snapshot-cache.ts +1057 -0
- package/src/ui/settings-overlay.ts +1067 -0
- package/src/ui/shared-overlay-scheduler.ts +96 -0
- package/src/ui/snapshot-types.ts +90 -0
- package/src/ui/spinner.ts +17 -0
- package/src/ui/status-colors.ts +143 -0
- package/src/ui/syntax-highlight.ts +136 -0
- package/src/ui/terminal-status.ts +269 -0
- package/src/ui/theme-adapter.ts +271 -0
- package/src/ui/theme-discovery.ts +199 -0
- package/src/ui/tool-progress-formatter.ts +93 -0
- package/src/ui/tool-renderers/brief-mode.ts +225 -0
- package/src/ui/tool-renderers/index.ts +726 -0
- package/src/ui/transcript-cache.ts +128 -0
- package/src/ui/transcript-entries.ts +256 -0
- package/src/ui/transcript-viewer.ts +424 -0
- package/src/ui/widget/index.ts +418 -0
- package/src/ui/widget/widget-formatters.ts +182 -0
- package/src/ui/widget/widget-model.ts +110 -0
- package/src/ui/widget/widget-renderer.ts +182 -0
- package/src/ui/widget/widget-types.ts +36 -0
- package/src/utils/bm25-search.ts +235 -0
- package/src/utils/completion-dedupe.ts +63 -0
- package/src/utils/conflict-detect.ts +668 -0
- package/src/utils/env-allowlist.ts +30 -0
- package/src/utils/env-filter.ts +155 -0
- package/src/utils/file-coalescer.ts +98 -0
- package/src/utils/fingerprint.ts +180 -0
- package/src/utils/frontmatter.ts +70 -0
- package/src/utils/fs-watch.ts +37 -0
- package/src/utils/gh-protocol.ts +556 -0
- package/src/utils/git.ts +260 -0
- package/src/utils/guards.ts +107 -0
- package/src/utils/ids.ts +24 -0
- package/src/utils/incremental-reader.ts +220 -0
- package/src/utils/internal-error.ts +10 -0
- package/src/utils/names.ts +36 -0
- package/src/utils/ndjson.ts +115 -0
- package/src/utils/paths.ts +227 -0
- package/src/utils/project-detector.ts +160 -0
- package/src/utils/redaction.ts +370 -0
- package/src/utils/resolve-shell.ts +36 -0
- package/src/utils/run-watcher-registry.ts +152 -0
- package/src/utils/safe-paths.ts +393 -0
- package/src/utils/scan-cache.ts +144 -0
- package/src/utils/session-utils.ts +110 -0
- package/src/utils/sleep.ts +54 -0
- package/src/utils/socket-path.ts +164 -0
- package/src/utils/sse-parser.ts +131 -0
- package/src/utils/task-name-generator.ts +337 -0
- package/src/utils/timings.ts +33 -0
- package/src/utils/token-counter.ts +200 -0
- package/src/utils/visual.ts +207 -0
- package/src/workflows/cost-estimator.ts +34 -0
- package/src/workflows/discover-workflows.ts +308 -0
- package/src/workflows/intermediate-store.ts +166 -0
- package/src/workflows/preflight-validator.ts +168 -0
- package/src/workflows/topology-analyzer.ts +203 -0
- package/src/workflows/validate-workflow.ts +50 -0
- package/src/workflows/workflow-config.ts +75 -0
- package/src/workflows/workflow-serializer.ts +32 -0
- package/src/worktree/branch-freshness.ts +112 -0
- package/src/worktree/cleanup.ts +341 -0
- 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
|
+
}
|