@pellux/goodvibes-agent 0.1.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.
Files changed (398) hide show
  1. package/.goodvibes/GOODVIBES.md +35 -0
  2. package/.goodvibes/agents/reviewer.md +48 -0
  3. package/.goodvibes/skills/add-provider/SKILL.md +199 -0
  4. package/CHANGELOG.md +25 -0
  5. package/README.md +74 -0
  6. package/bin/goodvibes-agent.ts +2 -0
  7. package/docs/README.md +23 -0
  8. package/docs/deployment-and-services.md +57 -0
  9. package/docs/getting-started.md +53 -0
  10. package/docs/release-and-publishing.md +46 -0
  11. package/package.json +134 -0
  12. package/scripts/check-bun.sh +20 -0
  13. package/src/audio/player.ts +156 -0
  14. package/src/audio/spoken-turn-controller.ts +203 -0
  15. package/src/audio/spoken-turn-model-routing.ts +117 -0
  16. package/src/audio/spoken-turn-wiring.ts +44 -0
  17. package/src/audio/text-chunker.ts +110 -0
  18. package/src/cli/bundle-command.ts +227 -0
  19. package/src/cli/completion.ts +90 -0
  20. package/src/cli/config-overrides.ts +159 -0
  21. package/src/cli/endpoints.ts +63 -0
  22. package/src/cli/entrypoint.ts +172 -0
  23. package/src/cli/help.ts +299 -0
  24. package/src/cli/index.ts +11 -0
  25. package/src/cli/management-commands.ts +426 -0
  26. package/src/cli/management.ts +744 -0
  27. package/src/cli/network-posture.ts +46 -0
  28. package/src/cli/package-verification.ts +123 -0
  29. package/src/cli/parser.ts +369 -0
  30. package/src/cli/provider-auth-routes.ts +22 -0
  31. package/src/cli/provider-classification.ts +107 -0
  32. package/src/cli/redaction.ts +105 -0
  33. package/src/cli/service-command.ts +26 -0
  34. package/src/cli/service-posture.ts +482 -0
  35. package/src/cli/status.ts +383 -0
  36. package/src/cli/surface-command.ts +247 -0
  37. package/src/cli/tui-startup.ts +32 -0
  38. package/src/cli/types.ts +69 -0
  39. package/src/cli-flags.ts +21 -0
  40. package/src/config/goodvibes-home-audit.ts +465 -0
  41. package/src/config/index.ts +57 -0
  42. package/src/config/provider-model.ts +23 -0
  43. package/src/config/secret-config.ts +119 -0
  44. package/src/config/secrets.ts +71 -0
  45. package/src/config/surface.ts +1 -0
  46. package/src/core/composer-state.ts +61 -0
  47. package/src/core/conversation-rendering.ts +359 -0
  48. package/src/core/conversation.ts +551 -0
  49. package/src/core/history.ts +45 -0
  50. package/src/core/orchestrator.ts +7 -0
  51. package/src/core/system-message-router.ts +171 -0
  52. package/src/daemon/cli.ts +55 -0
  53. package/src/daemon/safe-serve.ts +61 -0
  54. package/src/input/agent-workspace.ts +428 -0
  55. package/src/input/autocomplete.ts +96 -0
  56. package/src/input/bookmark-modal.ts +115 -0
  57. package/src/input/command-args-hint.ts +36 -0
  58. package/src/input/command-registry.ts +329 -0
  59. package/src/input/commands/agent-externalized-tui.ts +73 -0
  60. package/src/input/commands/agent-workspace-runtime.ts +17 -0
  61. package/src/input/commands/branch-runtime.ts +72 -0
  62. package/src/input/commands/cloudflare-runtime.ts +370 -0
  63. package/src/input/commands/config.ts +18 -0
  64. package/src/input/commands/control-room-runtime.ts +255 -0
  65. package/src/input/commands/conversation-runtime.ts +207 -0
  66. package/src/input/commands/discovery-runtime.ts +52 -0
  67. package/src/input/commands/eval.ts +204 -0
  68. package/src/input/commands/experience-runtime.ts +278 -0
  69. package/src/input/commands/guidance-runtime.ts +106 -0
  70. package/src/input/commands/health-runtime.ts +434 -0
  71. package/src/input/commands/hooks-runtime.ts +148 -0
  72. package/src/input/commands/incident-runtime.ts +95 -0
  73. package/src/input/commands/integration-runtime.ts +394 -0
  74. package/src/input/commands/intelligence-runtime.ts +223 -0
  75. package/src/input/commands/knowledge.ts +531 -0
  76. package/src/input/commands/local-auth-runtime.ts +105 -0
  77. package/src/input/commands/local-provider-runtime.ts +170 -0
  78. package/src/input/commands/local-runtime.ts +392 -0
  79. package/src/input/commands/local-setup-review.ts +199 -0
  80. package/src/input/commands/local-setup-transfer.ts +135 -0
  81. package/src/input/commands/local-setup.ts +282 -0
  82. package/src/input/commands/managed-runtime.ts +209 -0
  83. package/src/input/commands/marketplace-runtime.ts +290 -0
  84. package/src/input/commands/mcp-runtime.ts +432 -0
  85. package/src/input/commands/memory-product-runtime.ts +111 -0
  86. package/src/input/commands/memory.ts +151 -0
  87. package/src/input/commands/notify-runtime.ts +83 -0
  88. package/src/input/commands/onboarding-runtime.ts +14 -0
  89. package/src/input/commands/operator-panel-runtime.ts +146 -0
  90. package/src/input/commands/operator-runtime.ts +392 -0
  91. package/src/input/commands/planning-runtime.ts +205 -0
  92. package/src/input/commands/platform-access-runtime.ts +422 -0
  93. package/src/input/commands/platform-services-runtime.ts +246 -0
  94. package/src/input/commands/policy-dispatch.ts +339 -0
  95. package/src/input/commands/policy.ts +17 -0
  96. package/src/input/commands/product-runtime.ts +351 -0
  97. package/src/input/commands/profile-sync-runtime.ts +99 -0
  98. package/src/input/commands/provider-accounts-runtime.ts +113 -0
  99. package/src/input/commands/provider.ts +363 -0
  100. package/src/input/commands/qrcode-runtime.ts +20 -0
  101. package/src/input/commands/quit-shared.ts +162 -0
  102. package/src/input/commands/recall-bundle.ts +132 -0
  103. package/src/input/commands/recall-capture.ts +152 -0
  104. package/src/input/commands/recall-query.ts +229 -0
  105. package/src/input/commands/recall-review.ts +98 -0
  106. package/src/input/commands/recall-shared.ts +22 -0
  107. package/src/input/commands/remote-runtime-pool.ts +106 -0
  108. package/src/input/commands/remote-runtime-setup.ts +199 -0
  109. package/src/input/commands/remote-runtime.ts +431 -0
  110. package/src/input/commands/replay-runtime.ts +18 -0
  111. package/src/input/commands/runtime-services.ts +291 -0
  112. package/src/input/commands/schedule-runtime.ts +91 -0
  113. package/src/input/commands/services-runtime.ts +209 -0
  114. package/src/input/commands/session-content.ts +408 -0
  115. package/src/input/commands/session-workflow.ts +464 -0
  116. package/src/input/commands/session.ts +375 -0
  117. package/src/input/commands/settings-sync-runtime.ts +174 -0
  118. package/src/input/commands/share-runtime.ts +119 -0
  119. package/src/input/commands/shell-core.ts +307 -0
  120. package/src/input/commands/skills-runtime.ts +221 -0
  121. package/src/input/commands/subscription-runtime.ts +434 -0
  122. package/src/input/commands/tasks-runtime.ts +230 -0
  123. package/src/input/commands/teamwork-runtime.ts +339 -0
  124. package/src/input/commands/teleport-runtime.ts +57 -0
  125. package/src/input/commands/tts-runtime.ts +29 -0
  126. package/src/input/commands/work-plan-runtime.ts +169 -0
  127. package/src/input/commands.ts +131 -0
  128. package/src/input/feed-context-factory.ts +254 -0
  129. package/src/input/file-picker.ts +192 -0
  130. package/src/input/handler-command-route.ts +180 -0
  131. package/src/input/handler-content-actions.ts +497 -0
  132. package/src/input/handler-feed-routes.ts +648 -0
  133. package/src/input/handler-feed.ts +452 -0
  134. package/src/input/handler-interactions.ts +281 -0
  135. package/src/input/handler-modal-routes.ts +418 -0
  136. package/src/input/handler-modal-stack.ts +263 -0
  137. package/src/input/handler-modal-token-routes.ts +329 -0
  138. package/src/input/handler-onboarding-cloudflare.ts +391 -0
  139. package/src/input/handler-onboarding.ts +620 -0
  140. package/src/input/handler-picker-routes.ts +472 -0
  141. package/src/input/handler-prompt-buffer.ts +320 -0
  142. package/src/input/handler-shortcuts.ts +213 -0
  143. package/src/input/handler-ui-state.ts +372 -0
  144. package/src/input/handler.ts +729 -0
  145. package/src/input/input-history.ts +297 -0
  146. package/src/input/keybindings.ts +292 -0
  147. package/src/input/mcp-workspace.ts +554 -0
  148. package/src/input/model-picker-provider-filter.ts +28 -0
  149. package/src/input/model-picker-types.ts +137 -0
  150. package/src/input/model-picker.ts +797 -0
  151. package/src/input/onboarding/handler-onboarding-routes.ts +125 -0
  152. package/src/input/onboarding/onboarding-runtime-status.ts +87 -0
  153. package/src/input/onboarding/onboarding-wizard-apply.ts +277 -0
  154. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
  155. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
  156. package/src/input/onboarding/onboarding-wizard-constants.ts +158 -0
  157. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +130 -0
  158. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +762 -0
  159. package/src/input/onboarding/onboarding-wizard-helpers.ts +167 -0
  160. package/src/input/onboarding/onboarding-wizard-rules.ts +256 -0
  161. package/src/input/onboarding/onboarding-wizard-state.ts +365 -0
  162. package/src/input/onboarding/onboarding-wizard-steps.ts +798 -0
  163. package/src/input/onboarding/onboarding-wizard-types.ts +195 -0
  164. package/src/input/onboarding/onboarding-wizard.ts +711 -0
  165. package/src/input/panel-integration-actions.ts +78 -0
  166. package/src/input/profile-picker-modal.ts +222 -0
  167. package/src/input/search.ts +100 -0
  168. package/src/input/selection-modal.ts +163 -0
  169. package/src/input/selection.ts +135 -0
  170. package/src/input/session-picker-modal.ts +136 -0
  171. package/src/input/settings-modal-behavior.ts +37 -0
  172. package/src/input/settings-modal-secrets.ts +41 -0
  173. package/src/input/settings-modal-subscriptions.ts +95 -0
  174. package/src/input/settings-modal-types.ts +91 -0
  175. package/src/input/settings-modal.ts +793 -0
  176. package/src/input/submission-intent.ts +17 -0
  177. package/src/input/submission-router.ts +59 -0
  178. package/src/input/tts-settings-actions.ts +100 -0
  179. package/src/main.ts +792 -0
  180. package/src/mcp/runtime-reload.ts +81 -0
  181. package/src/panels/agent-inspector-panel.ts +521 -0
  182. package/src/panels/agent-inspector-shared.ts +94 -0
  183. package/src/panels/agent-logs-panel.ts +559 -0
  184. package/src/panels/agent-logs-shared.ts +129 -0
  185. package/src/panels/approval-panel.ts +150 -0
  186. package/src/panels/automation-control-panel.ts +212 -0
  187. package/src/panels/base-panel.ts +254 -0
  188. package/src/panels/builtin/agent.ts +117 -0
  189. package/src/panels/builtin/development.ts +31 -0
  190. package/src/panels/builtin/knowledge.ts +26 -0
  191. package/src/panels/builtin/operations.ts +349 -0
  192. package/src/panels/builtin/session.ts +129 -0
  193. package/src/panels/builtin/shared.ts +274 -0
  194. package/src/panels/builtin-panels.ts +23 -0
  195. package/src/panels/cockpit-panel.ts +183 -0
  196. package/src/panels/communication-panel.ts +153 -0
  197. package/src/panels/confirm-state.ts +61 -0
  198. package/src/panels/context-visualizer-panel.ts +204 -0
  199. package/src/panels/control-plane-panel.ts +211 -0
  200. package/src/panels/cost-tracker-panel.ts +444 -0
  201. package/src/panels/debug-panel.ts +432 -0
  202. package/src/panels/diff-panel.ts +520 -0
  203. package/src/panels/docs-panel.ts +283 -0
  204. package/src/panels/eval-panel.ts +399 -0
  205. package/src/panels/file-explorer-panel.ts +584 -0
  206. package/src/panels/file-preview-panel.ts +434 -0
  207. package/src/panels/forensics-panel.ts +364 -0
  208. package/src/panels/git-panel.ts +638 -0
  209. package/src/panels/hooks-panel.ts +239 -0
  210. package/src/panels/incident-review-panel.ts +197 -0
  211. package/src/panels/index.ts +46 -0
  212. package/src/panels/intelligence-panel.ts +176 -0
  213. package/src/panels/knowledge-panel.ts +345 -0
  214. package/src/panels/local-auth-panel.ts +130 -0
  215. package/src/panels/marketplace-panel.ts +212 -0
  216. package/src/panels/memory-panel.ts +225 -0
  217. package/src/panels/ops-control-panel.ts +150 -0
  218. package/src/panels/ops-strategy-panel.ts +235 -0
  219. package/src/panels/orchestration-panel.ts +273 -0
  220. package/src/panels/panel-list-panel.ts +509 -0
  221. package/src/panels/panel-manager.ts +570 -0
  222. package/src/panels/panel-picker.ts +106 -0
  223. package/src/panels/plan-dashboard-panel.ts +274 -0
  224. package/src/panels/plugins-panel.ts +178 -0
  225. package/src/panels/policy-panel.ts +308 -0
  226. package/src/panels/polish.ts +717 -0
  227. package/src/panels/project-planning-panel.ts +711 -0
  228. package/src/panels/provider-account-snapshot.ts +259 -0
  229. package/src/panels/provider-accounts-panel.ts +218 -0
  230. package/src/panels/provider-health-domains.ts +215 -0
  231. package/src/panels/provider-health-panel.ts +727 -0
  232. package/src/panels/provider-health-tracker.ts +115 -0
  233. package/src/panels/provider-stats-panel.ts +366 -0
  234. package/src/panels/qr-panel.ts +182 -0
  235. package/src/panels/remote-panel.ts +449 -0
  236. package/src/panels/routes-panel.ts +178 -0
  237. package/src/panels/sandbox-panel.ts +283 -0
  238. package/src/panels/schedule-panel.ts +329 -0
  239. package/src/panels/scrollable-list-panel.ts +491 -0
  240. package/src/panels/search-focus.ts +32 -0
  241. package/src/panels/security-panel.ts +295 -0
  242. package/src/panels/services-panel.ts +231 -0
  243. package/src/panels/session-browser-panel.ts +400 -0
  244. package/src/panels/session-maintenance.ts +125 -0
  245. package/src/panels/settings-sync-panel.ts +120 -0
  246. package/src/panels/skills-panel.ts +431 -0
  247. package/src/panels/subscription-panel.ts +263 -0
  248. package/src/panels/symbol-outline-panel.ts +486 -0
  249. package/src/panels/system-messages-panel.ts +230 -0
  250. package/src/panels/tasks-panel.ts +399 -0
  251. package/src/panels/thinking-panel.ts +304 -0
  252. package/src/panels/token-budget-panel.ts +475 -0
  253. package/src/panels/tool-inspector-panel.ts +429 -0
  254. package/src/panels/types.ts +54 -0
  255. package/src/panels/watchers-panel.ts +193 -0
  256. package/src/panels/work-plan-panel.ts +175 -0
  257. package/src/panels/worktree-panel.ts +182 -0
  258. package/src/panels/wrfc-panel.ts +609 -0
  259. package/src/permissions/prompt.ts +165 -0
  260. package/src/planning/project-planning-coordinator.ts +543 -0
  261. package/src/plugins/loader.ts +15 -0
  262. package/src/renderer/agent-detail-modal.ts +331 -0
  263. package/src/renderer/agent-workspace.ts +238 -0
  264. package/src/renderer/ansi-sanitize.ts +76 -0
  265. package/src/renderer/autocomplete-overlay.ts +154 -0
  266. package/src/renderer/block-actions.ts +76 -0
  267. package/src/renderer/bookmark-modal.ts +101 -0
  268. package/src/renderer/bottom-bar.ts +58 -0
  269. package/src/renderer/buffer.ts +113 -0
  270. package/src/renderer/code-block.ts +373 -0
  271. package/src/renderer/compositor.ts +283 -0
  272. package/src/renderer/context-inspector.ts +219 -0
  273. package/src/renderer/conversation-layout.ts +67 -0
  274. package/src/renderer/conversation-overlays.ts +140 -0
  275. package/src/renderer/conversation-surface.ts +260 -0
  276. package/src/renderer/diff-view.ts +132 -0
  277. package/src/renderer/diff.ts +130 -0
  278. package/src/renderer/file-picker-overlay.ts +101 -0
  279. package/src/renderer/file-tree.ts +153 -0
  280. package/src/renderer/fullscreen-primitives.ts +130 -0
  281. package/src/renderer/fullscreen-workspace.ts +199 -0
  282. package/src/renderer/git-status.ts +89 -0
  283. package/src/renderer/help-overlay.ts +267 -0
  284. package/src/renderer/history-search-overlay.ts +73 -0
  285. package/src/renderer/layout-engine.ts +97 -0
  286. package/src/renderer/layout.ts +32 -0
  287. package/src/renderer/live-tail-modal.ts +156 -0
  288. package/src/renderer/markdown.ts +635 -0
  289. package/src/renderer/mcp-workspace.ts +237 -0
  290. package/src/renderer/modal-factory.ts +467 -0
  291. package/src/renderer/modal-utils.ts +24 -0
  292. package/src/renderer/model-picker-overlay.ts +473 -0
  293. package/src/renderer/model-workspace.ts +488 -0
  294. package/src/renderer/onboarding/onboarding-wizard.ts +615 -0
  295. package/src/renderer/overlay-box.ts +146 -0
  296. package/src/renderer/overlay-viewport.ts +104 -0
  297. package/src/renderer/panel-composite.ts +158 -0
  298. package/src/renderer/panel-picker-overlay.ts +202 -0
  299. package/src/renderer/panel-tab-bar.ts +69 -0
  300. package/src/renderer/panel-workspace-bar.ts +42 -0
  301. package/src/renderer/process-indicator.ts +96 -0
  302. package/src/renderer/process-modal.ts +656 -0
  303. package/src/renderer/process-summary.ts +67 -0
  304. package/src/renderer/profile-picker-modal.ts +129 -0
  305. package/src/renderer/progress.ts +98 -0
  306. package/src/renderer/qr-renderer.ts +120 -0
  307. package/src/renderer/search-overlay.ts +54 -0
  308. package/src/renderer/selection-modal-overlay.ts +214 -0
  309. package/src/renderer/semantic-diff.ts +369 -0
  310. package/src/renderer/session-picker-modal.ts +127 -0
  311. package/src/renderer/settings-modal-helpers.ts +193 -0
  312. package/src/renderer/settings-modal.ts +537 -0
  313. package/src/renderer/shell-surface.ts +88 -0
  314. package/src/renderer/status-glyphs.ts +21 -0
  315. package/src/renderer/status-token.ts +67 -0
  316. package/src/renderer/surface-layout.ts +101 -0
  317. package/src/renderer/syntax-highlighter.ts +542 -0
  318. package/src/renderer/system-message.ts +83 -0
  319. package/src/renderer/tab-strip.ts +108 -0
  320. package/src/renderer/text-layout.ts +31 -0
  321. package/src/renderer/thinking.ts +17 -0
  322. package/src/renderer/tool-call.ts +234 -0
  323. package/src/renderer/ui-factory.ts +524 -0
  324. package/src/renderer/ui-primitives.ts +96 -0
  325. package/src/runtime/bootstrap-command-context.ts +278 -0
  326. package/src/runtime/bootstrap-command-parts.ts +386 -0
  327. package/src/runtime/bootstrap-core.ts +540 -0
  328. package/src/runtime/bootstrap-hook-bridge.ts +112 -0
  329. package/src/runtime/bootstrap-shell.ts +283 -0
  330. package/src/runtime/bootstrap.ts +575 -0
  331. package/src/runtime/cloudflare-control-plane.ts +349 -0
  332. package/src/runtime/context.ts +142 -0
  333. package/src/runtime/diagnostics/panels/index.ts +24 -0
  334. package/src/runtime/diagnostics/panels/ops.ts +156 -0
  335. package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
  336. package/src/runtime/diagnostics/panels/policy.ts +177 -0
  337. package/src/runtime/index.ts +662 -0
  338. package/src/runtime/onboarding/apply.ts +642 -0
  339. package/src/runtime/onboarding/derivation.ts +534 -0
  340. package/src/runtime/onboarding/index.ts +7 -0
  341. package/src/runtime/onboarding/markers.ts +148 -0
  342. package/src/runtime/onboarding/snapshot.ts +406 -0
  343. package/src/runtime/onboarding/state.ts +141 -0
  344. package/src/runtime/onboarding/types.ts +404 -0
  345. package/src/runtime/onboarding/verify.ts +171 -0
  346. package/src/runtime/operator-token-cleanup.ts +27 -0
  347. package/src/runtime/perf/panel-contracts.ts +32 -0
  348. package/src/runtime/perf/panel-health-monitor.ts +18 -0
  349. package/src/runtime/sandbox-public-gaps.ts +358 -0
  350. package/src/runtime/services.ts +670 -0
  351. package/src/runtime/store/domains/domain-read-matrix.ts +15 -0
  352. package/src/runtime/store/domains/index.ts +222 -0
  353. package/src/runtime/store/domains/panels.ts +117 -0
  354. package/src/runtime/store/domains/ui-perf.ts +103 -0
  355. package/src/runtime/store/index.ts +305 -0
  356. package/src/runtime/store/selectors/index.ts +359 -0
  357. package/src/runtime/store/state.ts +145 -0
  358. package/src/runtime/surface-feature-flags.ts +65 -0
  359. package/src/runtime/terminal-output-guard.ts +228 -0
  360. package/src/runtime/ui/index.ts +39 -0
  361. package/src/runtime/ui/model-picker/data-provider.ts +182 -0
  362. package/src/runtime/ui/model-picker/health-enrichment.ts +228 -0
  363. package/src/runtime/ui/model-picker/index.ts +59 -0
  364. package/src/runtime/ui/model-picker/types.ts +149 -0
  365. package/src/runtime/ui/provider-health/data-provider.ts +244 -0
  366. package/src/runtime/ui/provider-health/fallback-visualizer.ts +71 -0
  367. package/src/runtime/ui/provider-health/index.ts +46 -0
  368. package/src/runtime/ui/provider-health/types.ts +146 -0
  369. package/src/runtime/ui-events.ts +1 -0
  370. package/src/runtime/ui-read-model-helpers.ts +1 -0
  371. package/src/runtime/ui-read-models-observability-maintenance.ts +1 -0
  372. package/src/runtime/ui-read-models-observability-options.ts +1 -0
  373. package/src/runtime/ui-read-models-observability-remote.ts +1 -0
  374. package/src/runtime/ui-read-models-observability-security.ts +1 -0
  375. package/src/runtime/ui-read-models-observability-system.ts +1 -0
  376. package/src/runtime/ui-read-models-observability.ts +1 -0
  377. package/src/runtime/ui-read-models.ts +61 -0
  378. package/src/runtime/ui-service-queries.ts +1 -0
  379. package/src/runtime/ui-services.ts +190 -0
  380. package/src/scripts/process-messages.ts +42 -0
  381. package/src/shell/blocking-input.ts +98 -0
  382. package/src/shell/service-settings-sync.ts +273 -0
  383. package/src/shell/ui-openers.ts +352 -0
  384. package/src/tools/index.ts +1 -0
  385. package/src/tools/wrfc-agent-guard.ts +49 -0
  386. package/src/types/grid.ts +48 -0
  387. package/src/types/sql-js.d.ts +15 -0
  388. package/src/utils/clipboard.ts +22 -0
  389. package/src/utils/splash-lines.ts +46 -0
  390. package/src/utils/terminal-width.ts +185 -0
  391. package/src/verification/live-verifier.ts +430 -0
  392. package/src/verification/verification-ledger.ts +242 -0
  393. package/src/version.ts +17 -0
  394. package/src/widget/index.ts +2 -0
  395. package/src/widget/types.ts +9 -0
  396. package/src/widget/widget.ts +8 -0
  397. package/src/work-plans/work-plan-store.ts +374 -0
  398. package/tsconfig.json +18 -0
