@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,431 @@
1
+ import { promises as fsPromises } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { Line } from '../types/grid.ts';
4
+ import { createEmptyLine } from '../types/grid.ts';
5
+ import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
6
+ import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
7
+ import { SearchableListPanel } from './scrollable-list-panel.ts';
8
+ import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
9
+ import type { ShellPathService } from '@/runtime/index.ts';
10
+ import {
11
+ buildPanelLine,
12
+ buildPanelWorkspace,
13
+ DEFAULT_PANEL_PALETTE,
14
+ } from './polish.ts';
15
+ import {
16
+ getPanelSearchFocusTransition,
17
+ isPanelSearchCancel,
18
+ } from './search-focus.ts';
19
+ import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
20
+
21
+ const C = {
22
+ ...DEFAULT_PANEL_PALETTE,
23
+ header: '#94a3b8',
24
+ headerBg: '#1e293b',
25
+ searchFg: '#f97316',
26
+ searchBg: '#1e293b',
27
+ label: '#64748b',
28
+ value: '#e2e8f0',
29
+ dim: '#64748b',
30
+ empty: '#334155',
31
+ selectedFg: '#e2e8f0',
32
+ selectedBg: '#1e3a5f',
33
+ project: '#38bdf8',
34
+ global: '#a78bfa',
35
+ hint: '#475569',
36
+ path: '#94a3b8',
37
+ selectBg: '#1e3a5f',
38
+ } as const;
39
+
40
+ export type SkillOrigin = 'project-local' | 'global' | 'custom';
41
+
42
+ export interface SkillRecord {
43
+ name: string;
44
+ description: string;
45
+ path: string;
46
+ origin: SkillOrigin;
47
+ dependencies: string[];
48
+ includes: string[];
49
+ frontmatter: Record<string, string>;
50
+ }
51
+
52
+ export interface SkillsPanelOptions {
53
+ shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>;
54
+ componentHealthMonitor?: ComponentHealthMonitor;
55
+ }
56
+
57
+ function parseFrontmatter(content: string): Record<string, string> {
58
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
59
+ if (!match) return {};
60
+ const result: Record<string, string> = {};
61
+ for (const line of match[1].split('\n')) {
62
+ const [key, ...rest] = line.split(':');
63
+ if (key && rest.length > 0) {
64
+ result[key.trim()] = rest.join(':').trim();
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+
70
+ function getSkillDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: SkillOrigin }> {
71
+ return [
72
+ { root: join(cwd, '.goodvibes', 'skills'), origin: 'project-local' },
73
+ { root: join(cwd, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'skills'), origin: 'project-local' },
74
+ { root: join(homeDir, '.goodvibes', 'skills'), origin: 'global' },
75
+ { root: join(homeDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'skills'), origin: 'global' },
76
+ ];
77
+ }
78
+
79
+ async function readSkillFile(path: string, origin: SkillOrigin): Promise<SkillRecord | null> {
80
+ let content = '';
81
+ try {
82
+ content = await fsPromises.readFile(path, 'utf-8');
83
+ } catch {
84
+ return null;
85
+ }
86
+
87
+ const frontmatter = parseFrontmatter(content);
88
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
89
+ const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/, '') ?? 'skill';
90
+ const description = frontmatter.description ?? frontmatter.summary ?? '';
91
+ const dependencies = frontmatter.depends_on
92
+ ? frontmatter.depends_on.split(',').map((item) => item.trim()).filter(Boolean)
93
+ : [];
94
+ const includes: string[] = [];
95
+ const includeRegex = /^@([\w/-]+)/gm;
96
+ let match: RegExpExecArray | null;
97
+ while ((match = includeRegex.exec(body)) !== null) {
98
+ includes.push(match[1]);
99
+ }
100
+
101
+ return {
102
+ name,
103
+ description,
104
+ path,
105
+ origin,
106
+ dependencies,
107
+ includes,
108
+ frontmatter,
109
+ };
110
+ }
111
+
112
+ async function scanSkillDirectory(root: string, origin: SkillOrigin): Promise<SkillRecord[]> {
113
+ let entries: string[] = [];
114
+ try {
115
+ entries = await fsPromises.readdir(root);
116
+ } catch {
117
+ return [];
118
+ }
119
+
120
+ const records: SkillRecord[] = [];
121
+ for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
122
+ if (entry.endsWith('.md')) {
123
+ const record = await readSkillFile(join(root, entry), origin);
124
+ if (record) records.push(record);
125
+ continue;
126
+ }
127
+
128
+ const markerPath = join(root, entry, 'SKILL.md');
129
+ const record = await readSkillFile(markerPath, origin);
130
+ if (record) records.push(record);
131
+ }
132
+
133
+ return records;
134
+ }
135
+
136
+ export async function discoverSkills(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>): Promise<SkillRecord[]> {
137
+ const cwd = shellPaths.workingDirectory;
138
+ const homeDir = shellPaths.homeDirectory;
139
+ const seen = new Set<string>();
140
+ const records: SkillRecord[] = [];
141
+
142
+ for (const { root, origin } of getSkillDirectories(cwd, homeDir)) {
143
+ for (const record of await scanSkillDirectory(root, origin)) {
144
+ if (seen.has(record.name.toLowerCase())) continue;
145
+ seen.add(record.name.toLowerCase());
146
+ records.push(record);
147
+ }
148
+ }
149
+
150
+ return records.sort((a, b) => {
151
+ const originRank = a.origin === b.origin
152
+ ? 0
153
+ : a.origin === 'project-local'
154
+ ? -1
155
+ : 1;
156
+ return originRank || a.name.localeCompare(b.name);
157
+ });
158
+ }
159
+
160
+ function wordWrap(text: string, maxWidth: number): string[] {
161
+ if (maxWidth <= 0) return [''];
162
+ const words = text.split(/\s+/).filter(Boolean);
163
+ if (words.length === 0) return [''];
164
+ const lines: string[] = [];
165
+ let line = '';
166
+ for (const word of words) {
167
+ if (!line) {
168
+ line = word;
169
+ continue;
170
+ }
171
+ if (line.length + 1 + word.length <= maxWidth) {
172
+ line += ` ${word}`;
173
+ } else {
174
+ lines.push(line);
175
+ line = word;
176
+ }
177
+ }
178
+ if (line) lines.push(line);
179
+ return lines.length > 0 ? lines : [''];
180
+ }
181
+
182
+ function truncatePathDisplay(path: string, width: number): string {
183
+ if (width <= 0) return '';
184
+ if (getDisplayWidth(path) <= width) return path;
185
+
186
+ const ellipsis = '…';
187
+ const ellipsisWidth = getDisplayWidth(ellipsis);
188
+ if (ellipsisWidth >= width) return truncateDisplay(path, width);
189
+
190
+ const available = width - ellipsisWidth;
191
+ const prefixBudget = Math.max(1, Math.floor(available * 0.35));
192
+ const suffixBudget = Math.max(1, available - prefixBudget);
193
+ const prefix = truncateDisplay(path, prefixBudget, '');
194
+
195
+ let suffix = '';
196
+ let suffixWidth = 0;
197
+ for (let index = path.length - 1; index >= 0; index -= 1) {
198
+ const char = path[index]!;
199
+ const charWidth = getDisplayWidth(char);
200
+ if (suffixWidth + charWidth > suffixBudget) break;
201
+ suffix = char + suffix;
202
+ suffixWidth += charWidth;
203
+ }
204
+
205
+ return `${prefix}${ellipsis}${suffix}`;
206
+ }
207
+
208
+ function originLabel(origin: SkillOrigin): string {
209
+ switch (origin) {
210
+ case 'project-local':
211
+ return 'project';
212
+ case 'global':
213
+ return 'global';
214
+ case 'custom':
215
+ return 'custom';
216
+ }
217
+ }
218
+
219
+ function originColor(origin: SkillOrigin): string {
220
+ switch (origin) {
221
+ case 'project-local':
222
+ return C.project;
223
+ case 'global':
224
+ return C.global;
225
+ case 'custom':
226
+ return C.dim;
227
+ }
228
+ }
229
+
230
+ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
231
+ private readonly shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>;
232
+ /** Whether the filter input row is focused for typing (vs. list navigation). */
233
+ private filterFocused = false;
234
+ private cached: SkillRecord[] | null = null;
235
+ private cacheDirty = true;
236
+ // I1: confirm state for destructive delete
237
+ private confirm: ConfirmState | null = null;
238
+ private readyPromise: Promise<void> | null = null;
239
+
240
+ public constructor(options: SkillsPanelOptions) {
241
+ super('skills', 'Skills', 'K', 'monitoring', options.componentHealthMonitor);
242
+ this.showSelectionGutter = true; // I5: non-color selection affordance
243
+ this.shellPaths = options.shellPaths;
244
+ }
245
+
246
+ // -------------------------------------------------------------------------
247
+ // SearchableListPanel implementation
248
+ // -------------------------------------------------------------------------
249
+
250
+ protected getAllItems(): readonly SkillRecord[] {
251
+ return this.cached ?? [];
252
+ }
253
+
254
+ private _loadSkillsAsync(): Promise<void> {
255
+ const p = (async () => {
256
+ try {
257
+ await this.withLoading('Scanning skills\u2026', async () => {
258
+ this.cached = await discoverSkills(this.shellPaths);
259
+ this.cacheDirty = false;
260
+ this.invalidateFilter();
261
+ });
262
+ } catch (err) {
263
+ this.setError(err instanceof Error ? err.message : String(err));
264
+ }
265
+ this.markDirty();
266
+ })();
267
+ this.readyPromise = p;
268
+ return p;
269
+ }
270
+
271
+ /** Resolves when the current load cycle has settled. */
272
+ public awaitReady(): Promise<void> {
273
+ return this.readyPromise ?? Promise.resolve();
274
+ }
275
+
276
+ protected matchesSearch(skill: SkillRecord, query: string): boolean {
277
+ const q = query.trim().toLowerCase();
278
+ if (!q) return true;
279
+ const haystack = [
280
+ skill.name,
281
+ skill.description,
282
+ skill.path,
283
+ skill.origin,
284
+ skill.dependencies.join(' '),
285
+ skill.includes.join(' '),
286
+ ].join(' ').toLowerCase();
287
+ return haystack.includes(q);
288
+ }
289
+
290
+ protected renderItem(skill: SkillRecord, index: number, selected: boolean, width: number): Line {
291
+ const bg = selected ? C.selectBg : undefined;
292
+ const dot = skill.origin === 'project-local' ? '\u25c6' : '\u2022';
293
+ const desc = skill.description || 'No description provided.';
294
+ const descWidth = Math.max(1, width - 4 - skill.name.length - 6);
295
+ const descLines = wordWrap(desc, descWidth);
296
+ return buildPanelLine(width, [
297
+ [selected ? '\u25b8' : ' ', C.selectedFg, bg],
298
+ [' ', C.dim, bg],
299
+ [dot, originColor(skill.origin), bg],
300
+ [' ', C.dim, bg],
301
+ [skill.name, selected ? C.selectedFg : C.value, bg],
302
+ [' ', C.dim, bg],
303
+ [descLines[0] ?? '', selected ? C.selectedFg : C.dim, bg],
304
+ ]);
305
+ }
306
+
307
+ protected override getPalette() { return C; }
308
+ protected override getEmptyStateMessage() { return ' No skills discovered.'; }
309
+ protected override getEmptyStateActions() {
310
+ return [
311
+ { command: '.goodvibes/skills', summary: 'place skill .md files here (project-local) or ~/.goodvibes/skills (global)' },
312
+ { command: '/registry search skills', summary: 'inspect the same skill directories from the shell' },
313
+ ];
314
+ }
315
+
316
+ public override onActivate(): void {
317
+ super.onActivate();
318
+ this.searchQuery = '';
319
+ this.invalidateFilter();
320
+ this.filterFocused = false;
321
+ this.cacheDirty = true;
322
+ void this._loadSkillsAsync();
323
+ }
324
+
325
+ public override onDestroy(): void {}
326
+
327
+ public handleInput(key: string): boolean {
328
+ // I1: y/n confirmation dialog for delete
329
+ const confirmResult = handleConfirmInput(this.confirm, key);
330
+ if (confirmResult === 'confirmed') {
331
+ const toDelete = this.confirm!.subject;
332
+ this.confirm = null;
333
+ // Skills are read from the filesystem — deletion requires a shell command.
334
+ // Surface an error directing the user to remove the file manually.
335
+ this.setError(`Delete via shell: rm "${toDelete}"`);
336
+ this.markDirty();
337
+ return true;
338
+ }
339
+ if (confirmResult === 'cancelled') {
340
+ this.confirm = null;
341
+ this.markDirty();
342
+ return true;
343
+ }
344
+ if (confirmResult === 'absorbed') return true;
345
+
346
+ const items = this.getItems();
347
+
348
+ // Filter-focus mode: typing goes into the search query
349
+ if (this.filterFocused) {
350
+ const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
351
+ if (transition === 'focus-list') {
352
+ this.filterFocused = false;
353
+ this.markDirty();
354
+ return true;
355
+ }
356
+ // Escape: also blur filter focus (clear + return to list navigation)
357
+ if (isPanelSearchCancel(key)) {
358
+ this.filterFocused = false;
359
+ // Delegate to super to clear the query. If the query is empty, super
360
+ // returns false and escape propagates to the panel dismissal handler —
361
+ // this is the intentional double-escape UX (blur filter, then close).
362
+ return super.handleInput(key);
363
+ }
364
+ // Delegate backspace/printable to SearchableListPanel.handleInput
365
+ return super.handleInput(key);
366
+ }
367
+
368
+ const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
369
+ if (transition === 'focus-search') {
370
+ this.filterFocused = true;
371
+ this.markDirty();
372
+ return true;
373
+ }
374
+
375
+ // I1: 'd' prompts delete confirmation
376
+ if (key === 'd') {
377
+ const skill = items[this.selectedIndex];
378
+ if (skill) {
379
+ this.confirm = { subject: skill.path, label: skill.name };
380
+ this.markDirty();
381
+ }
382
+ return true;
383
+ }
384
+
385
+ // Navigation + search: delegate to SearchableListPanel (up/down/g/G/page/enter + backspace/escape)
386
+ return super.handleInput(key);
387
+ }
388
+
389
+ public render(width: number, height: number): Line[] {
390
+ return this.trackedRender(() => {
391
+ this.needsRender = false;
392
+
393
+ // I1: show confirm dialog in place of normal content
394
+ if (this.confirm) {
395
+ const lines = buildPanelWorkspace(width, height, {
396
+ title: 'Skills - confirm action',
397
+ intro: '',
398
+ sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
399
+ palette: C,
400
+ });
401
+ while (lines.length < height) lines.push(createEmptyLine(width));
402
+ return lines.slice(0, height);
403
+ }
404
+
405
+ // Build filter input line (provided by SearchableListPanel base)
406
+ const filterLine = this.buildFilterInputLine(width, 'Filter', this.filterFocused);
407
+
408
+ // Build detail footer for the currently selected skill
409
+ const items = this.getItems();
410
+ const selected = items[this.selectedIndex];
411
+ const detailLines: Line[] = [];
412
+ if (selected) {
413
+ detailLines.push(
414
+ buildPanelLine(width, [[' Selected: ', C.label], [selected.name, C.value], [' [', C.dim], [originLabel(selected.origin), originColor(selected.origin)], [']', C.dim]]),
415
+ buildPanelLine(width, [[' Path: ', C.label], [truncatePathDisplay(selected.path, Math.max(1, width - 8)), C.path]]),
416
+ buildPanelLine(width, [[' Desc: ', C.label], [selected.description || 'No description provided.', C.value]]),
417
+ buildPanelLine(width, [[' Depends: ', C.label], [selected.dependencies.length > 0 ? selected.dependencies.join(', ') : 'none', C.dim]]),
418
+ buildPanelLine(width, [[' Includes: ', C.label], [selected.includes.length > 0 ? selected.includes.join(', ') : 'none', C.dim]]),
419
+ );
420
+ }
421
+ detailLines.push(buildPanelLine(width, [[' Up/Down navigate / or Up-at-top focus filter Esc blur Backspace clear', C.hint]]));
422
+
423
+ const lines = this.renderList(width, height, {
424
+ title: 'Skills - discover project-local and global skill packs',
425
+ header: [filterLine],
426
+ footer: detailLines,
427
+ });
428
+ return lines;
429
+ });
430
+ }
431
+ }
@@ -0,0 +1,263 @@
1
+ import type { Line } from '../types/grid.ts';
2
+ import { createEmptyLine } from '../types/grid.ts';
3
+ import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
+ import type { ProviderSubscription, PendingSubscriptionLogin } from '@pellux/goodvibes-sdk/platform/config';
5
+ import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
6
+ import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime/ui-service-queries.ts';
7
+ import {
8
+ buildEmptyState,
9
+ buildGuidanceLine,
10
+ buildKeyValueLine,
11
+ buildPanelListRow,
12
+ buildPanelLine,
13
+ buildSummaryBlock,
14
+ buildPanelWorkspace,
15
+ DEFAULT_PANEL_PALETTE,
16
+ } from './polish.ts';
17
+
18
+ const C = {
19
+ ...DEFAULT_PANEL_PALETTE,
20
+ header: '#e2e8f0',
21
+ headerBg: '#1e293b',
22
+ dim: '#64748b',
23
+ info: '#38bdf8',
24
+ good: '#22c55e',
25
+ warn: '#f59e0b',
26
+ bad: '#ef4444',
27
+ selectedBg: '#0f172a',
28
+ } as const;
29
+
30
+ interface SubscriptionRow {
31
+ readonly provider: string;
32
+ readonly hasOAuthConfig: boolean;
33
+ readonly subscription: ProviderSubscription | null;
34
+ readonly pending: PendingSubscriptionLogin | null;
35
+ }
36
+
37
+ function statusOf(row: SubscriptionRow): 'active' | 'pending' | 'available' | 'unconfigured' {
38
+ if (row.subscription) return 'active';
39
+ if (row.pending) return 'pending';
40
+ return row.hasOAuthConfig ? 'available' : 'unconfigured';
41
+ }
42
+
43
+ function statusColor(status: ReturnType<typeof statusOf>): string {
44
+ switch (status) {
45
+ case 'active':
46
+ return C.good;
47
+ case 'pending':
48
+ return C.warn;
49
+ case 'available':
50
+ return C.info;
51
+ case 'unconfigured':
52
+ return C.dim;
53
+ }
54
+ }
55
+
56
+ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
57
+ private readonly serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
58
+ private readonly subscriptionManager: SubscriptionAccessQuery;
59
+ private rows: SubscriptionRow[] = [];
60
+ private logoutConfirmationTarget: string | null = null;
61
+
62
+ public constructor(
63
+ serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>,
64
+ subscriptionManager: SubscriptionAccessQuery,
65
+ ) {
66
+ super('subscription', 'Subscriptions', 'B', 'monitoring');
67
+ this.showSelectionGutter = true; // I5: non-color selection affordance
68
+ this.serviceRegistry = serviceRegistry;
69
+ this.subscriptionManager = subscriptionManager;
70
+ }
71
+
72
+ public override onActivate(): void {
73
+ super.onActivate();
74
+ this.refresh();
75
+ }
76
+
77
+ protected override getPalette() { return C; }
78
+ protected override getEmptyStateMessage() { return ' No provider subscriptions are active yet.'; }
79
+ protected override getEmptyStateActions() {
80
+ return [
81
+ { command: '/subscription login openai start', summary: 'start the first-class OpenAI subscription flow' },
82
+ { command: '/login provider <name> start', summary: 'use the front-door auth surface for supported providers' },
83
+ { command: '/services auth-review', summary: 'inspect configured service auth posture and stored secrets' },
84
+ ];
85
+ }
86
+
87
+ protected getItems(): readonly SubscriptionRow[] {
88
+ return this.rows;
89
+ }
90
+
91
+ protected renderItem(row: SubscriptionRow, index: number, selected: boolean, width: number): Line {
92
+ const status = statusOf(row);
93
+ return buildPanelListRow(width, [
94
+ { text: row.provider.padEnd(16).slice(0, 16), fg: C.value },
95
+ { text: ` ${status.toUpperCase().padEnd(12)}`, fg: statusColor(status) },
96
+ { text: ` oauth=${row.hasOAuthConfig ? 'yes' : 'no'} `, fg: row.hasOAuthConfig ? C.info : C.dim },
97
+ { text: ` override=${row.subscription ? 'active' : 'off'}`, fg: row.subscription ? C.good : C.dim },
98
+ ], C, { selected, selectedBg: C.selectedBg });
99
+ }
100
+
101
+ public handleInput(key: string): boolean {
102
+ if (this.rows.length === 0) return false;
103
+ const selected = this.rows[this.selectedIndex] ?? null;
104
+ if (key === 'ArrowUp' || key === 'k') {
105
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
106
+ this.logoutConfirmationTarget = null;
107
+ this.markDirty();
108
+ return true;
109
+ }
110
+ if (key === 'ArrowDown' || key === 'j') {
111
+ this.selectedIndex = Math.min(this.rows.length - 1, this.selectedIndex + 1);
112
+ this.logoutConfirmationTarget = null;
113
+ this.markDirty();
114
+ return true;
115
+ }
116
+ if (key === 'enter' || key === 'x') {
117
+ if (!selected?.subscription) return false;
118
+ if (this.logoutConfirmationTarget === null || this.logoutConfirmationTarget !== selected.provider) {
119
+ this.logoutConfirmationTarget = selected.provider;
120
+ this.markDirty();
121
+ return true;
122
+ }
123
+ this.subscriptionManager.logout(selected.provider);
124
+ this.logoutConfirmationTarget = null;
125
+ this.refresh();
126
+ this.markDirty();
127
+ return true;
128
+ }
129
+ if ((key === 'n' || key === 'escape') && this.logoutConfirmationTarget) {
130
+ this.logoutConfirmationTarget = null;
131
+ this.markDirty();
132
+ return true;
133
+ }
134
+ if (key === 'r') {
135
+ this.refresh();
136
+ this.logoutConfirmationTarget = null;
137
+ this.markDirty();
138
+ return true;
139
+ }
140
+ return false;
141
+ }
142
+
143
+ private refresh(): void {
144
+ const manager = this.subscriptionManager;
145
+ const services = this.serviceRegistry.getAll();
146
+ const builtinProviders = listBuiltinSubscriptionProviders();
147
+ const providers = new Set<string>([
148
+ ...builtinProviders.map((provider) => provider.provider),
149
+ ...Object.values(services)
150
+ .filter((service) => service.authType === 'oauth' && service.oauth)
151
+ .map((service) => service.providerId ?? service.name),
152
+ ...manager.list().map((entry) => entry.provider),
153
+ ...manager.listPending().map((entry) => entry.provider),
154
+ ]);
155
+ this.rows = [...providers]
156
+ .sort((a, b) => a.localeCompare(b))
157
+ .map((provider) => {
158
+ const service = Object.values(services).find((entry) => (
159
+ (entry.providerId ?? entry.name) === provider && entry.authType === 'oauth' && entry.oauth
160
+ ));
161
+ const builtin = builtinProviders.find((entry) => entry.provider === provider);
162
+ return {
163
+ provider,
164
+ hasOAuthConfig: Boolean(service?.oauth) || Boolean(builtin),
165
+ subscription: manager.get(provider),
166
+ pending: manager.getPending(provider),
167
+ } satisfies SubscriptionRow;
168
+ });
169
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.rows.length - 1));
170
+ }
171
+
172
+ public render(width: number, height: number): Line[] {
173
+ this.refresh();
174
+ this.clampSelection();
175
+ const intro = 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.';
176
+
177
+ const activeCount = this.rows.filter((row) => row.subscription).length;
178
+ const pendingCount = this.rows.filter((row) => row.pending).length;
179
+ const postureLines = [
180
+ buildKeyValueLine(width, [
181
+ { label: 'configured', value: String(this.rows.filter((row) => row.hasOAuthConfig).length), valueColor: C.info },
182
+ { label: 'active', value: String(activeCount), valueColor: activeCount > 0 ? C.good : C.dim },
183
+ { label: 'pending', value: String(pendingCount), valueColor: pendingCount > 0 ? C.warn : C.dim },
184
+ { label: 'providers', value: String(this.rows.length), valueColor: C.value },
185
+ ], C),
186
+ buildKeyValueLine(width, [
187
+ { label: 'selected', value: (this.rows[this.selectedIndex]?.provider ?? 'none'), valueColor: this.rows[this.selectedIndex] ? C.value : C.dim },
188
+ { label: 'status', value: this.rows[this.selectedIndex] ? statusOf(this.rows[this.selectedIndex]!) : 'n/a', valueColor: this.rows[this.selectedIndex] ? statusColor(statusOf(this.rows[this.selectedIndex]!)) : C.dim },
189
+ ], C),
190
+ buildGuidanceLine(width, '/subscription login <provider> start', 'start or repair browser login for the selected provider route', C),
191
+ ];
192
+
193
+ // Empty state: render posture + base empty state
194
+ if (this.rows.length === 0) {
195
+ const summaryLines = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
196
+ const emptyLines = buildEmptyState(
197
+ width,
198
+ this.getEmptyStateMessage(),
199
+ 'Built-in OAuth-capable providers and configured service providers will appear here once available for browser login or session import.',
200
+ this.getEmptyStateActions(),
201
+ C,
202
+ );
203
+ const workspace = buildPanelWorkspace(width, height, {
204
+ title: 'Provider Subscriptions',
205
+ intro,
206
+ sections: [{ lines: [...summaryLines, ...emptyLines] }],
207
+ footerLines: [
208
+ buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
209
+ buildPanelLine(width, [[' Up/Down move Enter/X sign out selected provider r refresh', C.dim]]),
210
+ ],
211
+ palette: C,
212
+ });
213
+ while (workspace.length < height) workspace.push(createEmptyLine(width));
214
+ return workspace.slice(0, height);
215
+ }
216
+
217
+ const selectedRow = this.rows[this.selectedIndex];
218
+ const detailRows: Line[] = [];
219
+ if (selectedRow) {
220
+ detailRows.push(buildKeyValueLine(width, [
221
+ { label: 'provider', value: selectedRow.provider, valueColor: C.value },
222
+ { label: 'status', value: statusOf(selectedRow), valueColor: statusColor(statusOf(selectedRow)) },
223
+ { label: 'oauth config', value: selectedRow.hasOAuthConfig ? 'present' : 'missing', valueColor: selectedRow.hasOAuthConfig ? C.good : C.bad },
224
+ ], C));
225
+ if (selectedRow.subscription) {
226
+ const expires = selectedRow.subscription.expiresAt
227
+ ? new Date(selectedRow.subscription.expiresAt).toISOString()
228
+ : 'n/a';
229
+ detailRows.push(buildKeyValueLine(width, [
230
+ { label: 'token type', value: selectedRow.subscription.tokenType, valueColor: C.info },
231
+ { label: 'expires', value: expires, valueColor: C.dim },
232
+ ], C));
233
+ detailRows.push(buildPanelLine(width, [[
234
+ ` ${selectedRow.subscription.overrideAmbientApiKeys
235
+ ? 'Provider subscription overrides ambient API-key resolution for this provider.'
236
+ : 'Stored for subscription-backed flows. Ambient API-key resolution remains unchanged.'}`,
237
+ C.dim,
238
+ ]]));
239
+ if (this.logoutConfirmationTarget === selectedRow.provider) {
240
+ detailRows.push(buildPanelLine(width, [[` Press Enter or X again to sign out ${selectedRow.provider}.`, C.warn]]));
241
+ }
242
+ } else if (selectedRow.pending) {
243
+ detailRows.push(buildPanelLine(width, [[' Login is pending. Finish with /subscription login <provider> finish <code>.', C.warn]]));
244
+ } else if (selectedRow.hasOAuthConfig) {
245
+ detailRows.push(buildPanelLine(width, [[' Ready for login. Start with /subscription login <provider> start.', C.dim]]));
246
+ } else {
247
+ detailRows.push(buildPanelLine(width, [[' Add a provider-specific OAuth config or enable a built-in subscription provider to use subscription login.', C.bad]]));
248
+ }
249
+ }
250
+
251
+ const headerLines: Line[] = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
252
+
253
+ return this.renderList(width, height, {
254
+ title: 'Provider Subscriptions',
255
+ header: headerLines,
256
+ footer: [
257
+ ...detailRows,
258
+ buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
259
+ buildPanelLine(width, [[' Up/Down move Enter/X sign out selected provider r refresh', C.dim]]),
260
+ ],
261
+ });
262
+ }
263
+ }