@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,570 @@
1
+ // ---------------------------------------------------------------------------
2
+ // PanelManager — central manager for panel lifecycle, navigation, and split
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type { Panel, PanelRegistration, PanelCategory } from './types.ts';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Pane
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface Pane {
12
+ panels: Panel[];
13
+ activeIndex: number;
14
+ }
15
+
16
+ export interface WorkspaceTab {
17
+ readonly id: string;
18
+ readonly name: string;
19
+ readonly icon: string;
20
+ readonly pane: 'top' | 'bottom';
21
+ readonly active: boolean;
22
+ readonly focused: boolean;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // PanelManager
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export class PanelManager {
30
+ private registry: PanelRegistration[] = [];
31
+ private retainedPanels = new Map<string, Panel>();
32
+ private _visible: boolean = false;
33
+ private _splitRatio: number = 0.6;
34
+
35
+ // Two panes for the top/bottom split within the panel area
36
+ private topPane: Pane = { panels: [], activeIndex: 0 };
37
+ private bottomPane: Pane = { panels: [], activeIndex: 0 };
38
+ private _focusedPane: 'top' | 'bottom' = 'top';
39
+ private _verticalSplitRatio: number = 0.5; // top gets 50% of panel height
40
+ private _bottomPaneVisible: boolean = false;
41
+
42
+ // Cache for getWorkspaceTabs() — invalidated on every panel lifecycle event
43
+ private _cachedWorkspaceTabs: readonly WorkspaceTab[] | null = null;
44
+
45
+ // -------------------------------------------------------------------------
46
+ // Registration
47
+ // -------------------------------------------------------------------------
48
+
49
+ registerType(registration: PanelRegistration): void {
50
+ const existing = this.registry.findIndex(r => r.id === registration.id);
51
+ if (existing >= 0) {
52
+ this.registry[existing] = registration;
53
+ } else {
54
+ this.registry.push(registration);
55
+ }
56
+ }
57
+
58
+ getRegisteredTypes(): PanelRegistration[] {
59
+ return [...this.registry];
60
+ }
61
+
62
+ getTypesByCategory(): Map<PanelCategory, PanelRegistration[]> {
63
+ const map = new Map<PanelCategory, PanelRegistration[]>();
64
+ for (const reg of this.registry) {
65
+ const list = map.get(reg.category) ?? [];
66
+ list.push(reg);
67
+ map.set(reg.category, list);
68
+ }
69
+ return map;
70
+ }
71
+
72
+ prewarmRegistered(): void {
73
+ for (const registration of this.registry) {
74
+ if (!registration.preload) continue;
75
+ if (this.getPanel(registration.id) || this.retainedPanels.has(registration.id)) continue;
76
+ const panel = registration.factory();
77
+ this.retainedPanels.set(registration.id, panel);
78
+ }
79
+ }
80
+
81
+ // -------------------------------------------------------------------------
82
+ // Panel lifecycle — operates on a specific pane (defaults to focused)
83
+ // -------------------------------------------------------------------------
84
+
85
+ /** Invalidate the workspace tab cache. Call on every panel lifecycle mutation. */
86
+ private _invalidateWorkspaceTabs(): void {
87
+ this._cachedWorkspaceTabs = null;
88
+ }
89
+
90
+ open(panelId: string, pane?: 'top' | 'bottom'): Panel {
91
+ const existingPane = this._findPaneOf(panelId);
92
+ if (existingPane) {
93
+ this._activateByIdInPane(panelId, existingPane);
94
+ this._visible = true;
95
+ this._focusedPane = existingPane;
96
+ if (existingPane === 'bottom') this._bottomPaneVisible = true;
97
+ return this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]!;
98
+ }
99
+
100
+ const targetPane = pane ?? this._focusedPane;
101
+ const p = this._getPane(targetPane);
102
+
103
+ const oldPanel = p.panels[p.activeIndex];
104
+ if (oldPanel) oldPanel.onDeactivate();
105
+
106
+ const panel = this._obtainPanel(panelId);
107
+ p.panels.push(panel);
108
+ p.activeIndex = p.panels.length - 1;
109
+ this._visible = true;
110
+ // If opening into bottom pane, also make it visible
111
+ if (targetPane === 'bottom') {
112
+ this._bottomPaneVisible = true;
113
+ this._focusedPane = 'bottom';
114
+ } else {
115
+ this._focusedPane = 'top';
116
+ }
117
+ panel.onActivate();
118
+ this._invalidateWorkspaceTabs();
119
+ return panel;
120
+ }
121
+
122
+ close(panelId: string): void {
123
+ // Search both panes
124
+ for (const which of ['top', 'bottom'] as const) {
125
+ const p = this._getPane(which);
126
+ const index = p.panels.findIndex(panel => panel.id === panelId);
127
+ if (index < 0) continue;
128
+
129
+ const panel = p.panels[index];
130
+ const wasActive = index === p.activeIndex;
131
+ if (wasActive) panel.onDeactivate();
132
+ if (this._shouldRetain(panelId)) {
133
+ this.retainedPanels.set(panelId, panel);
134
+ } else {
135
+ panel.onDestroy();
136
+ }
137
+ p.panels.splice(index, 1);
138
+
139
+ if (p.panels.length === 0) {
140
+ p.activeIndex = 0;
141
+ if (which === 'bottom') {
142
+ this._bottomPaneVisible = false;
143
+ // Move focus to top if we were focused on empty bottom
144
+ if (this._focusedPane === 'bottom') this._focusedPane = 'top';
145
+ }
146
+ } else {
147
+ p.activeIndex = Math.min(p.activeIndex, p.panels.length - 1);
148
+ if (wasActive) {
149
+ const newActive = p.panels[p.activeIndex];
150
+ if (newActive) newActive.onActivate();
151
+ }
152
+ }
153
+
154
+ // Hide sidebar if no panels remain in either pane
155
+ if (this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
156
+ this._visible = false;
157
+ }
158
+ this._invalidateWorkspaceTabs();
159
+ return;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Move a panel to a specific pane. If panelId is omitted, moves the active
165
+ * panel from the currently focused pane.
166
+ */
167
+ moveToPane(dest: 'top' | 'bottom', panelId?: string): void {
168
+ const srcPaneName = panelId
169
+ ? this._findPaneOf(panelId) ?? this._focusedPane
170
+ : this._focusedPane;
171
+ if (srcPaneName === dest) return; // already there
172
+ const dstPaneName = dest;
173
+ this._moveBetweenPanes(srcPaneName, dstPaneName, panelId);
174
+ }
175
+
176
+ /**
177
+ * Move a panel to the other pane. If panelId is omitted, moves the active
178
+ * panel from the currently focused pane.
179
+ */
180
+ moveToOtherPane(panelId?: string): void {
181
+ const srcPaneName = panelId
182
+ ? this._findPaneOf(panelId) ?? this._focusedPane
183
+ : this._focusedPane;
184
+ const dstPaneName: 'top' | 'bottom' = srcPaneName === 'top' ? 'bottom' : 'top';
185
+ this._moveBetweenPanes(srcPaneName, dstPaneName, panelId);
186
+ }
187
+
188
+ // -------------------------------------------------------------------------
189
+ // Navigation — operates on focused pane
190
+ // -------------------------------------------------------------------------
191
+
192
+ nextPanel(): void {
193
+ const p = this._getFocusedPane();
194
+ if (p.panels.length === 0) return;
195
+ const oldPanel = p.panels[p.activeIndex];
196
+ if (oldPanel) oldPanel.onDeactivate();
197
+ p.activeIndex = (p.activeIndex + 1) % p.panels.length;
198
+ const newPanel = p.panels[p.activeIndex];
199
+ if (newPanel) newPanel.onActivate();
200
+ this._invalidateWorkspaceTabs();
201
+ }
202
+
203
+ nextWorkspaceTab(): void {
204
+ this._cycleWorkspaceTab(1);
205
+ }
206
+
207
+ prevWorkspaceTab(): void {
208
+ this._cycleWorkspaceTab(-1);
209
+ }
210
+
211
+ prevPanel(): void {
212
+ const p = this._getFocusedPane();
213
+ if (p.panels.length === 0) return;
214
+ const oldPanel = p.panels[p.activeIndex];
215
+ if (oldPanel) oldPanel.onDeactivate();
216
+ p.activeIndex = (p.activeIndex - 1 + p.panels.length) % p.panels.length;
217
+ const newPanel = p.panels[p.activeIndex];
218
+ if (newPanel) newPanel.onActivate();
219
+ this._invalidateWorkspaceTabs();
220
+ }
221
+
222
+ activateByIndex(index: number): void {
223
+ const p = this._getFocusedPane();
224
+ if (index < 0 || index >= p.panels.length) return;
225
+ if (index === p.activeIndex) return;
226
+ const oldPanel = p.panels[p.activeIndex];
227
+ if (oldPanel) oldPanel.onDeactivate();
228
+ p.activeIndex = index;
229
+ const newPanel = p.panels[p.activeIndex];
230
+ if (newPanel) newPanel.onActivate();
231
+ this._invalidateWorkspaceTabs();
232
+ }
233
+
234
+ activateById(panelId: string): void {
235
+ const which = this._findPaneOf(panelId);
236
+ if (!which) return;
237
+ this._activateByIdInPane(panelId, which);
238
+ }
239
+
240
+ // -------------------------------------------------------------------------
241
+ // Pane focus control
242
+ // -------------------------------------------------------------------------
243
+
244
+ focusPane(pane: 'top' | 'bottom'): void {
245
+ if (pane === 'bottom' && !this._bottomPaneVisible) return;
246
+ this._focusedPane = pane;
247
+ this._invalidateWorkspaceTabs();
248
+ }
249
+
250
+ getFocusedPane(): 'top' | 'bottom' {
251
+ return this._focusedPane;
252
+ }
253
+
254
+ /** Get the currently active (focused) panel, or null if none. */
255
+ getActivePanel(): Panel | null {
256
+ const p = this._getFocusedPane();
257
+ return p.panels[p.activeIndex] ?? null;
258
+ }
259
+
260
+ togglePaneFocus(): void {
261
+ if (!this._bottomPaneVisible || this.bottomPane.panels.length === 0) return;
262
+ this._focusedPane = this._focusedPane === 'top' ? 'bottom' : 'top';
263
+ this._invalidateWorkspaceTabs();
264
+ }
265
+
266
+ // -------------------------------------------------------------------------
267
+ // Pane visibility
268
+ // -------------------------------------------------------------------------
269
+
270
+ toggleBottomPane(): void {
271
+ this._invalidateWorkspaceTabs();
272
+ if (this._bottomPaneVisible) {
273
+ this._bottomPaneVisible = false;
274
+ if (this._focusedPane === 'bottom') this._focusedPane = 'top';
275
+ } else {
276
+ this._bottomPaneVisible = true;
277
+ // If bottom pane is empty, populate it
278
+ if (this.bottomPane.panels.length === 0) {
279
+ if (this.topPane.panels.length > 1) {
280
+ // Move last panel from top to bottom
281
+ const panel = this.topPane.panels.pop()!;
282
+ if (this.topPane.activeIndex >= this.topPane.panels.length) {
283
+ this.topPane.activeIndex = Math.max(0, this.topPane.panels.length - 1);
284
+ }
285
+ this.bottomPane.panels.push(panel);
286
+ this.bottomPane.activeIndex = 0;
287
+ } else {
288
+ // Open a default panel in bottom pane
289
+ const firstType = this.registry[0];
290
+ if (firstType) {
291
+ this.open(firstType.id, 'bottom');
292
+ }
293
+ }
294
+ }
295
+ this._focusedPane = 'bottom';
296
+ }
297
+ }
298
+
299
+ isBottomPaneVisible(): boolean {
300
+ return this._bottomPaneVisible && this.bottomPane.panels.length > 0;
301
+ }
302
+
303
+ // -------------------------------------------------------------------------
304
+ // Pane state accessors
305
+ // -------------------------------------------------------------------------
306
+
307
+ getTopPane(): Readonly<Pane> {
308
+ return this.topPane;
309
+ }
310
+
311
+ getBottomPane(): Readonly<Pane> {
312
+ return this.bottomPane;
313
+ }
314
+
315
+ // -------------------------------------------------------------------------
316
+ // Backward-compatible accessors (operate on focused pane)
317
+ // -------------------------------------------------------------------------
318
+
319
+ getOpen(): Panel[] {
320
+ const p = this._getFocusedPane();
321
+ return [...p.panels];
322
+ }
323
+
324
+ /**
325
+ * Returns all panels across both panes (top then bottom).
326
+ * Use this when you need to know if any panels exist at all.
327
+ */
328
+ getAllOpen(): Panel[] {
329
+ return [...this.topPane.panels, ...this.bottomPane.panels];
330
+ }
331
+
332
+ getActive(): Panel | null {
333
+ const p = this._getFocusedPane();
334
+ if (p.panels.length === 0) return null;
335
+ return p.panels[p.activeIndex] ?? null;
336
+ }
337
+
338
+ getPanel(panelId: string): Panel | null {
339
+ return this.topPane.panels.find((panel) => panel.id === panelId)
340
+ ?? this.bottomPane.panels.find((panel) => panel.id === panelId)
341
+ ?? null;
342
+ }
343
+
344
+ getPaneOf(panelId: string): 'top' | 'bottom' | null {
345
+ return this._findPaneOf(panelId);
346
+ }
347
+
348
+ getWorkspaceTabs(): readonly WorkspaceTab[] {
349
+ if (this._cachedWorkspaceTabs !== null) return this._cachedWorkspaceTabs;
350
+ // `active` = the currently selected tab in its own pane (independent of focus).
351
+ // `focused` = true only for the one tab in the globally focused pane that is active.
352
+ const focusedPanelId = this.getActivePanel()?.id;
353
+ const topActivePanelId = this.topPane.panels[this.topPane.activeIndex]?.id;
354
+ const bottomActivePanelId = this.bottomPane.panels[this.bottomPane.activeIndex]?.id;
355
+ const topTabs = this.topPane.panels.map((panel) => ({
356
+ id: panel.id,
357
+ name: panel.name,
358
+ icon: panel.icon,
359
+ pane: 'top' as const,
360
+ active: panel.id === topActivePanelId,
361
+ focused: panel.id === focusedPanelId,
362
+ }));
363
+ const bottomTabs = this.bottomPane.panels.map((panel) => ({
364
+ id: panel.id,
365
+ name: panel.name,
366
+ icon: panel.icon,
367
+ pane: 'bottom' as const,
368
+ active: panel.id === bottomActivePanelId,
369
+ focused: panel.id === focusedPanelId,
370
+ }));
371
+ const tabs = [...topTabs, ...bottomTabs] as WorkspaceTab[];
372
+ this._cachedWorkspaceTabs = tabs;
373
+ return tabs;
374
+ }
375
+
376
+ activateWorkspaceIndex(index: number): void {
377
+ const tabs = this.getWorkspaceTabs();
378
+ if (index < 0 || index >= tabs.length) return;
379
+ const tab = tabs[index]!;
380
+ this._focusedPane = tab.pane;
381
+ if (tab.pane === 'bottom') this._bottomPaneVisible = true;
382
+ this._activateByIdInPane(tab.id, tab.pane);
383
+ this._invalidateWorkspaceTabs();
384
+ }
385
+
386
+ // -------------------------------------------------------------------------
387
+ // Visibility
388
+ // -------------------------------------------------------------------------
389
+
390
+ toggle(): void {
391
+ this._visible = !this._visible;
392
+ // Auto-open a default panel if toggling visible with nothing open
393
+ if (this._visible && this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
394
+ const defaultPanel = this._getRegistration('panel-list') ?? this.registry[0];
395
+ if (defaultPanel) this.open(defaultPanel.id);
396
+ }
397
+ }
398
+
399
+ show(): void {
400
+ this._visible = true;
401
+ }
402
+
403
+ hide(): void {
404
+ this._visible = false;
405
+ }
406
+
407
+ isVisible(): boolean {
408
+ return this._visible;
409
+ }
410
+
411
+ // -------------------------------------------------------------------------
412
+ // Horizontal split control (left/right)
413
+ // -------------------------------------------------------------------------
414
+
415
+ getSplitRatio(): number {
416
+ return this._splitRatio;
417
+ }
418
+
419
+ setSplitRatio(ratio: number): void {
420
+ this._splitRatio = Math.max(0.3, Math.min(0.85, ratio));
421
+ }
422
+
423
+ widenLeft(): void {
424
+ this.setSplitRatio(this._splitRatio + 0.05);
425
+ }
426
+
427
+ widenRight(): void {
428
+ this.setSplitRatio(this._splitRatio - 0.05);
429
+ }
430
+
431
+ getLeftWidth(totalWidth: number): number {
432
+ return Math.floor(totalWidth * this._splitRatio);
433
+ }
434
+
435
+ getRightWidth(totalWidth: number): number {
436
+ return totalWidth - this.getLeftWidth(totalWidth);
437
+ }
438
+
439
+ // -------------------------------------------------------------------------
440
+ // Vertical split control (top/bottom within panel area)
441
+ // -------------------------------------------------------------------------
442
+
443
+ getVerticalSplitRatio(): number {
444
+ return this._verticalSplitRatio;
445
+ }
446
+
447
+ setVerticalSplitRatio(ratio: number): void {
448
+ this._verticalSplitRatio = Math.max(0.2, Math.min(0.8, ratio));
449
+ }
450
+
451
+ // -------------------------------------------------------------------------
452
+ // Cleanup
453
+ // -------------------------------------------------------------------------
454
+
455
+ destroyAll(): void {
456
+ for (const panel of [...this.topPane.panels, ...this.bottomPane.panels, ...this.retainedPanels.values()]) {
457
+ panel.onDestroy();
458
+ }
459
+ this.topPane = { panels: [], activeIndex: 0 };
460
+ this.bottomPane = { panels: [], activeIndex: 0 };
461
+ this.retainedPanels.clear();
462
+ this.registry = [];
463
+ this._focusedPane = 'top';
464
+ this._bottomPaneVisible = false;
465
+ this._visible = false;
466
+ this._invalidateWorkspaceTabs();
467
+ }
468
+
469
+ // -------------------------------------------------------------------------
470
+ // Private helpers
471
+ // -------------------------------------------------------------------------
472
+
473
+ private _getPane(which: 'top' | 'bottom'): Pane {
474
+ return which === 'top' ? this.topPane : this.bottomPane;
475
+ }
476
+
477
+ private _getFocusedPane(): Pane {
478
+ return this._getPane(this._focusedPane);
479
+ }
480
+
481
+ private _findPaneOf(panelId: string): 'top' | 'bottom' | null {
482
+ if (this.topPane.panels.some(p => p.id === panelId)) return 'top';
483
+ if (this.bottomPane.panels.some(p => p.id === panelId)) return 'bottom';
484
+ return null;
485
+ }
486
+
487
+ private _moveBetweenPanes(srcPaneName: 'top' | 'bottom', dstPaneName: 'top' | 'bottom', panelId?: string): void {
488
+ const src = this._getPane(srcPaneName);
489
+ const dst = this._getPane(dstPaneName);
490
+
491
+ const id = panelId ?? src.panels[src.activeIndex]?.id;
492
+ if (!id) return;
493
+
494
+ const index = src.panels.findIndex(p => p.id === id);
495
+ if (index < 0) return;
496
+
497
+ const panel = src.panels[index];
498
+ const wasActive = index === src.activeIndex;
499
+ if (wasActive) panel.onDeactivate();
500
+ src.panels.splice(index, 1);
501
+ src.activeIndex = Math.min(src.activeIndex, Math.max(0, src.panels.length - 1));
502
+
503
+ if (wasActive && src.panels.length > 0) {
504
+ src.panels[src.activeIndex]?.onActivate();
505
+ }
506
+
507
+ // Deactivate current active in dest
508
+ const oldDstActive = dst.panels[dst.activeIndex];
509
+ if (oldDstActive) oldDstActive.onDeactivate();
510
+
511
+ dst.panels.push(panel);
512
+ dst.activeIndex = dst.panels.length - 1;
513
+ panel.onActivate();
514
+
515
+ if (dstPaneName === 'bottom') {
516
+ this._bottomPaneVisible = true;
517
+ }
518
+ this._focusedPane = dstPaneName;
519
+ this._invalidateWorkspaceTabs();
520
+ }
521
+
522
+ private _cycleWorkspaceTab(direction: 1 | -1): void {
523
+ const tabs = this.getWorkspaceTabs();
524
+ if (tabs.length === 0) return;
525
+ const currentIndex = tabs.findIndex((tab) => tab.focused);
526
+ const nextIndex = currentIndex < 0
527
+ ? 0
528
+ : (currentIndex + direction + tabs.length) % tabs.length;
529
+ this.activateWorkspaceIndex(nextIndex);
530
+ }
531
+
532
+ private _obtainPanel(panelId: string): Panel {
533
+ const retained = this.retainedPanels.get(panelId);
534
+ if (retained) {
535
+ this.retainedPanels.delete(panelId);
536
+ return retained;
537
+ }
538
+ const registration = this._getRegistration(panelId);
539
+ if (!registration) {
540
+ throw new Error(`No panel type registered with id: ${panelId}`);
541
+ }
542
+ return registration.factory();
543
+ }
544
+
545
+ private _getRegistration(panelId: string): PanelRegistration | undefined {
546
+ return this.registry.find((registration) => registration.id === panelId);
547
+ }
548
+
549
+ private _shouldRetain(panelId: string): boolean {
550
+ return this._getRegistration(panelId)?.preload === true;
551
+ }
552
+
553
+ private _activateByIdInPane(panelId: string, which: 'top' | 'bottom'): void {
554
+ const p = this._getPane(which);
555
+ const index = p.panels.findIndex(panel => panel.id === panelId);
556
+ if (index < 0) return;
557
+ if (index === p.activeIndex) {
558
+ p.panels[index]?.onActivate();
559
+ return;
560
+ }
561
+ if (index !== p.activeIndex) {
562
+ const oldPanel = p.panels[p.activeIndex];
563
+ if (oldPanel) oldPanel.onDeactivate();
564
+ p.activeIndex = index;
565
+ const newPanel = p.panels[p.activeIndex];
566
+ if (newPanel) newPanel.onActivate();
567
+ this._invalidateWorkspaceTabs();
568
+ }
569
+ }
570
+ }
@@ -0,0 +1,106 @@
1
+ import type { PanelRegistration, PanelCategory } from './types.ts';
2
+
3
+ /** Display order for panel categories. */
4
+ const CATEGORY_ORDER: PanelCategory[] = ['development', 'agent', 'monitoring', 'session', 'ai'];
5
+
6
+ /** Human-readable labels for each category. */
7
+ const CATEGORY_LABELS: Record<PanelCategory, string> = {
8
+ development: 'Development',
9
+ agent: 'Agent',
10
+ monitoring: 'Monitoring',
11
+ session: 'Session',
12
+ ai: 'AI',
13
+ };
14
+
15
+ /**
16
+ * Modal state for browsing and selecting panel registrations.
17
+ * The filtered list is a flat list of PanelRegistration items matching the
18
+ * current search query. Category headers are rendered by the overlay, not
19
+ * stored here — keeping this class pure data/logic.
20
+ */
21
+ export class PanelPicker {
22
+ public active: boolean = false;
23
+ public selectedIndex: number = 0;
24
+ public searchQuery: string = '';
25
+ private items: PanelRegistration[] = [];
26
+ private filtered: PanelRegistration[] = [];
27
+
28
+ /** Open the picker with the given set of registered panels. */
29
+ open(registrations: PanelRegistration[]): void {
30
+ this.items = registrations;
31
+ this.searchQuery = '';
32
+ this.filtered = this._applyFilter('');
33
+ this.selectedIndex = 0;
34
+ this.active = true;
35
+ }
36
+
37
+ /** Close and reset the picker. */
38
+ close(): void {
39
+ this.active = false;
40
+ this.searchQuery = '';
41
+ this.selectedIndex = 0;
42
+ }
43
+
44
+ /**
45
+ * Filter the list by name or description (case-insensitive).
46
+ * Resets selectedIndex to 0.
47
+ */
48
+ search(query: string): void {
49
+ this.searchQuery = query;
50
+ this.filtered = this._applyFilter(query);
51
+ this.selectedIndex = 0;
52
+ }
53
+
54
+ /** Move selection up by one, wrapping at top. */
55
+ moveUp(): void {
56
+ if (this.filtered.length === 0) return;
57
+ this.selectedIndex = this.selectedIndex <= 0
58
+ ? this.filtered.length - 1
59
+ : this.selectedIndex - 1;
60
+ }
61
+
62
+ /** Move selection down by one, wrapping at bottom. */
63
+ moveDown(): void {
64
+ if (this.filtered.length === 0) return;
65
+ this.selectedIndex = this.selectedIndex >= this.filtered.length - 1
66
+ ? 0
67
+ : this.selectedIndex + 1;
68
+ }
69
+
70
+ /**
71
+ * Returns the filtered list, sorted by category order then name.
72
+ * The overlay is responsible for inserting category headers between groups.
73
+ */
74
+ getVisible(): PanelRegistration[] {
75
+ return this.filtered;
76
+ }
77
+
78
+ /** Returns the currently selected registration, or null if the list is empty. */
79
+ getSelected(): PanelRegistration | null {
80
+ if (this.filtered.length === 0) return null;
81
+ return this.filtered[this.selectedIndex] ?? null;
82
+ }
83
+
84
+ // ── Private ──────────────────────────────────────────────────────────────
85
+
86
+ private _applyFilter(query: string): PanelRegistration[] {
87
+ const q = query.trim().toLowerCase();
88
+ const source = q
89
+ ? this.items.filter(
90
+ r =>
91
+ r.name.toLowerCase().includes(q) ||
92
+ r.description.toLowerCase().includes(q),
93
+ )
94
+ : [...this.items];
95
+
96
+ // Sort by category order, then alphabetically within each category
97
+ return source.sort((a, b) => {
98
+ const ai = CATEGORY_ORDER.indexOf(a.category);
99
+ const bi = CATEGORY_ORDER.indexOf(b.category);
100
+ if (ai !== bi) return ai - bi;
101
+ return a.name.localeCompare(b.name);
102
+ });
103
+ }
104
+ }
105
+
106
+ export { CATEGORY_LABELS, CATEGORY_ORDER };