@@ -0,0 +1,656 @@
1
+ import { type Line } from '../types/grid.ts';
2
+ import { ModalFactory } from './modal-factory.ts';
3
+ import { formatDuration } from './modal-utils.ts';
4
+ import type { ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
5
+ import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
6
+ import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
7
+ import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
8
+ import { getVisibleWindow } from './surface-layout.ts';
9
+
10
+ // ─── ProcessEntry ─────────────────────────────────────────────────────────────
11
+
12
+ export interface ProcessEntry {
13
+ /** Unique process identifier */
14
+ id: string;
15
+ /** Display label (agent task or exec command) */
16
+ label: string;
17
+ /** Tree prefix for child processes, e.g. "└─ " under a WRFC owner. */
18
+ treePrefix?: string;
19
+ /** Process type */
20
+ type: 'agent' | 'exec';
21
+ /** Current status string */
22
+ status: string;
23
+ /** Elapsed milliseconds since start */
24
+ elapsedMs: number;
25
+ /** Live streaming snippet for tracked delegated sessions (last ~60 chars of current turn output). */
26
+ streamSnippet?: string;
27
+ }
28
+
29
+ // ─── Constants ────────────────────────────────────────────────────────────────
30
+
31
+ /** Maximum characters from agent task / exec command stored in ProcessEntry.label. */
32
+ const MAX_LABEL_LENGTH = 80;
33
+ /** Border and margin width subtracted from terminal width to get modal content width. */
34
+ const MODAL_BORDER_WIDTH = 8;
35
+
36
+ const WRFC_ROLE_ORDER: Record<string, number> = {
37
+ owner: 0,
38
+ engineer: 1,
39
+ reviewer: 2,
40
+ fixer: 3,
41
+ verifier: 4,
42
+ };
43
+
44
+ export interface ProcessModalDeps {
45
+ readonly agentManager: Pick<AgentManager, 'list' | 'getStatus' | 'cancel'>;
46
+ readonly processManager: Pick<ProcessManager, 'list' | 'getStatus' | 'stop'>;
47
+ readonly wrfcController: Pick<WrfcController, 'getChain'> & Partial<Pick<WrfcController, 'listChains'>>;
48
+ }
49
+
50
+ type WrfcChainLike = {
51
+ readonly id: string;
52
+ readonly state: string;
53
+ readonly task: string;
54
+ readonly ownerAgentId: string;
55
+ readonly engineerAgentId?: string;
56
+ readonly reviewerAgentId?: string;
57
+ readonly fixerAgentId?: string;
58
+ readonly allAgentIds?: readonly string[];
59
+ readonly constraints?: readonly unknown[];
60
+ };
61
+
62
+ /** Build a display label for an agent based on its task and template. */
63
+ function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
64
+ const task = rec.task;
65
+
66
+ // Look up the original task from the WRFC chain if available
67
+ const originalTask = getChainTask(rec.wrfcId, deps);
68
+
69
+ if (rec.wrfcRole === 'owner') {
70
+ const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
71
+ return `[WRFC owner] ${desc}`;
72
+ }
73
+
74
+ if (rec.wrfcRole === 'engineer') {
75
+ const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 11);
76
+ return `[Engineer] ${desc}`;
77
+ }
78
+
79
+ if (rec.wrfcRole === 'verifier') {
80
+ const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
81
+ return `[Verifier] ${desc}`;
82
+ }
83
+
84
+ // WRFC Review agent
85
+ if (task.startsWith('WRFC Review Request')) {
86
+ const thresholdMatch = task.match(/threshold is (\d+(?:\.\d+)?)/);
87
+ const threshold = thresholdMatch ? thresholdMatch[1] : '9.9';
88
+ const desc = truncateFirst(originalTask ?? 'review in progress', 50);
89
+ return `[Review] ${desc} (target: ${threshold}/10)`;
90
+ }
91
+
92
+ // WRFC Fix agent
93
+ if (task.startsWith('WRFC Fix Request')) {
94
+ const scoreMatch = task.match(/Review score:\s*(\d+(?:\.\d+)?)\/(\d+)\s*\(threshold:\s*(\d+(?:\.\d+)?)/);
95
+ const fromScore = scoreMatch ? scoreMatch[1] : '?';
96
+ const toScore = scoreMatch ? scoreMatch[3] : '?';
97
+ const attemptMatch = task.match(/Fix attempt:\s*(\d+)/);
98
+ const attempt = attemptMatch ? attemptMatch[1] : '?';
99
+ const desc = truncateFirst(originalTask ?? 'fix in progress', 45);
100
+ // Show constraint count when the chain has constraints to target (SDK 0.23.0)
101
+ const chain = rec.wrfcId ? safeGetChain(rec.wrfcId, deps) : null;
102
+ const constraintCount = chain && (chain.constraints?.length ?? 0) > 0 ? chain.constraints?.length ?? 0 : 0;
103
+ const constraintSuffix = constraintCount > 0 ? ` [${constraintCount}c]` : '';
104
+ return `[Fix #${attempt}] ${desc} (${fromScore} \u2192 ${toScore}/10)${constraintSuffix}`;
105
+ }
106
+
107
+ // Regular agent — show template and truncated first line
108
+ const templateLabels: Record<string, string> = {
109
+ engineer: 'Engineer', reviewer: 'Reviewer', tester: 'Tester',
110
+ researcher: 'Researcher', general: 'Agent',
111
+ };
112
+ const tag = templateLabels[rec.template] ?? 'Agent';
113
+ const maxDesc = MAX_LABEL_LENGTH - tag.length - 3;
114
+ return `[${tag}] ${truncateFirst(task, maxDesc)}`;
115
+ }
116
+
117
+ function isActiveAgent(rec: AgentRecord): boolean {
118
+ return rec.status !== 'completed' && rec.status !== 'failed' && rec.status !== 'cancelled';
119
+ }
120
+
121
+ function isActiveWrfcState(state: string): boolean {
122
+ return state !== 'passed' && state !== 'failed';
123
+ }
124
+
125
+ function getStreamSnippet(rec: AgentRecord): string | undefined {
126
+ if (!rec.streamingContent) return undefined;
127
+ const raw = rec.streamingContent.replace(/\n/g, ' ').trim();
128
+ return raw.length > 60 ? '...' + raw.slice(-57) : raw;
129
+ }
130
+
131
+ function compareAgents(a: AgentRecord, b: AgentRecord): number {
132
+ const roleDelta = (WRFC_ROLE_ORDER[a.wrfcRole ?? ''] ?? 50) - (WRFC_ROLE_ORDER[b.wrfcRole ?? ''] ?? 50);
133
+ if (roleDelta !== 0) return roleDelta;
134
+ return a.startedAt - b.startedAt || a.id.localeCompare(b.id);
135
+ }
136
+
137
+ function buildAgentEntry(
138
+ rec: AgentRecord,
139
+ deps: ProcessModalDeps,
140
+ now: number,
141
+ treePrefix = '',
142
+ ): ProcessEntry {
143
+ return {
144
+ id: rec.id,
145
+ label: buildAgentLabel(rec, deps),
146
+ treePrefix,
147
+ type: 'agent',
148
+ status: rec.status,
149
+ elapsedMs: now - rec.startedAt,
150
+ streamSnippet: getStreamSnippet(rec),
151
+ };
152
+ }
153
+
154
+ function appendAgentSubtree(
155
+ result: ProcessEntry[],
156
+ rec: AgentRecord,
157
+ childrenByParent: Map<string, AgentRecord[]>,
158
+ deps: ProcessModalDeps,
159
+ now: number,
160
+ prefix: string,
161
+ connector: string,
162
+ visited: Set<string>,
163
+ ): void {
164
+ if (visited.has(rec.id)) return;
165
+ visited.add(rec.id);
166
+ result.push(buildAgentEntry(rec, deps, now, `${prefix}${connector}`));
167
+
168
+ const children = (childrenByParent.get(rec.id) ?? []).slice().sort(compareAgents);
169
+ const descendantPrefix = connector === '├─ ' ? '│ ' : connector === '└─ ' ? ' ' : '';
170
+ children.forEach((child, index) => {
171
+ const last = index === children.length - 1;
172
+ appendAgentSubtree(
173
+ result,
174
+ child,
175
+ childrenByParent,
176
+ deps,
177
+ now,
178
+ `${prefix}${descendantPrefix}`,
179
+ last ? '└─ ' : '├─ ',
180
+ visited,
181
+ );
182
+ });
183
+ }
184
+
185
+ function appendAgentGroupEntries(
186
+ result: ProcessEntry[],
187
+ records: AgentRecord[],
188
+ deps: ProcessModalDeps,
189
+ now: number,
190
+ ): void {
191
+ const group = records.slice().sort(compareAgents);
192
+ const byId = new Map(group.map((rec) => [rec.id, rec]));
193
+ const childrenByParent = new Map<string, AgentRecord[]>();
194
+
195
+ for (const rec of group) {
196
+ if (!rec.parentAgentId || !byId.has(rec.parentAgentId)) continue;
197
+ const children = childrenByParent.get(rec.parentAgentId) ?? [];
198
+ children.push(rec);
199
+ childrenByParent.set(rec.parentAgentId, children);
200
+ }
201
+
202
+ const chain = group[0]?.wrfcId ? safeGetChain(group[0].wrfcId, deps) : null;
203
+ const owner = group.find((rec) => rec.id === chain?.ownerAgentId)
204
+ ?? group.find((rec) => rec.wrfcRole === 'owner');
205
+ const roots = owner
206
+ ? [owner]
207
+ : group.filter((rec) => !rec.parentAgentId || !byId.has(rec.parentAgentId));
208
+ const visited = new Set<string>();
209
+
210
+ roots.forEach((root, index) => {
211
+ const connector = owner || roots.length === 1 ? '' : (index === roots.length - 1 ? '└─ ' : '├─ ');
212
+ appendAgentSubtree(result, root, childrenByParent, deps, now, '', connector, visited);
213
+ });
214
+
215
+ const leftovers = group.filter((rec) => !visited.has(rec.id));
216
+ leftovers.forEach((rec, index) => {
217
+ appendAgentSubtree(
218
+ result,
219
+ rec,
220
+ childrenByParent,
221
+ deps,
222
+ now,
223
+ '',
224
+ index === leftovers.length - 1 ? '└─ ' : '├─ ',
225
+ visited,
226
+ );
227
+ });
228
+ }
229
+
230
+ function buildAgentEntries(
231
+ agents: AgentRecord[],
232
+ deps: ProcessModalDeps,
233
+ now: number,
234
+ getGroupOrder?: (key: string) => number | undefined,
235
+ ensureGroupOrder?: (key: string) => number,
236
+ ): ProcessEntry[] {
237
+ const result: ProcessEntry[] = [];
238
+ const displayAgents = prepareAgentRecordsForDisplay(agents, deps);
239
+ const activeById = new Map(displayAgents.map((agent) => [agent.id, agent]));
240
+ const groups = new Map<string, AgentRecord[]>();
241
+
242
+ for (const agent of displayAgents) {
243
+ const groupKey = getAgentGroupKey(agent, activeById);
244
+ const group = groups.get(groupKey) ?? [];
245
+ group.push(agent);
246
+ groups.set(groupKey, group);
247
+ }
248
+
249
+ const sortedGroups = Array.from(groups.entries()).sort(([aKey, a], [bKey, b]) => {
250
+ const aOrder = getGroupOrder?.(aKey);
251
+ const bOrder = getGroupOrder?.(bKey);
252
+ if (aOrder !== undefined || bOrder !== undefined) {
253
+ if (aOrder === undefined) return 1;
254
+ if (bOrder === undefined) return -1;
255
+ return aOrder - bOrder;
256
+ }
257
+ const aStarted = Math.min(...a.map((rec) => rec.startedAt));
258
+ const bStarted = Math.min(...b.map((rec) => rec.startedAt));
259
+ return aStarted - bStarted || aKey.localeCompare(bKey);
260
+ });
261
+ for (const [key, group] of sortedGroups) {
262
+ ensureGroupOrder?.(key);
263
+ appendAgentGroupEntries(result, group, deps, now);
264
+ }
265
+
266
+ return result;
267
+ }
268
+
269
+ function prepareAgentRecordsForDisplay(agents: AgentRecord[], deps: ProcessModalDeps): AgentRecord[] {
270
+ const chains = listWrfcChains(deps);
271
+ const agentById = new Map(agents.map((agent) => [agent.id, agent]));
272
+ const normalizedById = new Map<string, AgentRecord>();
273
+
274
+ for (const agent of agents) {
275
+ if (!isActiveAgent(agent)) continue;
276
+ normalizedById.set(agent.id, normalizeWrfcAgentRecord(agent, chains));
277
+ }
278
+
279
+ // A WRFC owner is the durable root of the chain. Keep it visible until the
280
+ // chain itself is terminal, even if the underlying owner agent has already
281
+ // emitted a completed phase event before reviewer/fixer/gate work finishes.
282
+ for (const chain of chains) {
283
+ if (!isActiveWrfcState(chain.state)) continue;
284
+ const owner = agentById.get(chain.ownerAgentId);
285
+ if (!owner || normalizedById.has(owner.id)) continue;
286
+ const chainHasActiveMember = agents.some((agent) =>
287
+ agent.id !== owner.id
288
+ && isActiveAgent(agent)
289
+ && isAgentInChain(agent, chain)
290
+ );
291
+ if (!chainHasActiveMember) continue;
292
+ normalizedById.set(owner.id, normalizeWrfcAgentRecord({
293
+ ...owner,
294
+ status: 'running',
295
+ completedAt: undefined,
296
+ progress: owner.progress ?? `WRFC chain ${chain.state}`,
297
+ }, chains));
298
+ }
299
+
300
+ const normalized = Array.from(normalizedById.values());
301
+ return inferDuplicateWrfcOwnerRows(normalized);
302
+ }
303
+
304
+ function normalizeWrfcAgentRecord(agent: AgentRecord, chains: WrfcChainLike[]): AgentRecord {
305
+ const chain = findChainForAgent(agent, chains);
306
+ if (!chain) return agent;
307
+
308
+ const role = inferWrfcRole(agent, chain);
309
+ const parentAgentId = role && role !== 'owner'
310
+ ? agent.parentAgentId ?? chain.ownerAgentId
311
+ : agent.parentAgentId;
312
+
313
+ return {
314
+ ...agent,
315
+ wrfcId: agent.wrfcId ?? chain.id,
316
+ wrfcRole: agent.wrfcRole ?? role,
317
+ parentAgentId,
318
+ };
319
+ }
320
+
321
+ function inferDuplicateWrfcOwnerRows(agents: AgentRecord[]): AgentRecord[] {
322
+ const byTask = new Map<string, AgentRecord[]>();
323
+ for (const agent of agents) {
324
+ if (agent.wrfcId || agent.wrfcRole || agent.parentAgentId) continue;
325
+ if (agent.reviewMode !== 'wrfc') continue;
326
+ const key = agent.task.trim();
327
+ if (!key) continue;
328
+ const group = byTask.get(key) ?? [];
329
+ group.push(agent);
330
+ byTask.set(key, group);
331
+ }
332
+
333
+ const inferredIds = new Set<string>();
334
+ const inferred = new Map<string, AgentRecord>();
335
+ for (const [task, group] of byTask) {
336
+ if (group.length < 2) continue;
337
+ const sorted = group.slice().sort((a, b) => a.startedAt - b.startedAt || a.id.localeCompare(b.id));
338
+ const owner = sorted[0]!;
339
+ const syntheticWrfcId = `inferred:${owner.id}`;
340
+ inferred.set(owner.id, {
341
+ ...owner,
342
+ wrfcId: syntheticWrfcId,
343
+ wrfcRole: 'owner',
344
+ });
345
+ inferredIds.add(owner.id);
346
+ for (const child of sorted.slice(1)) {
347
+ inferred.set(child.id, {
348
+ ...child,
349
+ wrfcId: syntheticWrfcId,
350
+ wrfcRole: child.template === 'reviewer' ? 'reviewer' : 'engineer',
351
+ parentAgentId: owner.id,
352
+ });
353
+ inferredIds.add(child.id);
354
+ }
355
+
356
+ // Avoid accidentally grouping unrelated long-running WRFC roots that just
357
+ // happen to share an empty or generic task after this exact duplicate group.
358
+ byTask.delete(task);
359
+ }
360
+
361
+ if (inferredIds.size === 0) return agents;
362
+ return agents.map((agent) => inferred.get(agent.id) ?? agent);
363
+ }
364
+
365
+ function listWrfcChains(deps: ProcessModalDeps): WrfcChainLike[] {
366
+ const controller = deps.wrfcController as ProcessModalDeps['wrfcController'] & {
367
+ listChains?: () => unknown;
368
+ };
369
+ if (typeof controller.listChains !== 'function') return [];
370
+ try {
371
+ const value = controller.listChains();
372
+ return Array.isArray(value) ? value.filter(isWrfcChainLike) : [];
373
+ } catch {
374
+ return [];
375
+ }
376
+ }
377
+
378
+ function isWrfcChainLike(value: unknown): value is WrfcChainLike {
379
+ if (!value || typeof value !== 'object') return false;
380
+ const record = value as Record<string, unknown>;
381
+ return typeof record.id === 'string'
382
+ && typeof record.state === 'string'
383
+ && typeof record.task === 'string'
384
+ && typeof record.ownerAgentId === 'string';
385
+ }
386
+
387
+ function findChainForAgent(agent: AgentRecord, chains: WrfcChainLike[]): WrfcChainLike | null {
388
+ if (agent.wrfcId) {
389
+ const direct = chains.find((chain) => chain.id === agent.wrfcId);
390
+ if (direct) return direct;
391
+ }
392
+ return chains.find((chain) => isAgentInChain(agent, chain)) ?? null;
393
+ }
394
+
395
+ function isAgentInChain(agent: AgentRecord, chain: WrfcChainLike): boolean {
396
+ return chain.ownerAgentId === agent.id
397
+ || chain.engineerAgentId === agent.id
398
+ || chain.reviewerAgentId === agent.id
399
+ || chain.fixerAgentId === agent.id
400
+ || (chain.allAgentIds?.includes(agent.id) ?? false)
401
+ || agent.wrfcId === chain.id;
402
+ }
403
+
404
+ function inferWrfcRole(agent: AgentRecord, chain: WrfcChainLike): AgentRecord['wrfcRole'] {
405
+ if (agent.wrfcRole) return agent.wrfcRole;
406
+ if (chain.ownerAgentId === agent.id) return 'owner';
407
+ if (chain.engineerAgentId === agent.id) return 'engineer';
408
+ if (chain.reviewerAgentId === agent.id) return 'reviewer';
409
+ if (chain.fixerAgentId === agent.id) return 'fixer';
410
+ if (agent.template === 'reviewer') return 'reviewer';
411
+ return 'engineer';
412
+ }
413
+
414
+ function getAgentGroupKey(agent: AgentRecord, activeById: Map<string, AgentRecord>): string {
415
+ if (agent.wrfcId) return `wrfc:${agent.wrfcId}`;
416
+
417
+ const seen = new Set<string>();
418
+ let root = agent;
419
+ while (root.parentAgentId && activeById.has(root.parentAgentId) && !seen.has(root.parentAgentId)) {
420
+ seen.add(root.id);
421
+ root = activeById.get(root.parentAgentId)!;
422
+ }
423
+
424
+ // If the active root is an orphaned child, keep it anchored to its missing parent id
425
+ // so it does not jump to a new group when the parent exits before its children.
426
+ return `root:${root.parentAgentId ?? root.id}`;
427
+ }
428
+
429
+ function safeGetChain(wrfcId: string, deps: Pick<ProcessModalDeps, 'wrfcController'>): WrfcChainLike | null {
430
+ try {
431
+ const chain = deps.wrfcController.getChain(wrfcId);
432
+ return isWrfcChainLike(chain) ? chain : null;
433
+ } catch {
434
+ return null;
435
+ }
436
+ }
437
+
438
+ /** Get the original task description from a WRFC chain. */
439
+ function getChainTask(wrfcId: string | undefined, deps: Pick<ProcessModalDeps, 'wrfcController'>): string | null {
440
+ if (!wrfcId) return null;
441
+ return safeGetChain(wrfcId, deps)?.task ?? null;
442
+ }
443
+
444
+ /** Truncate to first line, capped at max chars. */
445
+ function truncateFirst(text: string, max: number): string {
446
+ const line = text.split('\n')[0].trim();
447
+ return line.length > max ? line.slice(0, Math.max(0, max - 3)) + '...' : line;
448
+ }
449
+
450
+ /** Truncate a command string to first line, capped at MAX_LABEL_LENGTH. */
451
+ function truncateCmd(text: string): string {
452
+ const firstLine = text.split('\n')[0].trim();
453
+ if (firstLine.length > MAX_LABEL_LENGTH) return firstLine.slice(0, MAX_LABEL_LENGTH - 3) + '...';
454
+ return firstLine;
455
+ }
456
+
457
+ // ─── ProcessModalState ────────────────────────────────────────────────────────
458
+
459
+ /**
460
+ * ProcessModal — manages the state for the background-process list modal.
461
+ *
462
+ * Holds the list of ProcessEntry items, selected index, and active flag.
463
+ * Rendering is done by renderProcessModal().
464
+ */
465
+ export class ProcessModal {
466
+ public active = false;
467
+ public selectedIndex = 0;
468
+ public entries: ProcessEntry[] = [];
469
+ private refreshTimer: ReturnType<typeof setInterval> | null = null;
470
+ private onRefresh: (() => void) | null = null;
471
+ private groupOrder = new Map<string, number>();
472
+ private nextGroupOrder = 0;
473
+
474
+ constructor(private readonly deps: ProcessModalDeps) {}
475
+
476
+ /** Set a callback to trigger re-render on timer tick. */
477
+ setOnRefresh(fn: () => void): void {
478
+ this.onRefresh = fn;
479
+ }
480
+
481
+ open(): void {
482
+ this.refresh();
483
+ this.active = true;
484
+ this.selectedIndex = 0;
485
+ if (this.refreshTimer) clearInterval(this.refreshTimer);
486
+ this.refreshTimer = setInterval(() => {
487
+ this.refresh();
488
+ this.onRefresh?.();
489
+ }, 1000);
490
+ }
491
+
492
+ close(): void {
493
+ this.active = false;
494
+ if (this.refreshTimer) {
495
+ clearInterval(this.refreshTimer);
496
+ this.refreshTimer = null;
497
+ }
498
+ }
499
+
500
+ /** Rebuild entries from the currently owned runtime services. */
501
+ refresh(): void {
502
+ const manager = this.deps.agentManager;
503
+ if (typeof manager?.list !== 'function') return; // Guard against test mock pollution
504
+ const now = Date.now();
505
+ const result: ProcessEntry[] = [];
506
+
507
+ // Agents — only show active (pending/running), grouped by stable parent/child hierarchy.
508
+ result.push(...buildAgentEntries(
509
+ manager.list(),
510
+ this.deps,
511
+ now,
512
+ (key) => this.groupOrder.get(key),
513
+ (key) => this.ensureGroupOrder(key),
514
+ ));
515
+
516
+ // Background exec processes — only show running
517
+ const pm = this.deps.processManager;
518
+ for (const p of pm.list()) {
519
+ if (p.status.startsWith('done')) continue;
520
+ const startTime = pm.getStatus(p.id)?.startTime ?? now;
521
+ result.push({
522
+ id: p.id,
523
+ label: truncateCmd(p.cmd),
524
+ type: 'exec',
525
+ status: p.status,
526
+ elapsedMs: now - startTime,
527
+ });
528
+ }
529
+
530
+ this.entries = result;
531
+
532
+ // Keep selection in-bounds
533
+ if (this.selectedIndex >= this.entries.length) {
534
+ this.selectedIndex = Math.max(0, this.entries.length - 1);
535
+ }
536
+ }
537
+
538
+ private ensureGroupOrder(key: string): number {
539
+ const existing = this.groupOrder.get(key);
540
+ if (existing !== undefined) return existing;
541
+ const next = this.nextGroupOrder++;
542
+ this.groupOrder.set(key, next);
543
+ return next;
544
+ }
545
+
546
+ moveUp(): void {
547
+ if (this.entries.length === 0) return;
548
+ this.selectedIndex = (this.selectedIndex - 1 + this.entries.length) % this.entries.length;
549
+ }
550
+
551
+ moveDown(): void {
552
+ if (this.entries.length === 0) return;
553
+ this.selectedIndex = (this.selectedIndex + 1) % this.entries.length;
554
+ }
555
+
556
+ getSelected(): ProcessEntry | undefined {
557
+ return this.entries[this.selectedIndex];
558
+ }
559
+
560
+ /**
561
+ * Kill the selected process.
562
+ * Returns true if a process was killed, false otherwise.
563
+ */
564
+ killSelected(): boolean {
565
+ const entry = this.getSelected();
566
+ if (!entry) return false;
567
+
568
+ if (entry.type === 'exec') {
569
+ return this.deps.processManager.stop(entry.id);
570
+ } else {
571
+ return this.deps.agentManager.cancel(entry.id);
572
+ }
573
+ }
574
+ }
575
+
576
+ // ─── renderProcessModal ───────────────────────────────────────────────────────
577
+
578
+ /**
579
+ * Render the process list modal as Line[] for overlay in the viewport.
580
+ *
581
+ * @param modal ProcessModal state
582
+ * @param width Terminal width
583
+ */
584
+ export function renderProcessModal(modal: ProcessModal, width: number, viewportHeight = 24): Line[] {
585
+ modal.refresh();
586
+
587
+ const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
588
+ margin: 2,
589
+ maxWidth: Math.max(24, width - 4),
590
+ chromeRows: 4,
591
+ minContentRows: 5,
592
+ maxContentRows: 9,
593
+ });
594
+ const boxMargin = metrics.margin;
595
+ const boxW = metrics.boxWidth;
596
+ const maxVisibleRows = metrics.contentRows;
597
+ const targetContentRows = getStableOverlayContentRows(metrics.contentRows, 7);
598
+
599
+ if (modal.entries.length === 0) {
600
+ return ModalFactory.createModal({
601
+ title: 'Background Processes',
602
+ width: boxW,
603
+ margin: boxMargin,
604
+ targetContentRows,
605
+ sections: [
606
+ { type: 'text', content: 'No background processes running.' },
607
+ ],
608
+ hints: ['[Esc] Close'],
609
+ }, width);
610
+ }
611
+
612
+ const maxLabelW = Math.max(10, boxW - MODAL_BORDER_WIDTH);
613
+ const window = getVisibleWindow(modal.entries.length, modal.selectedIndex, maxVisibleRows);
614
+ const visibleEntries = modal.entries.slice(window.start, window.end);
615
+
616
+ const items = visibleEntries.map((e, i) => {
617
+ const absoluteIndex = window.start + i;
618
+ const statusIcon = {
619
+ running: '●',
620
+ pending: '•',
621
+ completed: '✓',
622
+ failed: '✗',
623
+ cancelled: '–',
624
+ }[e.status] ?? '•';
625
+ const typeTag = e.type === 'agent' ? '[agent]' : '[exec]';
626
+ const dur = formatDuration(e.elapsedMs);
627
+ const statusStr = e.streamSnippet ? `streaming ${dur}` : `${e.status} ${dur}`;
628
+ const suffix = ` ${statusStr}`;
629
+ const treePrefix = e.treePrefix ?? '';
630
+ const maxDescW = maxLabelW - typeTag.length - treePrefix.length - suffix.length - 4; // icon + spaces
631
+ const desc = e.label.length > maxDescW ? e.label.slice(0, Math.max(0, maxDescW - 3)) + '...' : e.label;
632
+ const label = `${statusIcon} ${typeTag} ${treePrefix}${desc}${suffix}`;
633
+ return {
634
+ label,
635
+ selected: absoluteIndex === modal.selectedIndex,
636
+ };
637
+ });
638
+ const sections: import('./modal-factory.ts').ModalSection[] = [
639
+ { type: 'list', items },
640
+ ];
641
+ if (modal.entries.length > maxVisibleRows) {
642
+ sections.push({ type: 'separator' });
643
+ }
644
+
645
+ return ModalFactory.createModal({
646
+ title: 'Background Processes',
647
+ width: boxW,
648
+ margin: boxMargin,
649
+ targetContentRows,
650
+ sections,
651
+ helpers: modal.entries.length > maxVisibleRows
652
+ ? [{ content: `[${window.start + 1}-${window.end} of ${modal.entries.length}]` }]
653
+ : undefined,
654
+ hints: ['[Up/Down] Navigate', '[Enter] Peek output', '[k] Kill', '[Esc] Close'],
655
+ }, width);
656
+ }