@ag-eco/agentplate-cli 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +462 -0
- package/agents/ap-co-creation.md +90 -0
- package/agents/builder.md +144 -0
- package/agents/coordinator.md +377 -0
- package/agents/lead.md +435 -0
- package/agents/merger.md +164 -0
- package/agents/monitor.md +214 -0
- package/agents/orchestrator.md +239 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +125 -0
- package/agents/supervisor.md +427 -0
- package/package.json +66 -0
- package/src/agents/capabilities.test.ts +85 -0
- package/src/agents/capabilities.ts +125 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/copilot-hooks-deployer.test.ts +162 -0
- package/src/agents/copilot-hooks-deployer.ts +93 -0
- package/src/agents/guard-rules.test.ts +372 -0
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/headless-mail-injector.test.ts +709 -0
- package/src/agents/headless-mail-injector.ts +377 -0
- package/src/agents/headless-prompt.test.ts +102 -0
- package/src/agents/headless-prompt.ts +68 -0
- package/src/agents/hooks-deployer.test.ts +3119 -0
- package/src/agents/hooks-deployer.ts +804 -0
- package/src/agents/identity.test.ts +604 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/mail-poll-detect.test.ts +153 -0
- package/src/agents/mail-poll-detect.ts +73 -0
- package/src/agents/manifest.test.ts +1026 -0
- package/src/agents/manifest.ts +376 -0
- package/src/agents/overlay.test.ts +1058 -0
- package/src/agents/overlay.ts +490 -0
- package/src/agents/scope-detect.test.ts +190 -0
- package/src/agents/scope-detect.ts +146 -0
- package/src/agents/turn-lock.test.ts +181 -0
- package/src/agents/turn-lock.ts +235 -0
- package/src/agents/turn-runner-dispatch.test.ts +182 -0
- package/src/agents/turn-runner-dispatch.ts +105 -0
- package/src/agents/turn-runner.test.ts +2312 -0
- package/src/agents/turn-runner.ts +1383 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +230 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +328 -0
- package/src/commands/agents.ts +299 -0
- package/src/commands/clean.test.ts +797 -0
- package/src/commands/clean.ts +791 -0
- package/src/commands/completions.test.ts +348 -0
- package/src/commands/completions.ts +981 -0
- package/src/commands/coordinator.test.ts +2975 -0
- package/src/commands/coordinator.ts +1841 -0
- package/src/commands/costs.test.ts +1183 -0
- package/src/commands/costs.ts +599 -0
- package/src/commands/dashboard.test.ts +954 -0
- package/src/commands/dashboard.ts +1212 -0
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.test.ts +303 -0
- package/src/commands/doctor.ts +311 -0
- package/src/commands/ecosystem.test.ts +226 -0
- package/src/commands/ecosystem.ts +248 -0
- package/src/commands/errors.test.ts +654 -0
- package/src/commands/errors.ts +197 -0
- package/src/commands/feed.test.ts +709 -0
- package/src/commands/feed.ts +260 -0
- package/src/commands/group.test.ts +475 -0
- package/src/commands/group.ts +546 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +263 -0
- package/src/commands/init.test.ts +1011 -0
- package/src/commands/init.ts +967 -0
- package/src/commands/inspect.test.ts +1239 -0
- package/src/commands/inspect.ts +648 -0
- package/src/commands/log.test.ts +1913 -0
- package/src/commands/log.ts +958 -0
- package/src/commands/logs.test.ts +801 -0
- package/src/commands/logs.ts +483 -0
- package/src/commands/mail.test.ts +1501 -0
- package/src/commands/mail.ts +848 -0
- package/src/commands/merge.test.ts +864 -0
- package/src/commands/merge.ts +381 -0
- package/src/commands/metrics.test.ts +458 -0
- package/src/commands/metrics.ts +129 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +409 -0
- package/src/commands/nudge.test.ts +579 -0
- package/src/commands/nudge.ts +646 -0
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +612 -0
- package/src/commands/prime.ts +359 -0
- package/src/commands/replay.test.ts +757 -0
- package/src/commands/replay.ts +231 -0
- package/src/commands/run.test.ts +469 -0
- package/src/commands/run.ts +353 -0
- package/src/commands/serve/agent-actions.test.ts +210 -0
- package/src/commands/serve/agent-actions.ts +192 -0
- package/src/commands/serve/build.test.ts +202 -0
- package/src/commands/serve/build.ts +206 -0
- package/src/commands/serve/coordinator-actions.test.ts +339 -0
- package/src/commands/serve/coordinator-actions.ts +410 -0
- package/src/commands/serve/dev.test.ts +168 -0
- package/src/commands/serve/dev.ts +117 -0
- package/src/commands/serve/mail-actions.test.ts +312 -0
- package/src/commands/serve/mail-actions.ts +167 -0
- package/src/commands/serve/rest.test.ts +1680 -0
- package/src/commands/serve/rest.ts +1130 -0
- package/src/commands/serve/static.ts +51 -0
- package/src/commands/serve/ws.test.ts +361 -0
- package/src/commands/serve/ws.ts +332 -0
- package/src/commands/serve.test.ts +459 -0
- package/src/commands/serve.ts +654 -0
- package/src/commands/sling.test.ts +1583 -0
- package/src/commands/sling.ts +1351 -0
- package/src/commands/spec.test.ts +179 -0
- package/src/commands/spec.ts +105 -0
- package/src/commands/status.test.ts +614 -0
- package/src/commands/status.ts +403 -0
- package/src/commands/stop.test.ts +964 -0
- package/src/commands/stop.ts +319 -0
- package/src/commands/supervisor.test.ts +185 -0
- package/src/commands/supervisor.ts +537 -0
- package/src/commands/trace.test.ts +762 -0
- package/src/commands/trace.ts +205 -0
- package/src/commands/update.test.ts +466 -0
- package/src/commands/update.ts +263 -0
- package/src/commands/upgrade.test.ts +48 -0
- package/src/commands/upgrade.ts +240 -0
- package/src/commands/watch.test.ts +257 -0
- package/src/commands/watch.ts +308 -0
- package/src/commands/worktree.test.ts +1297 -0
- package/src/commands/worktree.ts +451 -0
- package/src/config.test.ts +1535 -0
- package/src/config.ts +1064 -0
- package/src/doctor/agents.test.ts +523 -0
- package/src/doctor/agents.ts +399 -0
- package/src/doctor/config-check.test.ts +191 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +807 -0
- package/src/doctor/consistency.ts +347 -0
- package/src/doctor/databases.test.ts +350 -0
- package/src/doctor/databases.ts +243 -0
- package/src/doctor/dependencies.test.ts +296 -0
- package/src/doctor/dependencies.ts +272 -0
- package/src/doctor/ecosystem.test.ts +308 -0
- package/src/doctor/ecosystem.ts +156 -0
- package/src/doctor/logs.test.ts +253 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +315 -0
- package/src/doctor/merge-queue.ts +167 -0
- package/src/doctor/providers.test.ts +409 -0
- package/src/doctor/providers.ts +250 -0
- package/src/doctor/serve.test.ts +95 -0
- package/src/doctor/serve.ts +86 -0
- package/src/doctor/structure.test.ts +423 -0
- package/src/doctor/structure.ts +285 -0
- package/src/doctor/types.ts +43 -0
- package/src/doctor/version.test.ts +241 -0
- package/src/doctor/version.ts +132 -0
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +214 -0
- package/src/e2e/init-sling-lifecycle.test.ts +283 -0
- package/src/errors.test.ts +350 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tailer.test.ts +719 -0
- package/src/events/tailer.ts +332 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +533 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/insights/quality-gates.test.ts +141 -0
- package/src/insights/quality-gates.ts +156 -0
- package/src/json.test.ts +72 -0
- package/src/json.ts +53 -0
- package/src/loam/client.test.ts +752 -0
- package/src/loam/client.ts +664 -0
- package/src/logging/color.test.ts +252 -0
- package/src/logging/color.ts +105 -0
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +255 -0
- package/src/logging/logger.test.ts +814 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +110 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/logging/theme.ts +140 -0
- package/src/mail/broadcast.test.ts +204 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +774 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +898 -0
- package/src/mail/store.ts +425 -0
- package/src/merge/lock.test.ts +149 -0
- package/src/merge/lock.ts +140 -0
- package/src/merge/predict.test.ts +387 -0
- package/src/merge/predict.ts +249 -0
- package/src/merge/queue.test.ts +426 -0
- package/src/merge/queue.ts +246 -0
- package/src/merge/resolver.test.ts +1993 -0
- package/src/merge/resolver.ts +926 -0
- package/src/metrics/pricing.test.ts +258 -0
- package/src/metrics/pricing.ts +135 -0
- package/src/metrics/store.test.ts +978 -0
- package/src/metrics/store.ts +501 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +483 -0
- package/src/metrics/transcript.ts +114 -0
- package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +1474 -0
- package/src/runtimes/claude.ts +579 -0
- package/src/runtimes/codex.test.ts +805 -0
- package/src/runtimes/codex.ts +273 -0
- package/src/runtimes/connections.test.ts +214 -0
- package/src/runtimes/connections.ts +103 -0
- package/src/runtimes/copilot.test.ts +707 -0
- package/src/runtimes/copilot.ts +316 -0
- package/src/runtimes/cursor.test.ts +497 -0
- package/src/runtimes/cursor.ts +205 -0
- package/src/runtimes/gemini.test.ts +537 -0
- package/src/runtimes/gemini.ts +243 -0
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/headless-connection.test.ts +264 -0
- package/src/runtimes/headless-connection.ts +158 -0
- package/src/runtimes/opencode.test.ts +325 -0
- package/src/runtimes/opencode.ts +188 -0
- package/src/runtimes/pi-guards.test.ts +486 -0
- package/src/runtimes/pi-guards.ts +367 -0
- package/src/runtimes/pi.test.ts +789 -0
- package/src/runtimes/pi.ts +305 -0
- package/src/runtimes/registry.test.ts +196 -0
- package/src/runtimes/registry.ts +99 -0
- package/src/runtimes/sapling.test.ts +1267 -0
- package/src/runtimes/sapling.ts +710 -0
- package/src/runtimes/types.ts +266 -0
- package/src/schema-consistency.test.ts +246 -0
- package/src/sessions/compat.test.ts +281 -0
- package/src/sessions/compat.ts +105 -0
- package/src/sessions/store.test.ts +1748 -0
- package/src/sessions/store.ts +858 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +145 -0
- package/src/test-setup.test.ts +31 -0
- package/src/test-setup.ts +28 -0
- package/src/tools/loam/api.ts +368 -0
- package/src/tools/loam/cli.ts +278 -0
- package/src/tools/loam/commands/add.ts +52 -0
- package/src/tools/loam/commands/archive.ts +214 -0
- package/src/tools/loam/commands/audit.ts +276 -0
- package/src/tools/loam/commands/compact.ts +1062 -0
- package/src/tools/loam/commands/completions.ts +79 -0
- package/src/tools/loam/commands/config.ts +381 -0
- package/src/tools/loam/commands/delete-domain.ts +121 -0
- package/src/tools/loam/commands/delete.ts +316 -0
- package/src/tools/loam/commands/diff.ts +200 -0
- package/src/tools/loam/commands/doctor.ts +1113 -0
- package/src/tools/loam/commands/edit.ts +226 -0
- package/src/tools/loam/commands/init.ts +31 -0
- package/src/tools/loam/commands/learn.ts +179 -0
- package/src/tools/loam/commands/move.ts +323 -0
- package/src/tools/loam/commands/onboard.ts +374 -0
- package/src/tools/loam/commands/outcome.ts +185 -0
- package/src/tools/loam/commands/prime.ts +688 -0
- package/src/tools/loam/commands/prune.ts +614 -0
- package/src/tools/loam/commands/query.ts +218 -0
- package/src/tools/loam/commands/rank.ts +180 -0
- package/src/tools/loam/commands/ready.ts +189 -0
- package/src/tools/loam/commands/record.ts +1210 -0
- package/src/tools/loam/commands/restore.ts +166 -0
- package/src/tools/loam/commands/search.ts +327 -0
- package/src/tools/loam/commands/setup.ts +887 -0
- package/src/tools/loam/commands/status.ts +103 -0
- package/src/tools/loam/commands/sync.ts +298 -0
- package/src/tools/loam/commands/update.ts +19 -0
- package/src/tools/loam/commands/upgrade.ts +93 -0
- package/src/tools/loam/commands/validate.ts +190 -0
- package/src/tools/loam/index.ts +62 -0
- package/src/tools/loam/log.ts +127 -0
- package/src/tools/loam/registry/builtins.ts +409 -0
- package/src/tools/loam/registry/custom.ts +431 -0
- package/src/tools/loam/registry/init.ts +55 -0
- package/src/tools/loam/registry/template.ts +40 -0
- package/src/tools/loam/registry/type-registry.ts +113 -0
- package/src/tools/loam/schemas/config-schema.ts +489 -0
- package/src/tools/loam/schemas/config.ts +245 -0
- package/src/tools/loam/schemas/index.ts +18 -0
- package/src/tools/loam/schemas/record-schema.ts +191 -0
- package/src/tools/loam/schemas/record.ts +115 -0
- package/src/tools/loam/utils/active-work.ts +205 -0
- package/src/tools/loam/utils/anchor-validity.ts +80 -0
- package/src/tools/loam/utils/archive.ts +146 -0
- package/src/tools/loam/utils/audit.ts +667 -0
- package/src/tools/loam/utils/bm25.ts +238 -0
- package/src/tools/loam/utils/budget.ts +142 -0
- package/src/tools/loam/utils/config.ts +344 -0
- package/src/tools/loam/utils/dir-anchors.ts +62 -0
- package/src/tools/loam/utils/domain-rules.ts +114 -0
- package/src/tools/loam/utils/expertise.ts +393 -0
- package/src/tools/loam/utils/format-helpers.ts +96 -0
- package/src/tools/loam/utils/format.ts +1234 -0
- package/src/tools/loam/utils/git-context.ts +50 -0
- package/src/tools/loam/utils/git.ts +183 -0
- package/src/tools/loam/utils/hooks.ts +299 -0
- package/src/tools/loam/utils/index.ts +52 -0
- package/src/tools/loam/utils/json-output.ts +13 -0
- package/src/tools/loam/utils/lock.ts +76 -0
- package/src/tools/loam/utils/markers.ts +48 -0
- package/src/tools/loam/utils/numeric-flags.ts +20 -0
- package/src/tools/loam/utils/palette.ts +44 -0
- package/src/tools/loam/utils/prime-ranking.ts +135 -0
- package/src/tools/loam/utils/recipe-discovery.ts +195 -0
- package/src/tools/loam/utils/runtime-flags.ts +28 -0
- package/src/tools/loam/utils/scoring.ts +94 -0
- package/src/tools/loam/utils/version.ts +116 -0
- package/src/tools/sprout/commands/block.ts +64 -0
- package/src/tools/sprout/commands/blocked.ts +86 -0
- package/src/tools/sprout/commands/close.ts +129 -0
- package/src/tools/sprout/commands/completions.ts +198 -0
- package/src/tools/sprout/commands/config.ts +238 -0
- package/src/tools/sprout/commands/create.ts +164 -0
- package/src/tools/sprout/commands/dep.ts +148 -0
- package/src/tools/sprout/commands/doctor.ts +979 -0
- package/src/tools/sprout/commands/init.ts +83 -0
- package/src/tools/sprout/commands/label.ts +178 -0
- package/src/tools/sprout/commands/list.ts +210 -0
- package/src/tools/sprout/commands/migrate.ts +133 -0
- package/src/tools/sprout/commands/onboard.ts +207 -0
- package/src/tools/sprout/commands/plan-show.ts +278 -0
- package/src/tools/sprout/commands/plan.ts +2526 -0
- package/src/tools/sprout/commands/prime.ts +399 -0
- package/src/tools/sprout/commands/ready.ts +245 -0
- package/src/tools/sprout/commands/search.ts +221 -0
- package/src/tools/sprout/commands/show.ts +277 -0
- package/src/tools/sprout/commands/stats.ts +146 -0
- package/src/tools/sprout/commands/sync.ts +134 -0
- package/src/tools/sprout/commands/tpl.ts +364 -0
- package/src/tools/sprout/commands/unblock.ts +115 -0
- package/src/tools/sprout/commands/update.ts +257 -0
- package/src/tools/sprout/commands/upgrade.ts +91 -0
- package/src/tools/sprout/config-schema.ts +152 -0
- package/src/tools/sprout/config.ts +355 -0
- package/src/tools/sprout/filter.ts +107 -0
- package/src/tools/sprout/format.ts +43 -0
- package/src/tools/sprout/id.ts +22 -0
- package/src/tools/sprout/index.ts +204 -0
- package/src/tools/sprout/log.ts +76 -0
- package/src/tools/sprout/markers.ts +22 -0
- package/src/tools/sprout/output.ts +121 -0
- package/src/tools/sprout/plan-backref.ts +93 -0
- package/src/tools/sprout/plan-context.ts +81 -0
- package/src/tools/sprout/plan-domain.ts +139 -0
- package/src/tools/sprout/plan-lifecycle.ts +65 -0
- package/src/tools/sprout/plan-loam.ts +207 -0
- package/src/tools/sprout/plan-schema.ts +209 -0
- package/src/tools/sprout/sort.ts +31 -0
- package/src/tools/sprout/store.ts +172 -0
- package/src/tools/sprout/types.ts +118 -0
- package/src/tools/sprout/validation.ts +119 -0
- package/src/tools/sprout/version.ts +1 -0
- package/src/tools/sprout/yaml.ts +387 -0
- package/src/tools/trellis/commands/archive.ts +87 -0
- package/src/tools/trellis/commands/completions.ts +610 -0
- package/src/tools/trellis/commands/config.ts +382 -0
- package/src/tools/trellis/commands/create.ts +252 -0
- package/src/tools/trellis/commands/diff.ts +150 -0
- package/src/tools/trellis/commands/doctor.ts +771 -0
- package/src/tools/trellis/commands/emit.ts +365 -0
- package/src/tools/trellis/commands/history.ts +83 -0
- package/src/tools/trellis/commands/import.ts +198 -0
- package/src/tools/trellis/commands/init.ts +81 -0
- package/src/tools/trellis/commands/list.ts +103 -0
- package/src/tools/trellis/commands/onboard.ts +156 -0
- package/src/tools/trellis/commands/pin.ts +172 -0
- package/src/tools/trellis/commands/prime.ts +193 -0
- package/src/tools/trellis/commands/render.ts +122 -0
- package/src/tools/trellis/commands/schema.ts +353 -0
- package/src/tools/trellis/commands/show.ts +115 -0
- package/src/tools/trellis/commands/stats.ts +65 -0
- package/src/tools/trellis/commands/sync.ts +112 -0
- package/src/tools/trellis/commands/tree.ts +123 -0
- package/src/tools/trellis/commands/update.ts +330 -0
- package/src/tools/trellis/commands/upgrade.ts +95 -0
- package/src/tools/trellis/commands/validate.ts +166 -0
- package/src/tools/trellis/config-schema.ts +81 -0
- package/src/tools/trellis/config.ts +108 -0
- package/src/tools/trellis/frontmatter.ts +348 -0
- package/src/tools/trellis/id.ts +24 -0
- package/src/tools/trellis/index.ts +209 -0
- package/src/tools/trellis/markers.ts +28 -0
- package/src/tools/trellis/output.ts +84 -0
- package/src/tools/trellis/render.ts +212 -0
- package/src/tools/trellis/store.ts +144 -0
- package/src/tools/trellis/types.ts +82 -0
- package/src/tools/trellis/validate.ts +199 -0
- package/src/tools/trellis/yaml.ts +309 -0
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +90 -0
- package/src/tracker/factory.ts +65 -0
- package/src/tracker/sprout.test.ts +461 -0
- package/src/tracker/sprout.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/trellis/client.test.ts +107 -0
- package/src/trellis/client.ts +179 -0
- package/src/types.ts +970 -0
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/browser.test.ts +49 -0
- package/src/utils/browser.ts +48 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +152 -0
- package/src/utils/pid.ts +130 -0
- package/src/utils/process-scan.test.ts +53 -0
- package/src/utils/process-scan.ts +76 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/version.ts +5 -0
- package/src/watchdog/daemon.test.ts +3721 -0
- package/src/watchdog/daemon.ts +1257 -0
- package/src/watchdog/health.test.ts +830 -0
- package/src/watchdog/health.ts +434 -0
- package/src/watchdog/triage.test.ts +205 -0
- package/src/watchdog/triage.ts +205 -0
- package/src/worktree/manager.test.ts +720 -0
- package/src/worktree/manager.ts +405 -0
- package/src/worktree/process.test.ts +172 -0
- package/src/worktree/process.ts +131 -0
- package/src/worktree/tmux.test.ts +1616 -0
- package/src/worktree/tmux.ts +721 -0
- package/templates/CLAUDE.md.tmpl +100 -0
- package/templates/copilot-hooks.json.tmpl +13 -0
- package/templates/hooks.json.tmpl +109 -0
- package/templates/overlay.md.tmpl +88 -0
- package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
- package/ui/dist/chunk-8s31f05k.css +1 -0
- package/ui/dist/chunk-vm5rz679.js +300 -0
- package/ui/dist/favicon-nzb39vza.svg +4 -0
- package/ui/dist/index.html +17 -0
|
@@ -0,0 +1,1212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: ap dashboard [--interval <ms>] [--all]
|
|
3
|
+
*
|
|
4
|
+
* Rich terminal dashboard using raw ANSI escape codes (zero runtime deps).
|
|
5
|
+
* Polls existing data sources and renders multi-panel layout with agent status,
|
|
6
|
+
* mail activity, merge queue, metrics, tasks, and recent event feed.
|
|
7
|
+
*
|
|
8
|
+
* Layout:
|
|
9
|
+
* Row 1-2: Header
|
|
10
|
+
* Row 3-N: Agents (60% width, dynamic height) | Tasks (upper-right 40%) + Feed (lower-right 40%)
|
|
11
|
+
* Row N+1: Mail (50%) | Merge Queue (50%)
|
|
12
|
+
* Row M: Metrics
|
|
13
|
+
*
|
|
14
|
+
* By default, all panels are scoped to the current run (current-run.txt).
|
|
15
|
+
* Use --all to show data across all runs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { join, resolve } from "node:path";
|
|
20
|
+
import { Command } from "commander";
|
|
21
|
+
import { loadConfig } from "../config.ts";
|
|
22
|
+
import { ValidationError } from "../errors.ts";
|
|
23
|
+
import { createEventStore } from "../events/store.ts";
|
|
24
|
+
import { accent, brand, color, visibleLength } from "../logging/color.ts";
|
|
25
|
+
import {
|
|
26
|
+
buildAgentColorMap,
|
|
27
|
+
extendAgentColorMap,
|
|
28
|
+
formatDuration,
|
|
29
|
+
formatEventLine,
|
|
30
|
+
formatRelativeTime,
|
|
31
|
+
mergeStatusColor,
|
|
32
|
+
numericPriorityColor,
|
|
33
|
+
priorityColor,
|
|
34
|
+
} from "../logging/format.ts";
|
|
35
|
+
import { stateColor, stateIcon } from "../logging/theme.ts";
|
|
36
|
+
import { createMailStore, type MailStore } from "../mail/store.ts";
|
|
37
|
+
import { createMergeQueue, type MergeQueue } from "../merge/queue.ts";
|
|
38
|
+
import { createMetricsStore, type MetricsStore } from "../metrics/store.ts";
|
|
39
|
+
import { openSessionStore } from "../sessions/compat.ts";
|
|
40
|
+
import type { SessionStore } from "../sessions/store.ts";
|
|
41
|
+
import { createTrackerClient, resolveBackend } from "../tracker/factory.ts";
|
|
42
|
+
import type { TrackerIssue } from "../tracker/types.ts";
|
|
43
|
+
import type {
|
|
44
|
+
AgentplateConfig,
|
|
45
|
+
AgentSession,
|
|
46
|
+
EventStore,
|
|
47
|
+
MailMessage,
|
|
48
|
+
StoredEvent,
|
|
49
|
+
TaskTrackerBackend,
|
|
50
|
+
} from "../types.ts";
|
|
51
|
+
import { openBrowser } from "../utils/browser.ts";
|
|
52
|
+
import { evaluateHealth } from "../watchdog/health.ts";
|
|
53
|
+
import { isProcessAlive } from "../worktree/tmux.ts";
|
|
54
|
+
import { DEFAULT_SERVE_PORT, runServe } from "./serve.ts";
|
|
55
|
+
import { getCachedTmuxSessions, getCachedWorktrees, type StatusData } from "./status.ts";
|
|
56
|
+
|
|
57
|
+
const pkgPath = resolve(import.meta.dir, "../../package.json");
|
|
58
|
+
const PKG_VERSION: string = JSON.parse(await Bun.file(pkgPath).text()).version ?? "unknown";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Terminal control codes (cursor movement, screen clearing).
|
|
62
|
+
* These are not colors, so they stay separate from the color module.
|
|
63
|
+
*/
|
|
64
|
+
const CURSOR = {
|
|
65
|
+
clear: "\x1b[H\x1b[J", // Home cursor then clear from cursor to end
|
|
66
|
+
home: "\x1b[H", // Home cursor only (for redraw without full clear)
|
|
67
|
+
cursorTo: (row: number, col: number) => `\x1b[${row};${col}H`,
|
|
68
|
+
hideCursor: "\x1b[?25l",
|
|
69
|
+
showCursor: "\x1b[?25h",
|
|
70
|
+
enterAltScreen: "\x1b[?1049h", // Enter alternate screen buffer
|
|
71
|
+
leaveAltScreen: "\x1b[?1049l", // Leave alternate screen buffer
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Box drawing characters for panel borders (plain — not used for rendering,
|
|
76
|
+
* kept for backward compat with tests and horizontalLine helper).
|
|
77
|
+
*/
|
|
78
|
+
const BOX = {
|
|
79
|
+
topLeft: "┌",
|
|
80
|
+
topRight: "┐",
|
|
81
|
+
bottomLeft: "└",
|
|
82
|
+
bottomRight: "┘",
|
|
83
|
+
horizontal: "─",
|
|
84
|
+
vertical: "│",
|
|
85
|
+
tee: "├",
|
|
86
|
+
teeRight: "┤",
|
|
87
|
+
cross: "┼",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Dimmed version of BOX characters — for subdued borders that do not
|
|
92
|
+
* compete visually with panel content.
|
|
93
|
+
*/
|
|
94
|
+
export const dimBox = {
|
|
95
|
+
topLeft: color.dim("┌"),
|
|
96
|
+
topRight: color.dim("┐"),
|
|
97
|
+
bottomLeft: color.dim("└"),
|
|
98
|
+
bottomRight: color.dim("┘"),
|
|
99
|
+
horizontal: color.dim("─"),
|
|
100
|
+
vertical: color.dim("│"),
|
|
101
|
+
tee: color.dim("├"),
|
|
102
|
+
teeRight: color.dim("┤"),
|
|
103
|
+
cross: color.dim("┼"),
|
|
104
|
+
} as const;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Truncate a string to fit within maxLen characters, adding ellipsis if needed.
|
|
108
|
+
*/
|
|
109
|
+
function truncate(str: string, maxLen: number): string {
|
|
110
|
+
if (maxLen <= 0) return "";
|
|
111
|
+
if (str.length <= maxLen) return str;
|
|
112
|
+
return `${str.slice(0, maxLen - 1)}…`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Pad or truncate a string to exactly the given width.
|
|
117
|
+
*/
|
|
118
|
+
function pad(str: string, width: number): string {
|
|
119
|
+
if (width <= 0) return "";
|
|
120
|
+
if (str.length >= width) return str.slice(0, width);
|
|
121
|
+
return str + " ".repeat(width - str.length);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Draw a horizontal line with left/right connectors using plain BOX chars.
|
|
126
|
+
* Exported for backward compat in tests.
|
|
127
|
+
*/
|
|
128
|
+
function horizontalLine(width: number, left: string, _middle: string, right: string): string {
|
|
129
|
+
return left + BOX.horizontal.repeat(Math.max(0, width - 2)) + right;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Draw a horizontal line using dimmed border characters.
|
|
134
|
+
* ANSI-aware: uses visibleLength() for padding calculations.
|
|
135
|
+
*/
|
|
136
|
+
function dimHorizontalLine(width: number, left: string, right: string): string {
|
|
137
|
+
const fillCount = Math.max(0, width - visibleLength(left) - visibleLength(right));
|
|
138
|
+
return left + dimBox.horizontal.repeat(fillCount) + right;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { pad, truncate, horizontalLine };
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compute agent panel height from screen height and agent count.
|
|
145
|
+
* min 8 rows, max floor(height * 0.35), grows with agent count (+4 for chrome).
|
|
146
|
+
*/
|
|
147
|
+
export function computeAgentPanelHeight(height: number, agentCount: number): number {
|
|
148
|
+
return Math.max(8, Math.min(Math.floor(height * 0.35), agentCount + 4));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Filter agents by run ID. When run-scoped, also includes sessions with null
|
|
153
|
+
* runId (e.g. coordinator) because SQL WHERE run_id = ? never matches NULL.
|
|
154
|
+
*/
|
|
155
|
+
export function filterAgentsByRun<T extends { runId: string | null }>(
|
|
156
|
+
agents: T[],
|
|
157
|
+
runId: string | null | undefined,
|
|
158
|
+
): T[] {
|
|
159
|
+
if (!runId) return agents;
|
|
160
|
+
return agents.filter((a) => a.runId === runId || a.runId === null);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Pre-opened database handles for the dashboard poll loop.
|
|
165
|
+
* Stores are opened once and reused across ticks to avoid
|
|
166
|
+
* repeated open/close/PRAGMA/WAL checkpoint overhead.
|
|
167
|
+
*/
|
|
168
|
+
export interface DashboardStores {
|
|
169
|
+
sessionStore: SessionStore;
|
|
170
|
+
mailStore: MailStore | null;
|
|
171
|
+
mergeQueue: MergeQueue | null;
|
|
172
|
+
metricsStore: MetricsStore | null;
|
|
173
|
+
eventStore: EventStore | null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Open all database connections needed by the dashboard.
|
|
178
|
+
* Returns null handles for databases that do not exist on disk.
|
|
179
|
+
*/
|
|
180
|
+
export function openDashboardStores(root: string): DashboardStores {
|
|
181
|
+
const agentplateDir = join(root, ".agentplate");
|
|
182
|
+
const { store: sessionStore } = openSessionStore(agentplateDir);
|
|
183
|
+
|
|
184
|
+
let mailStore: MailStore | null = null;
|
|
185
|
+
try {
|
|
186
|
+
const mailDbPath = join(agentplateDir, "mail.db");
|
|
187
|
+
if (existsSync(mailDbPath)) {
|
|
188
|
+
mailStore = createMailStore(mailDbPath);
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// mail db might not be openable
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let mergeQueue: MergeQueue | null = null;
|
|
195
|
+
try {
|
|
196
|
+
const queuePath = join(agentplateDir, "merge-queue.db");
|
|
197
|
+
if (existsSync(queuePath)) {
|
|
198
|
+
mergeQueue = createMergeQueue(queuePath);
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// queue db might not be openable
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let metricsStore: MetricsStore | null = null;
|
|
205
|
+
try {
|
|
206
|
+
const metricsDbPath = join(agentplateDir, "metrics.db");
|
|
207
|
+
if (existsSync(metricsDbPath)) {
|
|
208
|
+
metricsStore = createMetricsStore(metricsDbPath);
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// metrics db might not be openable
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let eventStore: EventStore | null = null;
|
|
215
|
+
try {
|
|
216
|
+
const eventsDbPath = join(agentplateDir, "events.db");
|
|
217
|
+
if (existsSync(eventsDbPath)) {
|
|
218
|
+
eventStore = createEventStore(eventsDbPath);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// events db might not be openable
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { sessionStore, mailStore, mergeQueue, metricsStore, eventStore };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Close all dashboard database connections.
|
|
229
|
+
*/
|
|
230
|
+
export function closeDashboardStores(stores: DashboardStores): void {
|
|
231
|
+
try {
|
|
232
|
+
stores.sessionStore.close();
|
|
233
|
+
} catch {
|
|
234
|
+
/* best effort */
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
stores.mailStore?.close();
|
|
238
|
+
} catch {
|
|
239
|
+
/* best effort */
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
stores.mergeQueue?.close();
|
|
243
|
+
} catch {
|
|
244
|
+
/* best effort */
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
stores.metricsStore?.close();
|
|
248
|
+
} catch {
|
|
249
|
+
/* best effort */
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
stores.eventStore?.close();
|
|
253
|
+
} catch {
|
|
254
|
+
/* best effort */
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Rolling event buffer with incremental dedup by lastSeenId.
|
|
260
|
+
* Maintains a fixed-size window of the most recent events.
|
|
261
|
+
*/
|
|
262
|
+
export class EventBuffer {
|
|
263
|
+
private events: StoredEvent[] = [];
|
|
264
|
+
private lastSeenId = 0;
|
|
265
|
+
private colorMap: Map<string, (s: string) => string> = new Map();
|
|
266
|
+
private readonly maxSize: number;
|
|
267
|
+
|
|
268
|
+
constructor(maxSize = 100) {
|
|
269
|
+
this.maxSize = maxSize;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
poll(eventStore: EventStore): void {
|
|
273
|
+
const since = new Date(Date.now() - 60 * 1000).toISOString();
|
|
274
|
+
const allEvents = eventStore.getTimeline({ since, limit: 1000 });
|
|
275
|
+
const newEvents = allEvents.filter((e) => e.id > this.lastSeenId);
|
|
276
|
+
|
|
277
|
+
if (newEvents.length === 0) return;
|
|
278
|
+
|
|
279
|
+
extendAgentColorMap(this.colorMap, newEvents);
|
|
280
|
+
this.events = [...this.events, ...newEvents].slice(-this.maxSize);
|
|
281
|
+
|
|
282
|
+
const lastEvent = newEvents[newEvents.length - 1];
|
|
283
|
+
if (lastEvent) {
|
|
284
|
+
this.lastSeenId = lastEvent.id;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
getEvents(): StoredEvent[] {
|
|
289
|
+
return this.events;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
getColorMap(): Map<string, (s: string) => string> {
|
|
293
|
+
return this.colorMap;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
get size(): number {
|
|
297
|
+
return this.events.length;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Tracker data cached between dashboard ticks (10s TTL). */
|
|
302
|
+
interface TrackerCache {
|
|
303
|
+
tasks: TrackerIssue[];
|
|
304
|
+
fetchedAt: number; // Date.now() ms
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Module-level tracker cache (persists across poll ticks). */
|
|
308
|
+
let trackerCache: TrackerCache | null = null;
|
|
309
|
+
const TRACKER_CACHE_TTL_MS = 10_000; // 10 seconds
|
|
310
|
+
|
|
311
|
+
/** Session data cached between ticks — stale-on-error fallback. */
|
|
312
|
+
interface SessionDataCache {
|
|
313
|
+
sessions: AgentSession[];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Module-level session cache (persists across poll ticks, used as fallback on SQLite errors). */
|
|
317
|
+
let sessionDataCache: SessionDataCache | null = null;
|
|
318
|
+
|
|
319
|
+
interface DashboardData {
|
|
320
|
+
currentRunId?: string | null;
|
|
321
|
+
status: StatusData;
|
|
322
|
+
recentMail: MailMessage[];
|
|
323
|
+
mergeQueue: Array<{ branchName: string; agentName: string; status: string }>;
|
|
324
|
+
metrics: {
|
|
325
|
+
totalSessions: number;
|
|
326
|
+
avgDuration: number;
|
|
327
|
+
byCapability: Record<string, number>;
|
|
328
|
+
};
|
|
329
|
+
tasks: TrackerIssue[];
|
|
330
|
+
recentEvents: StoredEvent[];
|
|
331
|
+
feedColorMap: Map<string, (s: string) => string>;
|
|
332
|
+
/** Runtime config for resolving per-capability runtime names in the agent panel. */
|
|
333
|
+
runtimeConfig?: AgentplateConfig["runtime"];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Read the current run ID from current-run.txt, or null if no active run.
|
|
338
|
+
*/
|
|
339
|
+
async function readCurrentRunId(agentplateDir: string): Promise<string | null> {
|
|
340
|
+
const path = join(agentplateDir, "current-run.txt");
|
|
341
|
+
const file = Bun.file(path);
|
|
342
|
+
if (!(await file.exists())) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const text = await file.text();
|
|
346
|
+
const trimmed = text.trim();
|
|
347
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Load all data sources for the dashboard using pre-opened store handles.
|
|
352
|
+
* When runId is provided, all panels are scoped to agents in that run.
|
|
353
|
+
* No stores are opened or closed here — that is the caller's responsibility.
|
|
354
|
+
*/
|
|
355
|
+
async function loadDashboardData(
|
|
356
|
+
root: string,
|
|
357
|
+
stores: DashboardStores,
|
|
358
|
+
runId?: string | null,
|
|
359
|
+
thresholds?: { staleMs: number; zombieMs: number },
|
|
360
|
+
eventBuffer?: EventBuffer,
|
|
361
|
+
runtimeConfig?: AgentplateConfig["runtime"],
|
|
362
|
+
taskTrackerBackend?: TaskTrackerBackend,
|
|
363
|
+
): Promise<DashboardData> {
|
|
364
|
+
// Get all sessions from the pre-opened session store — fall back to cache on SQLite errors.
|
|
365
|
+
let allSessions: AgentSession[];
|
|
366
|
+
try {
|
|
367
|
+
allSessions = stores.sessionStore.getAll();
|
|
368
|
+
sessionDataCache = { sessions: allSessions };
|
|
369
|
+
} catch {
|
|
370
|
+
// SQLite lock contention or I/O error — use last known sessions
|
|
371
|
+
allSessions = sessionDataCache?.sessions ?? [];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Get worktrees and tmux sessions via cached subprocess helpers
|
|
375
|
+
const worktrees = await getCachedWorktrees(root);
|
|
376
|
+
const tmuxSessions = await getCachedTmuxSessions();
|
|
377
|
+
|
|
378
|
+
// Evaluate health for active agents using the same logic as the watchdog.
|
|
379
|
+
const tmuxSessionNames = new Set(tmuxSessions.map((s) => s.name));
|
|
380
|
+
const healthThresholds = thresholds ?? { staleMs: 300_000, zombieMs: 600_000 };
|
|
381
|
+
try {
|
|
382
|
+
for (const session of allSessions) {
|
|
383
|
+
if (session.state === "completed") continue;
|
|
384
|
+
const tmuxAlive = tmuxSessionNames.has(session.tmuxSession);
|
|
385
|
+
const check = evaluateHealth(session, tmuxAlive, healthThresholds);
|
|
386
|
+
if (check.state !== session.state) {
|
|
387
|
+
try {
|
|
388
|
+
stores.sessionStore.updateState(session.agentName, check.state);
|
|
389
|
+
session.state = check.state;
|
|
390
|
+
} catch {
|
|
391
|
+
// Best effort: don't fail dashboard if update fails
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
// Best effort: evaluateHealth loop should not crash the dashboard
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// If run-scoped, filter agents to only those belonging to the current run.
|
|
400
|
+
const filteredAgents = filterAgentsByRun(allSessions, runId);
|
|
401
|
+
|
|
402
|
+
// Count unread mail
|
|
403
|
+
let unreadMailCount = 0;
|
|
404
|
+
if (stores.mailStore) {
|
|
405
|
+
try {
|
|
406
|
+
const unread = stores.mailStore.getAll({ to: "orchestrator", unread: true });
|
|
407
|
+
unreadMailCount = unread.length;
|
|
408
|
+
} catch {
|
|
409
|
+
// best effort
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Count merge queue pending entries
|
|
414
|
+
let mergeQueueCount = 0;
|
|
415
|
+
if (stores.mergeQueue) {
|
|
416
|
+
try {
|
|
417
|
+
mergeQueueCount = stores.mergeQueue.list("pending").length;
|
|
418
|
+
} catch {
|
|
419
|
+
// best effort
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Count recent metrics sessions
|
|
424
|
+
let recentMetricsCount = 0;
|
|
425
|
+
if (stores.metricsStore) {
|
|
426
|
+
try {
|
|
427
|
+
recentMetricsCount = stores.metricsStore.countSessions();
|
|
428
|
+
} catch {
|
|
429
|
+
// best effort
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const status: StatusData = {
|
|
434
|
+
currentRunId: runId,
|
|
435
|
+
agents: filteredAgents,
|
|
436
|
+
worktrees,
|
|
437
|
+
tmuxSessions,
|
|
438
|
+
unreadMailCount,
|
|
439
|
+
unreadMailScope: "orchestrator",
|
|
440
|
+
mergeQueueCount,
|
|
441
|
+
recentMetricsCount,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// Load recent mail from pre-opened mail store
|
|
445
|
+
let recentMail: MailMessage[] = [];
|
|
446
|
+
if (stores.mailStore) {
|
|
447
|
+
try {
|
|
448
|
+
if (runId && filteredAgents.length > 0) {
|
|
449
|
+
const agentNames = new Set(filteredAgents.map((a) => a.agentName));
|
|
450
|
+
const allMail = stores.mailStore.getAll({ limit: 50 });
|
|
451
|
+
recentMail = allMail
|
|
452
|
+
.filter((m) => agentNames.has(m.from) || agentNames.has(m.to))
|
|
453
|
+
.slice(0, 5);
|
|
454
|
+
} else {
|
|
455
|
+
recentMail = stores.mailStore.getAll({ limit: 5 });
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
// best effort
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Load merge queue entries from pre-opened merge queue
|
|
463
|
+
let mergeQueueEntries: Array<{ branchName: string; agentName: string; status: string }> = [];
|
|
464
|
+
if (stores.mergeQueue) {
|
|
465
|
+
try {
|
|
466
|
+
let entries = stores.mergeQueue.list();
|
|
467
|
+
if (runId && filteredAgents.length > 0) {
|
|
468
|
+
const agentNames = new Set(filteredAgents.map((a) => a.agentName));
|
|
469
|
+
entries = entries.filter((e) => agentNames.has(e.agentName));
|
|
470
|
+
}
|
|
471
|
+
mergeQueueEntries = entries.map((e) => ({
|
|
472
|
+
branchName: e.branchName,
|
|
473
|
+
agentName: e.agentName,
|
|
474
|
+
status: e.status,
|
|
475
|
+
}));
|
|
476
|
+
} catch {
|
|
477
|
+
// best effort
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Load metrics from pre-opened metrics store
|
|
482
|
+
let totalSessions = 0;
|
|
483
|
+
let avgDuration = 0;
|
|
484
|
+
const byCapability: Record<string, number> = {};
|
|
485
|
+
if (stores.metricsStore) {
|
|
486
|
+
try {
|
|
487
|
+
if (runId && filteredAgents.length > 0) {
|
|
488
|
+
const agentNames = new Set(filteredAgents.map((a) => a.agentName));
|
|
489
|
+
const sessions = stores.metricsStore.getRecentSessions(100);
|
|
490
|
+
const filtered = sessions.filter((s) => agentNames.has(s.agentName));
|
|
491
|
+
|
|
492
|
+
totalSessions = filtered.length;
|
|
493
|
+
|
|
494
|
+
const completedSessions = filtered.filter((s) => s.completedAt !== null);
|
|
495
|
+
if (completedSessions.length > 0) {
|
|
496
|
+
avgDuration =
|
|
497
|
+
completedSessions.reduce((sum, s) => sum + s.durationMs, 0) / completedSessions.length;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
for (const session of filtered) {
|
|
501
|
+
const cap = session.capability;
|
|
502
|
+
byCapability[cap] = (byCapability[cap] ?? 0) + 1;
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
totalSessions = stores.metricsStore.countSessions();
|
|
506
|
+
avgDuration = stores.metricsStore.getAverageDuration();
|
|
507
|
+
|
|
508
|
+
const sessions = stores.metricsStore.getRecentSessions(100);
|
|
509
|
+
for (const session of sessions) {
|
|
510
|
+
const cap = session.capability;
|
|
511
|
+
byCapability[cap] = (byCapability[cap] ?? 0) + 1;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} catch {
|
|
515
|
+
// best effort
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Load tasks from tracker with cache
|
|
520
|
+
let tasks: TrackerIssue[] = [];
|
|
521
|
+
const now2 = Date.now();
|
|
522
|
+
if (!trackerCache || now2 - trackerCache.fetchedAt > TRACKER_CACHE_TTL_MS) {
|
|
523
|
+
try {
|
|
524
|
+
const backend = await resolveBackend(taskTrackerBackend ?? "auto", root);
|
|
525
|
+
const tracker = createTrackerClient(backend, root);
|
|
526
|
+
tasks = await tracker.list({ limit: 10 });
|
|
527
|
+
trackerCache = { tasks, fetchedAt: now2 };
|
|
528
|
+
} catch {
|
|
529
|
+
// tracker unavailable — graceful degradation
|
|
530
|
+
tasks = trackerCache?.tasks ?? [];
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
tasks = trackerCache.tasks;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Load recent events via incremental buffer (or fallback to empty)
|
|
537
|
+
let recentEvents: StoredEvent[] = [];
|
|
538
|
+
let feedColorMap: Map<string, (s: string) => string> = new Map();
|
|
539
|
+
if (eventBuffer && stores.eventStore) {
|
|
540
|
+
try {
|
|
541
|
+
eventBuffer.poll(stores.eventStore);
|
|
542
|
+
recentEvents = [...eventBuffer.getEvents()].reverse();
|
|
543
|
+
feedColorMap = eventBuffer.getColorMap();
|
|
544
|
+
} catch {
|
|
545
|
+
/* best effort */
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
currentRunId: runId,
|
|
551
|
+
status,
|
|
552
|
+
recentMail,
|
|
553
|
+
mergeQueue: mergeQueueEntries,
|
|
554
|
+
metrics: { totalSessions, avgDuration, byCapability },
|
|
555
|
+
tasks,
|
|
556
|
+
recentEvents,
|
|
557
|
+
feedColorMap,
|
|
558
|
+
runtimeConfig,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Render the header bar (line 1).
|
|
564
|
+
*/
|
|
565
|
+
function renderHeader(width: number, interval: number, currentRunId?: string | null): string {
|
|
566
|
+
const left = brand.bold(`ap dashboard v${PKG_VERSION}`);
|
|
567
|
+
const now = new Date().toLocaleTimeString();
|
|
568
|
+
const scope = currentRunId ? ` [run: ${accent(currentRunId.slice(0, 8))}]` : " [all runs]";
|
|
569
|
+
const right = `${now}${scope} | refresh: ${interval}ms`;
|
|
570
|
+
const padding = width - visibleLength(left) - right.length;
|
|
571
|
+
const line = left + " ".repeat(Math.max(0, padding)) + right;
|
|
572
|
+
const separator = horizontalLine(width, BOX.topLeft, BOX.horizontal, BOX.topRight);
|
|
573
|
+
return `${line}\n${separator}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Resolve the runtime name for a given capability from config.
|
|
578
|
+
* Mirrors the lookup chain in runtimes/registry.ts getRuntime():
|
|
579
|
+
* capabilities[cap] > runtime.default > "claude"
|
|
580
|
+
*/
|
|
581
|
+
function resolveRuntimeName(
|
|
582
|
+
capability: string,
|
|
583
|
+
runtimeConfig?: AgentplateConfig["runtime"],
|
|
584
|
+
): string {
|
|
585
|
+
return runtimeConfig?.capabilities?.[capability] ?? runtimeConfig?.default ?? "claude";
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Render the agent panel (left 60%, dynamic height).
|
|
590
|
+
*/
|
|
591
|
+
export function renderAgentPanel(
|
|
592
|
+
data: DashboardData,
|
|
593
|
+
fullWidth: number,
|
|
594
|
+
panelHeight: number,
|
|
595
|
+
startRow: number,
|
|
596
|
+
): string {
|
|
597
|
+
const leftWidth = fullWidth;
|
|
598
|
+
let output = "";
|
|
599
|
+
|
|
600
|
+
// Panel header
|
|
601
|
+
const headerLine = `${dimBox.vertical} ${brand.bold("Agents")} (${data.status.agents.length})`;
|
|
602
|
+
const headerPadding = " ".repeat(
|
|
603
|
+
Math.max(0, leftWidth - visibleLength(headerLine) - visibleLength(dimBox.vertical)),
|
|
604
|
+
);
|
|
605
|
+
output += `${CURSOR.cursorTo(startRow, 1)}${headerLine}${headerPadding}${dimBox.vertical}\n`;
|
|
606
|
+
|
|
607
|
+
// Column headers
|
|
608
|
+
const colStr = `${dimBox.vertical} St Name Capability Runtime State Task ID Duration Live `;
|
|
609
|
+
const colPadding = " ".repeat(
|
|
610
|
+
Math.max(0, leftWidth - visibleLength(colStr) - visibleLength(dimBox.vertical)),
|
|
611
|
+
);
|
|
612
|
+
output += `${CURSOR.cursorTo(startRow + 1, 1)}${colStr}${colPadding}${dimBox.vertical}\n`;
|
|
613
|
+
|
|
614
|
+
// Separator
|
|
615
|
+
const separator = dimHorizontalLine(leftWidth, dimBox.tee, dimBox.teeRight);
|
|
616
|
+
output += `${CURSOR.cursorTo(startRow + 2, 1)}${separator}\n`;
|
|
617
|
+
|
|
618
|
+
// Sort agents: active first, then completed, then zombie
|
|
619
|
+
const agents = [...data.status.agents].sort((a, b) => {
|
|
620
|
+
const activeStates = ["working", "in_turn", "between_turns", "booting", "stalled"];
|
|
621
|
+
const aActive = activeStates.includes(a.state);
|
|
622
|
+
const bActive = activeStates.includes(b.state);
|
|
623
|
+
if (aActive && !bActive) return -1;
|
|
624
|
+
if (!aActive && bActive) return 1;
|
|
625
|
+
return 0;
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const now = Date.now();
|
|
629
|
+
const maxRows = panelHeight - 4; // header + col headers + separator + border
|
|
630
|
+
const visibleAgents = agents.slice(0, maxRows);
|
|
631
|
+
|
|
632
|
+
for (let i = 0; i < visibleAgents.length; i++) {
|
|
633
|
+
const agent = visibleAgents[i];
|
|
634
|
+
if (!agent) continue;
|
|
635
|
+
|
|
636
|
+
const icon = stateIcon(agent.state);
|
|
637
|
+
const stateColorFn = stateColor(agent.state);
|
|
638
|
+
const name = accent(pad(truncate(agent.agentName, 15), 15));
|
|
639
|
+
const capability = pad(truncate(agent.capability, 12), 12);
|
|
640
|
+
const runtimeName = resolveRuntimeName(agent.capability, data.runtimeConfig);
|
|
641
|
+
const runtime = pad(truncate(runtimeName, 8), 8);
|
|
642
|
+
const state = pad(agent.state, 10);
|
|
643
|
+
const taskId = accent(pad(truncate(agent.taskId, 16), 16));
|
|
644
|
+
const endTime =
|
|
645
|
+
agent.state === "completed" || agent.state === "zombie"
|
|
646
|
+
? new Date(agent.lastActivity).getTime()
|
|
647
|
+
: now;
|
|
648
|
+
const duration = formatDuration(endTime - new Date(agent.startedAt).getTime());
|
|
649
|
+
const durationPadded = pad(duration, 9);
|
|
650
|
+
// Three liveness topologies (agentplate-7a34):
|
|
651
|
+
// tmux: tmuxSession !== "" → tmux session must exist
|
|
652
|
+
// long-lived headless: tmuxSession === "" && pid !== null → PID must be alive
|
|
653
|
+
// spawn-per-turn: tmuxSession === "" && pid === null → no process between
|
|
654
|
+
// turns is normal, so liveness reduces to "state is non-terminal".
|
|
655
|
+
// Time-based stale/zombie classification is handled in evaluateHealth.
|
|
656
|
+
const isHeadless = agent.tmuxSession === "" && agent.pid !== null;
|
|
657
|
+
const isSpawnPerTurn = agent.tmuxSession === "" && agent.pid === null;
|
|
658
|
+
const alive = isSpawnPerTurn
|
|
659
|
+
? agent.state !== "zombie" && agent.state !== "completed"
|
|
660
|
+
: isHeadless
|
|
661
|
+
? agent.pid !== null && isProcessAlive(agent.pid)
|
|
662
|
+
: data.status.tmuxSessions.some((s) => s.name === agent.tmuxSession);
|
|
663
|
+
const aliveDot = alive ? color.green(">") : color.red("x");
|
|
664
|
+
|
|
665
|
+
const lineContent = `${dimBox.vertical} ${stateColorFn(icon)} ${name} ${capability} ${color.dim(runtime)} ${stateColorFn(state)} ${taskId} ${durationPadded} ${aliveDot} `;
|
|
666
|
+
const linePadding = " ".repeat(
|
|
667
|
+
Math.max(0, leftWidth - visibleLength(lineContent) - visibleLength(dimBox.vertical)),
|
|
668
|
+
);
|
|
669
|
+
output += `${CURSOR.cursorTo(startRow + 3 + i, 1)}${lineContent}${linePadding}${dimBox.vertical}\n`;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Fill remaining rows with empty lines
|
|
673
|
+
for (let i = visibleAgents.length; i < maxRows; i++) {
|
|
674
|
+
const emptyLine = `${dimBox.vertical}${" ".repeat(Math.max(0, leftWidth - 2))}${dimBox.vertical}`;
|
|
675
|
+
output += `${CURSOR.cursorTo(startRow + 3 + i, 1)}${emptyLine}\n`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Bottom border (joins the right column)
|
|
679
|
+
const bottomBorder = dimHorizontalLine(leftWidth, dimBox.tee, dimBox.teeRight);
|
|
680
|
+
output += `${CURSOR.cursorTo(startRow + 3 + maxRows, 1)}${bottomBorder}\n`;
|
|
681
|
+
|
|
682
|
+
return output;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Render the tasks panel (upper-right quadrant).
|
|
687
|
+
*/
|
|
688
|
+
export function renderTasksPanel(
|
|
689
|
+
data: DashboardData,
|
|
690
|
+
startCol: number,
|
|
691
|
+
panelWidth: number,
|
|
692
|
+
panelHeight: number,
|
|
693
|
+
startRow: number,
|
|
694
|
+
): string {
|
|
695
|
+
let output = "";
|
|
696
|
+
|
|
697
|
+
// Header
|
|
698
|
+
const headerLine = `${dimBox.vertical} ${brand.bold("Tasks")} (${data.tasks.length})`;
|
|
699
|
+
const headerPadding = " ".repeat(
|
|
700
|
+
Math.max(0, panelWidth - visibleLength(headerLine) - visibleLength(dimBox.vertical)),
|
|
701
|
+
);
|
|
702
|
+
output += `${CURSOR.cursorTo(startRow, startCol)}${headerLine}${headerPadding}${dimBox.vertical}\n`;
|
|
703
|
+
|
|
704
|
+
// Separator
|
|
705
|
+
const separator = dimHorizontalLine(panelWidth, dimBox.tee, dimBox.teeRight);
|
|
706
|
+
output += `${CURSOR.cursorTo(startRow + 1, startCol)}${separator}\n`;
|
|
707
|
+
|
|
708
|
+
const maxRows = panelHeight - 2; // header + separator
|
|
709
|
+
const visibleTasks = data.tasks.slice(0, maxRows);
|
|
710
|
+
|
|
711
|
+
if (visibleTasks.length === 0) {
|
|
712
|
+
const emptyMsg = color.dim("No tracker data");
|
|
713
|
+
const emptyLine = `${dimBox.vertical} ${emptyMsg}`;
|
|
714
|
+
const emptyPadding = " ".repeat(
|
|
715
|
+
Math.max(0, panelWidth - visibleLength(emptyLine) - visibleLength(dimBox.vertical)),
|
|
716
|
+
);
|
|
717
|
+
output += `${CURSOR.cursorTo(startRow + 2, startCol)}${emptyLine}${emptyPadding}${dimBox.vertical}\n`;
|
|
718
|
+
// Fill remaining rows
|
|
719
|
+
for (let i = 1; i < maxRows; i++) {
|
|
720
|
+
const blankLine = `${dimBox.vertical}${" ".repeat(Math.max(0, panelWidth - 2))}${dimBox.vertical}`;
|
|
721
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, startCol)}${blankLine}\n`;
|
|
722
|
+
}
|
|
723
|
+
return output;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
for (let i = 0; i < visibleTasks.length; i++) {
|
|
727
|
+
const task = visibleTasks[i];
|
|
728
|
+
if (!task) continue;
|
|
729
|
+
|
|
730
|
+
const idStr = accent(pad(truncate(task.id, 14), 14));
|
|
731
|
+
const priorityStr = numericPriorityColor(task.priority)(`P${task.priority}`);
|
|
732
|
+
const statusStr = pad(task.status, 12);
|
|
733
|
+
const titleMaxLen = Math.max(4, panelWidth - 44);
|
|
734
|
+
const titleStr = truncate(task.title, titleMaxLen);
|
|
735
|
+
|
|
736
|
+
const lineContent = `${dimBox.vertical} ${idStr} ${titleStr} ${priorityStr} ${statusStr}`;
|
|
737
|
+
const linePadding = " ".repeat(
|
|
738
|
+
Math.max(0, panelWidth - visibleLength(lineContent) - visibleLength(dimBox.vertical)),
|
|
739
|
+
);
|
|
740
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, startCol)}${lineContent}${linePadding}${dimBox.vertical}\n`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Fill remaining rows
|
|
744
|
+
for (let i = visibleTasks.length; i < maxRows; i++) {
|
|
745
|
+
const blankLine = `${dimBox.vertical}${" ".repeat(Math.max(0, panelWidth - 2))}${dimBox.vertical}`;
|
|
746
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, startCol)}${blankLine}\n`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return output;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Render the feed panel (lower-right quadrant).
|
|
754
|
+
*/
|
|
755
|
+
export function renderFeedPanel(
|
|
756
|
+
data: DashboardData,
|
|
757
|
+
startCol: number,
|
|
758
|
+
panelWidth: number,
|
|
759
|
+
panelHeight: number,
|
|
760
|
+
startRow: number,
|
|
761
|
+
): string {
|
|
762
|
+
let output = "";
|
|
763
|
+
|
|
764
|
+
// Header
|
|
765
|
+
const headerLine = `${dimBox.vertical} ${brand.bold("Feed")} (live)`;
|
|
766
|
+
const headerPadding = " ".repeat(
|
|
767
|
+
Math.max(0, panelWidth - visibleLength(headerLine) - visibleLength(dimBox.vertical)),
|
|
768
|
+
);
|
|
769
|
+
output += `${CURSOR.cursorTo(startRow, startCol)}${headerLine}${headerPadding}${dimBox.vertical}\n`;
|
|
770
|
+
|
|
771
|
+
// Separator
|
|
772
|
+
const separator = dimHorizontalLine(panelWidth, dimBox.tee, dimBox.teeRight);
|
|
773
|
+
output += `${CURSOR.cursorTo(startRow + 1, startCol)}${separator}\n`;
|
|
774
|
+
|
|
775
|
+
const maxRows = panelHeight - 2; // header + separator
|
|
776
|
+
|
|
777
|
+
if (data.recentEvents.length === 0) {
|
|
778
|
+
const emptyMsg = color.dim("No recent events");
|
|
779
|
+
const emptyLine = `${dimBox.vertical} ${emptyMsg}`;
|
|
780
|
+
const emptyPadding = " ".repeat(
|
|
781
|
+
Math.max(0, panelWidth - visibleLength(emptyLine) - visibleLength(dimBox.vertical)),
|
|
782
|
+
);
|
|
783
|
+
output += `${CURSOR.cursorTo(startRow + 2, startCol)}${emptyLine}${emptyPadding}${dimBox.vertical}\n`;
|
|
784
|
+
for (let i = 1; i < maxRows; i++) {
|
|
785
|
+
const blankLine = `${dimBox.vertical}${" ".repeat(Math.max(0, panelWidth - 2))}${dimBox.vertical}`;
|
|
786
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, startCol)}${blankLine}\n`;
|
|
787
|
+
}
|
|
788
|
+
return output;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const colorMap =
|
|
792
|
+
data.feedColorMap.size > 0 ? data.feedColorMap : buildAgentColorMap(data.recentEvents);
|
|
793
|
+
const visibleEvents = data.recentEvents.slice(0, maxRows);
|
|
794
|
+
|
|
795
|
+
for (let i = 0; i < visibleEvents.length; i++) {
|
|
796
|
+
const event = visibleEvents[i];
|
|
797
|
+
if (!event) continue;
|
|
798
|
+
|
|
799
|
+
const formatted = formatEventLine(event, colorMap);
|
|
800
|
+
// ANSI-safe truncation: trim to panelWidth - 4 (border + space each side)
|
|
801
|
+
const maxLineLen = panelWidth - 4;
|
|
802
|
+
let displayLine = formatted;
|
|
803
|
+
if (visibleLength(displayLine) > maxLineLen) {
|
|
804
|
+
// Truncate by stripping to visible characters
|
|
805
|
+
let count = 0;
|
|
806
|
+
let end = 0;
|
|
807
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: needed for ANSI
|
|
808
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
809
|
+
let lastIndex = 0;
|
|
810
|
+
let match = ANSI.exec(displayLine);
|
|
811
|
+
while (match !== null) {
|
|
812
|
+
const plainSegLen = match.index - lastIndex;
|
|
813
|
+
if (count + plainSegLen >= maxLineLen - 1) {
|
|
814
|
+
end = lastIndex + (maxLineLen - 1 - count);
|
|
815
|
+
count = maxLineLen - 1;
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
count += plainSegLen;
|
|
819
|
+
lastIndex = match.index + match[0].length;
|
|
820
|
+
end = lastIndex;
|
|
821
|
+
match = ANSI.exec(displayLine);
|
|
822
|
+
}
|
|
823
|
+
if (count < maxLineLen - 1) {
|
|
824
|
+
end = displayLine.length;
|
|
825
|
+
}
|
|
826
|
+
displayLine = `${displayLine.slice(0, end)}…`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const lineContent = `${dimBox.vertical} ${displayLine}`;
|
|
830
|
+
const contentLen = visibleLength(lineContent) + visibleLength(dimBox.vertical);
|
|
831
|
+
const linePadding = " ".repeat(Math.max(0, panelWidth - contentLen));
|
|
832
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, startCol)}${lineContent}${linePadding}${dimBox.vertical}\n`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Fill remaining rows
|
|
836
|
+
for (let i = visibleEvents.length; i < maxRows; i++) {
|
|
837
|
+
const blankLine = `${dimBox.vertical}${" ".repeat(Math.max(0, panelWidth - 2))}${dimBox.vertical}`;
|
|
838
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, startCol)}${blankLine}\n`;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return output;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Render the mail panel (bottom-left 50%).
|
|
846
|
+
*/
|
|
847
|
+
function renderMailPanel(
|
|
848
|
+
data: DashboardData,
|
|
849
|
+
panelWidth: number,
|
|
850
|
+
panelHeight: number,
|
|
851
|
+
startRow: number,
|
|
852
|
+
): string {
|
|
853
|
+
let output = "";
|
|
854
|
+
|
|
855
|
+
const unreadCount = data.status.unreadMailCount;
|
|
856
|
+
const headerLine = `${dimBox.vertical} ${brand.bold("Mail")} (${unreadCount} unread)`;
|
|
857
|
+
const headerPadding = " ".repeat(
|
|
858
|
+
Math.max(0, panelWidth - visibleLength(headerLine) - visibleLength(dimBox.vertical)),
|
|
859
|
+
);
|
|
860
|
+
output += `${CURSOR.cursorTo(startRow, 1)}${headerLine}${headerPadding}${dimBox.vertical}\n`;
|
|
861
|
+
|
|
862
|
+
const separator = dimHorizontalLine(panelWidth, dimBox.tee, dimBox.cross);
|
|
863
|
+
output += `${CURSOR.cursorTo(startRow + 1, 1)}${separator}\n`;
|
|
864
|
+
|
|
865
|
+
const maxRows = panelHeight - 3; // header + separator + border
|
|
866
|
+
const messages = data.recentMail.slice(0, maxRows);
|
|
867
|
+
|
|
868
|
+
for (let i = 0; i < messages.length; i++) {
|
|
869
|
+
const msg = messages[i];
|
|
870
|
+
if (!msg) continue;
|
|
871
|
+
|
|
872
|
+
const priorityColorFn = priorityColor(msg.priority);
|
|
873
|
+
const priority = msg.priority === "normal" ? "" : `[${msg.priority}] `;
|
|
874
|
+
const from = accent(truncate(msg.from, 12));
|
|
875
|
+
const to = accent(truncate(msg.to, 12));
|
|
876
|
+
const subject = truncate(msg.subject, panelWidth - 40);
|
|
877
|
+
const time = formatRelativeTime(msg.createdAt);
|
|
878
|
+
|
|
879
|
+
const coloredPriority = priority ? priorityColorFn(priority) : "";
|
|
880
|
+
const lineContent = `${dimBox.vertical} ${coloredPriority}${from} → ${to}: ${subject} (${time})`;
|
|
881
|
+
const padding = " ".repeat(
|
|
882
|
+
Math.max(0, panelWidth - visibleLength(lineContent) - visibleLength(dimBox.vertical)),
|
|
883
|
+
);
|
|
884
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, 1)}${lineContent}${padding}${dimBox.vertical}\n`;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Fill remaining rows with empty lines
|
|
888
|
+
for (let i = messages.length; i < maxRows; i++) {
|
|
889
|
+
const emptyLine = `${dimBox.vertical}${" ".repeat(Math.max(0, panelWidth - 2))}${dimBox.vertical}`;
|
|
890
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, 1)}${emptyLine}\n`;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return output;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Render the merge queue panel (bottom-right 50%).
|
|
898
|
+
*/
|
|
899
|
+
function renderMergeQueuePanel(
|
|
900
|
+
data: DashboardData,
|
|
901
|
+
panelWidth: number,
|
|
902
|
+
panelHeight: number,
|
|
903
|
+
startRow: number,
|
|
904
|
+
startCol: number,
|
|
905
|
+
): string {
|
|
906
|
+
let output = "";
|
|
907
|
+
|
|
908
|
+
const headerLine = `${dimBox.vertical} ${brand.bold("Merge Queue")} (${data.mergeQueue.length})`;
|
|
909
|
+
const headerPadding = " ".repeat(
|
|
910
|
+
Math.max(0, panelWidth - visibleLength(headerLine) - visibleLength(dimBox.vertical)),
|
|
911
|
+
);
|
|
912
|
+
output += `${CURSOR.cursorTo(startRow, startCol)}${headerLine}${headerPadding}${dimBox.vertical}\n`;
|
|
913
|
+
|
|
914
|
+
const separator = dimHorizontalLine(panelWidth, dimBox.cross, dimBox.teeRight);
|
|
915
|
+
output += `${CURSOR.cursorTo(startRow + 1, startCol)}${separator}\n`;
|
|
916
|
+
|
|
917
|
+
const maxRows = panelHeight - 3; // header + separator + border
|
|
918
|
+
const entries = data.mergeQueue.slice(0, maxRows);
|
|
919
|
+
|
|
920
|
+
for (let i = 0; i < entries.length; i++) {
|
|
921
|
+
const entry = entries[i];
|
|
922
|
+
if (!entry) continue;
|
|
923
|
+
|
|
924
|
+
const statusColorFn = mergeStatusColor(entry.status);
|
|
925
|
+
const status = pad(entry.status, 10);
|
|
926
|
+
const agent = accent(truncate(entry.agentName, 15));
|
|
927
|
+
const branch = truncate(entry.branchName, panelWidth - 30);
|
|
928
|
+
|
|
929
|
+
const lineContent = `${dimBox.vertical} ${statusColorFn(status)} ${agent} ${branch}`;
|
|
930
|
+
const padding = " ".repeat(
|
|
931
|
+
Math.max(0, panelWidth - visibleLength(lineContent) - visibleLength(dimBox.vertical)),
|
|
932
|
+
);
|
|
933
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, startCol)}${lineContent}${padding}${dimBox.vertical}\n`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Fill remaining rows with empty lines
|
|
937
|
+
for (let i = entries.length; i < maxRows; i++) {
|
|
938
|
+
const emptyLine = `${dimBox.vertical}${" ".repeat(Math.max(0, panelWidth - 2))}${dimBox.vertical}`;
|
|
939
|
+
output += `${CURSOR.cursorTo(startRow + 2 + i, startCol)}${emptyLine}\n`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return output;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Render the metrics panel (bottom strip).
|
|
947
|
+
*/
|
|
948
|
+
function renderMetricsPanel(
|
|
949
|
+
data: DashboardData,
|
|
950
|
+
width: number,
|
|
951
|
+
_height: number,
|
|
952
|
+
startRow: number,
|
|
953
|
+
): string {
|
|
954
|
+
let output = "";
|
|
955
|
+
|
|
956
|
+
const separator = dimHorizontalLine(width, dimBox.tee, dimBox.teeRight);
|
|
957
|
+
output += `${CURSOR.cursorTo(startRow, 1)}${separator}\n`;
|
|
958
|
+
|
|
959
|
+
const totalSessions = data.metrics.totalSessions;
|
|
960
|
+
const avgDur = formatDuration(data.metrics.avgDuration);
|
|
961
|
+
const byCapability = Object.entries(data.metrics.byCapability)
|
|
962
|
+
.map(([cap, count]) => `${cap}:${count}`)
|
|
963
|
+
.join(", ");
|
|
964
|
+
|
|
965
|
+
const metricsLine = `${dimBox.vertical} ${brand.bold("Metrics")} Total: ${totalSessions} | Avg: ${avgDur} | ${byCapability}`;
|
|
966
|
+
const metricsPadding = " ".repeat(
|
|
967
|
+
Math.max(0, width - visibleLength(metricsLine) - visibleLength(dimBox.vertical)),
|
|
968
|
+
);
|
|
969
|
+
output += `${CURSOR.cursorTo(startRow + 1, 1)}${metricsLine}${metricsPadding}${dimBox.vertical}\n`;
|
|
970
|
+
|
|
971
|
+
const bottomBorder = dimHorizontalLine(width, dimBox.bottomLeft, dimBox.bottomRight);
|
|
972
|
+
output += `${CURSOR.cursorTo(startRow + 2, 1)}${bottomBorder}\n`;
|
|
973
|
+
|
|
974
|
+
return output;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Render the full dashboard.
|
|
979
|
+
*/
|
|
980
|
+
function renderDashboard(data: DashboardData, interval: number, isFirstRender: boolean): void {
|
|
981
|
+
const width = process.stdout.columns ?? 100;
|
|
982
|
+
const height = process.stdout.rows ?? 30;
|
|
983
|
+
|
|
984
|
+
// First render: clear entire alt screen. Subsequent: just home cursor
|
|
985
|
+
// and overwrite in-place (avoids Warp's block-per-clear issue).
|
|
986
|
+
let output = isFirstRender ? CURSOR.clear : CURSOR.home;
|
|
987
|
+
|
|
988
|
+
// Header (rows 1-2)
|
|
989
|
+
output += renderHeader(width, interval, data.currentRunId);
|
|
990
|
+
|
|
991
|
+
// Agent panel: full width, capped at 35% of height
|
|
992
|
+
const agentPanelStart = 3;
|
|
993
|
+
const agentCount = data.status.agents.length;
|
|
994
|
+
const agentPanelHeight = computeAgentPanelHeight(height, agentCount);
|
|
995
|
+
output += renderAgentPanel(data, width, agentPanelHeight, agentPanelStart);
|
|
996
|
+
|
|
997
|
+
// Middle zone: Feed (left 60%) | Tasks (right 40%)
|
|
998
|
+
const middleStart = agentPanelStart + agentPanelHeight + 1;
|
|
999
|
+
const compactPanelHeight = 5; // fixed for mail/merge panels
|
|
1000
|
+
const metricsHeight = 3; // separator + data + border
|
|
1001
|
+
const middleHeight = Math.max(6, height - middleStart - compactPanelHeight - metricsHeight);
|
|
1002
|
+
|
|
1003
|
+
const feedWidth = Math.floor(width * 0.6);
|
|
1004
|
+
output += renderFeedPanel(data, 1, feedWidth, middleHeight, middleStart);
|
|
1005
|
+
|
|
1006
|
+
const taskWidth = width - feedWidth;
|
|
1007
|
+
const taskStartCol = feedWidth + 1;
|
|
1008
|
+
output += renderTasksPanel(data, taskStartCol, taskWidth, middleHeight, middleStart);
|
|
1009
|
+
|
|
1010
|
+
// Compact panels: Mail (left 50%) | Merge Queue (right 50%) — fixed 5 rows
|
|
1011
|
+
const compactStart = middleStart + middleHeight;
|
|
1012
|
+
const mailWidth = Math.floor(width * 0.5);
|
|
1013
|
+
output += renderMailPanel(data, mailWidth, compactPanelHeight, compactStart);
|
|
1014
|
+
|
|
1015
|
+
const mergeStartCol = mailWidth + 1;
|
|
1016
|
+
const mergeWidth = width - mailWidth;
|
|
1017
|
+
output += renderMergeQueuePanel(
|
|
1018
|
+
data,
|
|
1019
|
+
mergeWidth,
|
|
1020
|
+
compactPanelHeight,
|
|
1021
|
+
compactStart,
|
|
1022
|
+
mergeStartCol,
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
// Metrics footer
|
|
1026
|
+
const metricsStart = compactStart + compactPanelHeight;
|
|
1027
|
+
output += renderMetricsPanel(data, width, height, metricsStart);
|
|
1028
|
+
|
|
1029
|
+
process.stdout.write(output);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
interface DashboardOpts {
|
|
1033
|
+
interval?: string;
|
|
1034
|
+
all?: boolean;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function executeDashboard(opts: DashboardOpts): Promise<void> {
|
|
1038
|
+
const intervalStr = opts.interval;
|
|
1039
|
+
const interval = intervalStr ? Number.parseInt(intervalStr, 10) : 2000;
|
|
1040
|
+
const showAll = opts.all ?? false;
|
|
1041
|
+
|
|
1042
|
+
if (Number.isNaN(interval) || interval < 500) {
|
|
1043
|
+
throw new ValidationError("--interval must be a number >= 500 (milliseconds)", {
|
|
1044
|
+
field: "interval",
|
|
1045
|
+
value: intervalStr,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const cwd = process.cwd();
|
|
1050
|
+
const config = await loadConfig(cwd);
|
|
1051
|
+
const root = config.project.root;
|
|
1052
|
+
|
|
1053
|
+
// Read current run ID unless --all flag is set
|
|
1054
|
+
let runId: string | null | undefined;
|
|
1055
|
+
if (!showAll) {
|
|
1056
|
+
const agentplateDir = join(root, ".agentplate");
|
|
1057
|
+
runId = await readCurrentRunId(agentplateDir);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Open stores once for the entire poll loop lifetime
|
|
1061
|
+
const stores = openDashboardStores(root);
|
|
1062
|
+
|
|
1063
|
+
// Create rolling event buffer (persisted across poll ticks)
|
|
1064
|
+
const eventBuffer = new EventBuffer(100);
|
|
1065
|
+
|
|
1066
|
+
// Compute health thresholds once from config (reused across poll ticks)
|
|
1067
|
+
const thresholds = {
|
|
1068
|
+
staleMs: config.watchdog.staleThresholdMs,
|
|
1069
|
+
zombieMs: config.watchdog.zombieThresholdMs,
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// Enter alternate screen buffer (like vim/htop) + hide cursor + raw stdin
|
|
1073
|
+
process.stdout.write(CURSOR.enterAltScreen);
|
|
1074
|
+
process.stdout.write(CURSOR.hideCursor);
|
|
1075
|
+
if (process.stdin.isTTY) {
|
|
1076
|
+
process.stdin.setRawMode(true);
|
|
1077
|
+
process.stdin.resume();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Clean exit on Ctrl+C or 'q': restore original screen
|
|
1081
|
+
let running = true;
|
|
1082
|
+
const cleanup = () => {
|
|
1083
|
+
running = false;
|
|
1084
|
+
if (process.stdin.isTTY) {
|
|
1085
|
+
process.stdin.setRawMode(false);
|
|
1086
|
+
process.stdin.pause();
|
|
1087
|
+
}
|
|
1088
|
+
closeDashboardStores(stores);
|
|
1089
|
+
process.stdout.write(CURSOR.showCursor);
|
|
1090
|
+
process.stdout.write(CURSOR.leaveAltScreen);
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
process.on("SIGINT", () => {
|
|
1094
|
+
cleanup();
|
|
1095
|
+
process.exitCode = 0;
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// Allow 'q' to quit the dashboard
|
|
1099
|
+
process.stdin.on("data", (data: Buffer) => {
|
|
1100
|
+
const key = data.toString();
|
|
1101
|
+
if (key === "q" || key === "\x03") {
|
|
1102
|
+
// 'q' or Ctrl+C
|
|
1103
|
+
cleanup();
|
|
1104
|
+
process.exitCode = 0;
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
// Poll loop — errors are caught per-tick so transient DB failures never crash the dashboard.
|
|
1109
|
+
let isFirstRender = true;
|
|
1110
|
+
let lastGoodData: DashboardData | null = null;
|
|
1111
|
+
let lastErrorMsg: string | null = null;
|
|
1112
|
+
while (running) {
|
|
1113
|
+
try {
|
|
1114
|
+
const data = await loadDashboardData(
|
|
1115
|
+
root,
|
|
1116
|
+
stores,
|
|
1117
|
+
runId,
|
|
1118
|
+
thresholds,
|
|
1119
|
+
eventBuffer,
|
|
1120
|
+
config.runtime,
|
|
1121
|
+
config.taskTracker.backend,
|
|
1122
|
+
);
|
|
1123
|
+
lastGoodData = data;
|
|
1124
|
+
// If recovering from an error, clear the stale error line at the bottom
|
|
1125
|
+
if (lastErrorMsg !== null) {
|
|
1126
|
+
const w = process.stdout.columns ?? 100;
|
|
1127
|
+
const h = process.stdout.rows ?? 30;
|
|
1128
|
+
process.stdout.write(`${CURSOR.cursorTo(h, 1)}${" ".repeat(w)}`);
|
|
1129
|
+
}
|
|
1130
|
+
lastErrorMsg = null;
|
|
1131
|
+
renderDashboard(data, interval, isFirstRender);
|
|
1132
|
+
isFirstRender = false;
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
// Render last good frame so the TUI stays alive, then show the error inline.
|
|
1135
|
+
if (lastGoodData) {
|
|
1136
|
+
renderDashboard(lastGoodData, interval, isFirstRender);
|
|
1137
|
+
isFirstRender = false;
|
|
1138
|
+
}
|
|
1139
|
+
lastErrorMsg = err instanceof Error ? err.message : String(err);
|
|
1140
|
+
const w = process.stdout.columns ?? 100;
|
|
1141
|
+
const h = process.stdout.rows ?? 30;
|
|
1142
|
+
const errLine = `${CURSOR.cursorTo(h, 1)}\x1b[31m⚠ DB error (retrying):\x1b[0m ${truncate(lastErrorMsg, w - 30)}`;
|
|
1143
|
+
process.stdout.write(errLine);
|
|
1144
|
+
}
|
|
1145
|
+
await Bun.sleep(interval);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* `ap dashboard ui` — launch the web dashboard instead of the terminal TUI.
|
|
1151
|
+
*
|
|
1152
|
+
* Builds the SPA (via `runServe` → `ensureUiBuild`), starts the HTTP +
|
|
1153
|
+
* WebSocket server, and opens a browser at the served URL. `--no-open`
|
|
1154
|
+
* suppresses the browser launch (useful for remote/headless hosts).
|
|
1155
|
+
*/
|
|
1156
|
+
export function createDashboardUiCommand(): Command {
|
|
1157
|
+
return new Command("ui")
|
|
1158
|
+
.description("Launch the web dashboard (build SPA, start server, open browser)")
|
|
1159
|
+
.option("--port <n>", "TCP port to listen on", String(DEFAULT_SERVE_PORT))
|
|
1160
|
+
.option("--host <addr>", "Host/address to bind", "127.0.0.1")
|
|
1161
|
+
.option("--no-open", "Do not open a browser automatically")
|
|
1162
|
+
.option("--json", "Output startup info as JSON")
|
|
1163
|
+
.action(async (opts: { port?: string; host?: string; open?: boolean; json?: boolean }) => {
|
|
1164
|
+
const port = opts.port !== undefined ? Number.parseInt(opts.port, 10) : DEFAULT_SERVE_PORT;
|
|
1165
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
1166
|
+
throw new ValidationError(`Invalid port: ${opts.port ?? "undefined"}`, {
|
|
1167
|
+
field: "port",
|
|
1168
|
+
value: opts.port,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
const shouldOpen = opts.open !== false;
|
|
1172
|
+
await runServe({
|
|
1173
|
+
port,
|
|
1174
|
+
host: opts.host ?? "127.0.0.1",
|
|
1175
|
+
json: opts.json,
|
|
1176
|
+
onReady: (url) => {
|
|
1177
|
+
if (shouldOpen) openBrowser(url);
|
|
1178
|
+
},
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export function createDashboardCommand(): Command {
|
|
1184
|
+
return new Command("dashboard")
|
|
1185
|
+
.description("Live TUI dashboard for agent monitoring (Ctrl+C to stop)")
|
|
1186
|
+
.option("--interval <ms>", "Poll interval in milliseconds (default: 2000, min: 500)")
|
|
1187
|
+
.option("--all", "Show data from all runs (default: current run only)")
|
|
1188
|
+
.addCommand(createDashboardUiCommand())
|
|
1189
|
+
.action(async (opts: DashboardOpts) => {
|
|
1190
|
+
await executeDashboard(opts);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
export async function dashboardCommand(args: string[]): Promise<void> {
|
|
1195
|
+
const cmd = createDashboardCommand();
|
|
1196
|
+
cmd.exitOverride();
|
|
1197
|
+
try {
|
|
1198
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
1199
|
+
} catch (err: unknown) {
|
|
1200
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
1201
|
+
const code = (err as { code: string }).code;
|
|
1202
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (code.startsWith("commander.")) {
|
|
1206
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1207
|
+
throw new ValidationError(message, { field: "args" });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
throw err;
|
|
1211
|
+
}
|
|
1212
|
+
}
|