@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,67 @@
1
+ export type ProcessSummaryAgent = {
2
+ readonly id: string;
3
+ readonly progress?: string;
4
+ };
5
+
6
+ export type RuntimeProcessSummaryAgent = {
7
+ readonly id: string;
8
+ readonly latestProgress?: string;
9
+ };
10
+
11
+ export type WrfcProcessSummaryChain = {
12
+ readonly state?: string;
13
+ readonly ownerAgentId?: string;
14
+ readonly engineerAgentId?: string;
15
+ readonly reviewerAgentId?: string;
16
+ readonly fixerAgentId?: string;
17
+ readonly allAgentIds?: readonly unknown[];
18
+ };
19
+
20
+ export type RunningAgentSummary = {
21
+ readonly count: number;
22
+ readonly progress?: string;
23
+ };
24
+
25
+ export function summarizeRunningAgents(
26
+ managerAgents: readonly ProcessSummaryAgent[],
27
+ runtimeAgents: readonly RuntimeProcessSummaryAgent[],
28
+ wrfcChains: readonly WrfcProcessSummaryChain[],
29
+ ): RunningAgentSummary {
30
+ const runningAgentIds = new Set<string>();
31
+ let progress: string | undefined;
32
+
33
+ for (const agent of managerAgents) {
34
+ runningAgentIds.add(agent.id);
35
+ if (!progress && agent.progress) progress = agent.progress;
36
+ }
37
+
38
+ for (const agent of runtimeAgents) {
39
+ runningAgentIds.add(agent.id);
40
+ if (!progress && agent.latestProgress) progress = agent.latestProgress;
41
+ }
42
+
43
+ for (const chain of wrfcChains) {
44
+ if (isTerminalWrfcState(chain.state)) continue;
45
+ const chainAgentIds = collectChainAgentIds(chain);
46
+ const hasVisibleChainWork = Array.from(chainAgentIds).some((id) => runningAgentIds.has(id));
47
+ if (!hasVisibleChainWork || !chain.ownerAgentId) continue;
48
+ runningAgentIds.add(chain.ownerAgentId);
49
+ if (!progress) progress = `WRFC chain ${chain.state ?? 'running'}`;
50
+ }
51
+
52
+ return { count: runningAgentIds.size, progress };
53
+ }
54
+
55
+ function isTerminalWrfcState(state: string | undefined): boolean {
56
+ return state === 'passed' || state === 'failed';
57
+ }
58
+
59
+ function collectChainAgentIds(chain: WrfcProcessSummaryChain): Set<string> {
60
+ return new Set([
61
+ chain.ownerAgentId,
62
+ chain.engineerAgentId,
63
+ chain.reviewerAgentId,
64
+ chain.fixerAgentId,
65
+ ...(chain.allAgentIds ?? []),
66
+ ].filter((id): id is string => typeof id === 'string' && id.length > 0));
67
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * renderProfilePickerModal — renders the /profiles picker modal as Line[]
3
+ * using ModalFactory.
4
+ *
5
+ * Shows a list of saved profiles with:
6
+ * - name, timestamp (formatted), settings preview
7
+ * Footer hints: [Up/Down] Navigate [Enter] Load [d] Arm/Delete [s] Save current [Esc] Close
8
+ */
9
+
10
+ import type { Line } from '../types/grid.ts';
11
+ import { ModalFactory } from './modal-factory.ts';
12
+ import type { ProfilePickerModal } from '../input/profile-picker-modal.ts';
13
+ import { formatTimestamp } from './modal-utils.ts';
14
+ import { fitDisplay } from '../utils/terminal-width.ts';
15
+ import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Renderer
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Render the profile picker modal as Line[] for overlay in the viewport.
23
+ *
24
+ * @param modal ProfilePickerModal state object.
25
+ * @param width Terminal width.
26
+ */
27
+ export function renderProfilePickerModal(
28
+ modal: ProfilePickerModal,
29
+ width: number,
30
+ viewportHeight = 24,
31
+ ): Line[] {
32
+ const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
33
+ chromeRows: 6,
34
+ minContentRows: 5,
35
+ maxContentRows: 9,
36
+ });
37
+ const boxMargin = metrics.margin;
38
+ const boxW = metrics.boxWidth;
39
+ const contentW = metrics.contentWidth;
40
+ const visibleRows = metrics.contentRows;
41
+ const targetContentRows = getStableOverlayContentRows(metrics.contentRows, 8);
42
+ modal.setVisibleRows(visibleRows);
43
+
44
+ const sections: import('./modal-factory.ts').ModalSection[] = [];
45
+
46
+ if (modal.profiles.length === 0) {
47
+ sections.push({
48
+ type: 'text',
49
+ content: 'No saved profiles.',
50
+ style: { fg: '244', dim: true },
51
+ });
52
+ sections.push({
53
+ type: 'text',
54
+ content: 'Press [s] to save the current settings as a profile.',
55
+ style: { fg: '240', dim: true },
56
+ });
57
+ } else {
58
+ // Column widths: name(24) | timestamp(16) | preview(remaining)
59
+ const nameW = 24;
60
+ const tsW = 16;
61
+ const previewW = Math.max(4, contentW - nameW - tsW - 4);
62
+
63
+ // Column header
64
+ const nameHdr = fitDisplay('Name', nameW);
65
+ const tsHdr = fitDisplay('Saved', tsW);
66
+ const previewHdr = fitDisplay('Settings', previewW);
67
+ sections.push({
68
+ type: 'text',
69
+ content: `${nameHdr} ${tsHdr} ${previewHdr}`,
70
+ style: { fg: '240', dim: true },
71
+ });
72
+ sections.push({ type: 'separator' });
73
+
74
+ const visibleProfiles = modal.profiles.slice(modal.scrollOffset, modal.scrollOffset + visibleRows);
75
+ const listItems: import('./modal-factory.ts').ModalListItem[] = visibleProfiles.map((prof, idx) => {
76
+ const isSelected = modal.scrollOffset + idx === modal.selectedIndex;
77
+
78
+ const nameStr = fitDisplay(prof.name, nameW);
79
+
80
+ const tsStr = fitDisplay(formatTimestamp(prof.timestamp), tsW);
81
+
82
+ // Read the profile file to get a preview of settings
83
+ // (We only have name/timestamp in ProfileInfo, so show a placeholder)
84
+ const preview = fitDisplay('(display/provider/behavior)', previewW);
85
+
86
+ const label = `${nameStr} ${tsStr} ${preview}`;
87
+ return { label, selected: isSelected };
88
+ });
89
+
90
+ sections.push({ type: 'list', items: listItems });
91
+ if (modal.profiles.length > visibleRows) {
92
+ sections.push({ type: 'separator' });
93
+ sections.push({
94
+ type: 'text',
95
+ content: `[${modal.scrollOffset + 1}-${Math.min(modal.profiles.length, modal.scrollOffset + visibleRows)} of ${modal.profiles.length}]`,
96
+ style: { fg: '244', dim: true },
97
+ });
98
+ }
99
+ }
100
+
101
+ // Status message if present
102
+ if (modal.statusMessage) {
103
+ sections.push({ type: 'separator' });
104
+ sections.push({
105
+ type: 'text',
106
+ content: modal.statusMessage,
107
+ style: { fg: '#00ffcc' },
108
+ });
109
+ }
110
+ if (modal.deleteConfirmationTarget) {
111
+ sections.push({
112
+ type: 'text',
113
+ content: `Press [d] again to permanently delete ${modal.deleteConfirmationTarget}.`,
114
+ style: { fg: '#f59e0b', dim: true },
115
+ });
116
+ }
117
+
118
+ return ModalFactory.createModal(
119
+ {
120
+ title: 'Profiles',
121
+ width: boxW,
122
+ margin: boxMargin,
123
+ targetContentRows,
124
+ sections,
125
+ hints: ['[Up/Down] Navigate', '[Enter] Load', '[d] Arm/Delete', '[s] Save current', '[Esc] Close'],
126
+ },
127
+ width,
128
+ );
129
+ }
@@ -0,0 +1,98 @@
1
+ import { type Line } from '../types/grid.ts';
2
+ import { UIFactory } from './ui-factory.ts';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
+
5
+ // Rich spinner frames (used by progress indicators)
6
+ export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
+ // Braille thinking spinner frames (used by the orchestrator thinking animation)
8
+ export const THINKING_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
9
+
10
+ /**
11
+ * renderSpinner - Render a spinner with label as a single Line.
12
+ */
13
+ export function renderSpinner(
14
+ frame: string,
15
+ label: string,
16
+ width: number,
17
+ fg: string = '135'
18
+ ): Line {
19
+ const text = ` ${frame} ${label}`;
20
+ return UIFactory.stringToLine(text.padEnd(width), width, { fg, bold: true });
21
+ }
22
+
23
+ /**
24
+ * renderToolProgress - Render tool execution progress.
25
+ * E.g. "[2/5] Editing src/config.ts..."
26
+ */
27
+ export function renderToolProgress(
28
+ current: number,
29
+ total: number,
30
+ label: string,
31
+ width: number
32
+ ): Line[] {
33
+ const counter = `[${current}/${total}]`;
34
+ const text = ` ${counter} ${label}`;
35
+ return [
36
+ UIFactory.stringToLine(text.padEnd(width), width, { fg: '#ffcc00', bold: true }),
37
+ ];
38
+ }
39
+
40
+ /**
41
+ * renderTokenBar - Render a token usage bar for the footer.
42
+ * Shows used/max tokens as a visual bar + numbers.
43
+ */
44
+ export function renderTokenBar(
45
+ used: number,
46
+ max: number,
47
+ width: number,
48
+ model: string,
49
+ toolCount: number
50
+ ): Line[] {
51
+ const lines: Line[] = [];
52
+
53
+ // Stats row
54
+ const usedK = used >= 1000 ? `${(used / 1000).toFixed(1)}k` : String(used);
55
+ const maxK = max >= 1000 ? `${(max / 1000).toFixed(1)}k` : String(max);
56
+ const pct = max > 0 ? Math.min(100, Math.round((used / max) * 100)) : 0;
57
+ const toolStr = toolCount > 0 ? ` tools:${toolCount}` : '';
58
+ const statsText = ` ${model} in:${usedK}/${maxK} (${pct}%)${toolStr}`;
59
+
60
+ // Progress bar
61
+ const barLabel = ' ctx ';
62
+ const barLabelW = getDisplayWidth(barLabel);
63
+ const barW = Math.max(10, Math.floor(width * 0.3));
64
+ const filled = Math.round((pct / 100) * barW);
65
+ const empty = barW - filled;
66
+
67
+ // Color based on usage
68
+ const barFg = pct > 85 ? '#ef4444' : pct > 60 ? '#ffcc00' : '#22c55e';
69
+
70
+ const bar = '#'.repeat(filled) + '-'.repeat(empty);
71
+ const barText = barLabel + bar;
72
+
73
+ const statsW = getDisplayWidth(statsText);
74
+ const barTextW = getDisplayWidth(barText);
75
+ const spacingW = Math.max(1, width - statsW - barTextW - 2);
76
+
77
+ // Build with mixed colors
78
+ const line = UIFactory.stringToLine(statsText + ' '.repeat(spacingW), width, { fg: '244', dim: true });
79
+
80
+ // Overlay the bar with color
81
+ let barStartX = getDisplayWidth(statsText) + spacingW;
82
+ for (const ch of barLabel) {
83
+ if (barStartX >= width) break;
84
+ const cw = getDisplayWidth(ch);
85
+ line[barStartX] = { char: ch, fg: '244', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
86
+ if (cw === 2 && barStartX + 1 < width) line[barStartX + 1] = { ...line[barStartX], char: '' };
87
+ barStartX += cw;
88
+ }
89
+ for (let i = 0; i < filled && barStartX + i < width; i++) {
90
+ line[barStartX + i] = { char: '#', fg: barFg, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
91
+ }
92
+ for (let i = 0; i < empty && barStartX + filled + i < width; i++) {
93
+ line[barStartX + filled + i] = { char: '-', fg: '238', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
94
+ }
95
+
96
+ lines.push(line);
97
+ return lines;
98
+ }
@@ -0,0 +1,120 @@
1
+ import type { Line } from '../types/grid.ts';
2
+ import { createEmptyLine, createStyledCell } from '../types/grid.ts';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
+ import { generateQrMatrix } from '@pellux/goodvibes-sdk/platform/pairing';
5
+
6
+ export { generateQrMatrix };
7
+
8
+ /**
9
+ * Render a QR boolean matrix to terminal Lines using Unicode half-block characters.
10
+ *
11
+ * Two matrix rows map to one terminal row:
12
+ * top=dark, bottom=dark → '█' (FULL BLOCK)
13
+ * top=dark, bottom=light → '▀' (UPPER HALF BLOCK)
14
+ * top=light, bottom=dark → '▄' (LOWER HALF BLOCK)
15
+ * top=light, bottom=light → ' ' (SPACE)
16
+ *
17
+ * @param modules - 2D boolean matrix where true = dark module
18
+ * @param width - Terminal width available for centering
19
+ * @param options - Optional fg/bg overrides
20
+ */
21
+ export function renderQrMatrix(
22
+ modules: readonly boolean[][],
23
+ width: number,
24
+ options?: { fg?: string; bg?: string },
25
+ ): Line[] {
26
+ const fg = options?.fg ?? '#000000';
27
+ const bg = options?.bg ?? '#ffffff';
28
+
29
+ const rows = modules.length;
30
+ const cols = modules[0]?.length ?? 0;
31
+ if (rows === 0 || cols === 0) return [];
32
+
33
+ // Each terminal row covers two matrix rows
34
+ const terminalRows = Math.ceil(rows / 2);
35
+ // Left-align with a single-cell indent. Visually aligns with the text above
36
+ // the QR when rendered with half-block characters; bumping higher
37
+ // mis-registers the finder patterns by a visible unit.
38
+ const leftPad = 1;
39
+
40
+ const lines: Line[] = [];
41
+
42
+ // Prepend a half-height top quiet band: the BOTTOM half of this terminal row
43
+ // is white (QR bg) flush against the QR's first module row below; the TOP half
44
+ // is the terminal's default background (chrome). Using '▄' (LOWER HALF BLOCK)
45
+ // with fg = QR bg and no bg override achieves the half-height effect.
46
+ // Combined with the leftPad=1 on the horizontal axis, this keeps the
47
+ // finder-pattern square margin consistent on both axes without stealing a
48
+ // full row of vertical space.
49
+ {
50
+ const topBand = createEmptyLine(width);
51
+ const endCol = Math.min(leftPad + cols + 1, width);
52
+ for (let col = 0; col < endCol; col++) {
53
+ topBand[col] = createStyledCell('▄', { fg: bg });
54
+ }
55
+ lines.push(topBand);
56
+ }
57
+
58
+ for (let termRow = 0; termRow < terminalRows; termRow++) {
59
+ const matrixRowTop = termRow * 2;
60
+ const matrixRowBot = termRow * 2 + 1;
61
+ const topRow = modules[matrixRowTop];
62
+ const botRow = matrixRowBot < rows ? modules[matrixRowBot] : null;
63
+
64
+ const line = createEmptyLine(width);
65
+
66
+ // Fill leading padding with bg
67
+ for (let col = 0; col < leftPad && col < width; col++) {
68
+ line[col] = createStyledCell(' ', { fg, bg });
69
+ }
70
+
71
+ // Render QR columns
72
+ for (let col = 0; col < cols; col++) {
73
+ const termCol = leftPad + col;
74
+ if (termCol >= width) break;
75
+
76
+ const topDark = topRow ? (topRow[col] ?? false) : false;
77
+ const botDark = botRow ? (botRow[col] ?? false) : false;
78
+
79
+ let char: string;
80
+ let cellFg: string;
81
+ let cellBg: string;
82
+
83
+ if (topDark && botDark) {
84
+ char = '█';
85
+ cellFg = fg;
86
+ cellBg = bg;
87
+ } else if (topDark && !botDark) {
88
+ char = '▀';
89
+ cellFg = fg;
90
+ cellBg = bg;
91
+ } else if (!topDark && botDark) {
92
+ char = '▄';
93
+ cellFg = fg;
94
+ cellBg = bg;
95
+ } else {
96
+ char = ' ';
97
+ cellFg = fg;
98
+ cellBg = bg;
99
+ }
100
+
101
+ // Some terminals may not render block chars at full width — guard
102
+ const charWidth = getDisplayWidth(char);
103
+ if (charWidth <= 0) {
104
+ line[termCol] = createStyledCell(' ', { fg: cellFg, bg: cellBg });
105
+ } else {
106
+ line[termCol] = createStyledCell(char, { fg: cellFg, bg: cellBg });
107
+ }
108
+ }
109
+
110
+ // Fill trailing with bg up to end of QR block
111
+ for (let col = leftPad + cols; col < leftPad + cols + 1 && col < width; col++) {
112
+ line[col] = createStyledCell(' ', { fg, bg });
113
+ }
114
+
115
+ lines.push(line);
116
+ }
117
+
118
+ return lines;
119
+ }
120
+
@@ -0,0 +1,54 @@
1
+ import type { Line } from '../types/grid.ts';
2
+ import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
+ import type { SearchManager } from '../input/search.ts';
4
+ import { createBottomBarLine, writeBottomBarText } from './bottom-bar.ts';
5
+
6
+ /**
7
+ * Render the search bar as a single Line[] overlay at the bottom of the viewport.
8
+ * Format: [ Find: <query> 3/17 up/down [n] next [N] prev [Esc] close ]
9
+ * The match count is dim grey; the rest of the bar is teal.
10
+ */
11
+ export function renderSearchOverlay(
12
+ manager: SearchManager,
13
+ width: number
14
+ ): Line[] {
15
+ // Match count text — displayed in dim grey, right of query, left of hints
16
+ const matchCount = manager.matches?.length > 0
17
+ ? `${manager.currentMatch + 1}/${manager.matches.length} up/down`
18
+ : manager.query.length > 0
19
+ ? 'No matches'
20
+ : '';
21
+
22
+ const locked = manager.locked;
23
+ const cursor = locked ? '' : '█';
24
+ const queryDisplay = manager.query + cursor;
25
+ const hints = locked
26
+ ? ' [Up/Down] or [jk] navigate [Bksp] edit [Esc] close'
27
+ : ' [Enter/Tab] lock [Esc] close';
28
+ const label = ' Find: ';
29
+ const matchStr = matchCount ? ` ${matchCount}` : '';
30
+
31
+ // Build left portion: label + query (no match count — that gets separate styling)
32
+ const leftPart = label + queryDisplay;
33
+ const hintsW = getDisplayWidth(hints);
34
+ const matchStrW = getDisplayWidth(matchStr);
35
+ // Available width for left content (query area)
36
+ const leftWidth = width - hintsW - matchStrW - 2;
37
+ const truncatedLeft = fitDisplay(
38
+ getDisplayWidth(leftPart) > leftWidth ? truncateDisplay(leftPart, leftWidth) : leftPart,
39
+ leftWidth,
40
+ );
41
+
42
+ // Build the full line text (match count embedded for positional tracking)
43
+ const fullLine = truncatedLeft + matchStr + hints + ' ';
44
+ const line = createBottomBarLine(width, { fg: '#000000', bg: '#00ffcc' });
45
+ writeBottomBarText(line, 0, width, fitDisplay(truncateDisplay(fullLine, width), width), { fg: '#000000', bg: '#00ffcc' });
46
+
47
+ // Overwrite match count segment with dim grey styling
48
+ if (matchStr.length > 0) {
49
+ const matchStart = getDisplayWidth(truncatedLeft);
50
+ writeBottomBarText(line, matchStart, matchStrW, matchStr, { fg: '#888888', bg: '#00ffcc', dim: true });
51
+ }
52
+
53
+ return [line];
54
+ }
@@ -0,0 +1,214 @@
1
+ import { type Line } from '../types/grid.ts';
2
+ import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
+ import type { SelectionModal } from '../input/selection-modal.ts';
4
+ import {
5
+ createOverlayBoxLayout,
6
+ createOverlayContentLine,
7
+ createOverlayFilledBorderLine,
8
+ DEFAULT_OVERLAY_PALETTE,
9
+ OVERLAY_GLYPHS,
10
+ putOverlayText,
11
+ } from './overlay-box.ts';
12
+ import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
13
+ import { fitLabelDetailColumns, wrapWithHangingIndent } from './text-layout.ts';
14
+
15
+ const BORDER_FG = DEFAULT_OVERLAY_PALETTE.borderFg;
16
+ const TITLE_FG = DEFAULT_OVERLAY_PALETTE.titleFg;
17
+ const BODY_FG = DEFAULT_OVERLAY_PALETTE.bodyFg;
18
+ const MUTED_FG = DEFAULT_OVERLAY_PALETTE.mutedFg;
19
+ const CATEGORY_FG = '#4488cc';
20
+ const SELECTED_BG = DEFAULT_OVERLAY_PALETTE.selectedBg;
21
+
22
+ interface CellStyle {
23
+ fg: string;
24
+ bg?: string;
25
+ bold?: boolean;
26
+ dim?: boolean;
27
+ }
28
+
29
+ function putText(line: Line, startX: number, maxWidth: number, text: string, style: CellStyle): void {
30
+ putOverlayText(line, startX, maxWidth, text, style);
31
+ }
32
+
33
+ /**
34
+ * Render the selection modal as Line[] for overlay in the viewport.
35
+ */
36
+ export function renderSelectionModalOverlay(
37
+ modal: SelectionModal,
38
+ width: number,
39
+ viewportHeight = 24,
40
+ ): Line[] {
41
+ const lines: Line[] = [];
42
+ const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
43
+ margin: 4,
44
+ maxWidth: 72,
45
+ chromeRows: modal.allowSearch ? 5 : 4,
46
+ minContentRows: 6,
47
+ maxContentRows: 10,
48
+ });
49
+ const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
50
+
51
+ lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.topLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.topRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.titleBg));
52
+
53
+ const titleLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.titleBg);
54
+ putText(
55
+ titleLine,
56
+ layout.margin + 2,
57
+ layout.innerWidth,
58
+ fitDisplay(truncateDisplay(modal.title, layout.innerWidth), layout.innerWidth),
59
+ { fg: TITLE_FG, bold: true },
60
+ );
61
+ lines.push(titleLine);
62
+
63
+ if (modal.allowSearch) {
64
+ const labelLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
65
+ putText(labelLine, layout.margin + 2, layout.innerWidth, fitDisplay(' Search', layout.innerWidth), {
66
+ fg: CATEGORY_FG,
67
+ dim: true,
68
+ });
69
+ lines.push(labelLine);
70
+ const searchLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.inputBg);
71
+ const prefix = '/ ';
72
+ const queryAreaWidth = layout.innerWidth - getDisplayWidth(prefix);
73
+ const queryValue = modal.query + (modal.searchFocused ? OVERLAY_GLYPHS.cursor : '');
74
+ const queryText = fitDisplay(
75
+ truncateDisplay(queryValue, queryAreaWidth),
76
+ queryAreaWidth,
77
+ );
78
+ putText(searchLine, layout.margin + 2, getDisplayWidth(prefix), prefix, { fg: modal.searchFocused ? BODY_FG : MUTED_FG });
79
+ putText(searchLine, layout.margin + 2 + getDisplayWidth(prefix), queryAreaWidth, queryText, {
80
+ fg: modal.query.length > 0 || modal.searchFocused ? BODY_FG : MUTED_FG,
81
+ });
82
+ lines.push(searchLine);
83
+ lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg));
84
+ } else {
85
+ lines.push(createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg));
86
+ }
87
+
88
+ const listTitle = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
89
+ putText(listTitle, layout.margin + 2, layout.innerWidth, fitDisplay(' Results', layout.innerWidth), {
90
+ fg: CATEGORY_FG,
91
+ dim: true,
92
+ });
93
+ lines.push(listTitle);
94
+
95
+ const items = modal.filteredItems;
96
+ if (items.length === 0) {
97
+ const line = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.bodyBg);
98
+ const message = modal.query ? 'No matching items' : 'No items';
99
+ putText(line, layout.margin + 2, layout.innerWidth, fitDisplay(message, layout.innerWidth), { fg: MUTED_FG, dim: true });
100
+ lines.push(line);
101
+ } else {
102
+ const maxVisible = metrics.contentRows;
103
+ let startIdx = 0;
104
+ if (items.length > maxVisible) {
105
+ startIdx = Math.max(0, Math.min(
106
+ modal.selectedIndex - Math.floor(maxVisible / 2),
107
+ items.length - maxVisible,
108
+ ));
109
+ }
110
+ const endIdx = Math.min(startIdx + maxVisible, items.length);
111
+ let lastCategory: string | undefined;
112
+
113
+ for (let i = startIdx; i < endIdx; i++) {
114
+ const item = items[i];
115
+ const isSelected = i === modal.selectedIndex;
116
+
117
+ if (item.category && item.category !== lastCategory) {
118
+ lastCategory = item.category;
119
+ const categoryLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
120
+ putText(categoryLine, layout.margin + 2, layout.innerWidth, fitDisplay(` ${item.category}`, layout.innerWidth), {
121
+ fg: CATEGORY_FG,
122
+ dim: true,
123
+ });
124
+ lines.push(categoryLine);
125
+ }
126
+
127
+ const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
128
+ const indicatorWidth = 2;
129
+ const remaining = layout.innerWidth - indicatorWidth;
130
+ const labelColor = isSelected ? TITLE_FG : (item.fg ?? BODY_FG);
131
+ const detailColor = isSelected ? BODY_FG : MUTED_FG;
132
+ const labelWidth = item.detail
133
+ ? fitLabelDetailColumns(item.label, item.detail, remaining).labelWidth
134
+ : remaining;
135
+ const labelLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
136
+ putText(labelLine, layout.margin + 2, indicatorWidth, indicator, {
137
+ fg: isSelected ? TITLE_FG : MUTED_FG,
138
+ bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
139
+ bold: isSelected,
140
+ });
141
+ putText(labelLine, layout.margin + 2 + indicatorWidth, labelWidth, fitDisplay(truncateDisplay(item.label, labelWidth), labelWidth), {
142
+ fg: labelColor,
143
+ bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
144
+ bold: isSelected,
145
+ });
146
+ if (item.detail) {
147
+ const detailWidth = fitLabelDetailColumns(item.label, item.detail, remaining).detailWidth;
148
+ if (detailWidth >= 12) {
149
+ putText(labelLine, layout.margin + 2 + indicatorWidth + labelWidth, 2, ' ', {
150
+ fg: BODY_FG,
151
+ bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
152
+ });
153
+ putText(labelLine, layout.margin + 2 + indicatorWidth + labelWidth + 2, detailWidth, fitDisplay(truncateDisplay(item.detail, detailWidth), detailWidth), {
154
+ fg: detailColor,
155
+ bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
156
+ });
157
+ lines.push(labelLine);
158
+ } else {
159
+ lines.push(labelLine);
160
+ const wrappedDetails = wrapWithHangingIndent(item.detail, Math.max(8, remaining), '', 2);
161
+ for (const detailLineText of wrappedDetails) {
162
+ const detailLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
163
+ putText(detailLine, layout.margin + 2 + indicatorWidth, remaining, fitDisplay(truncateDisplay(detailLineText, remaining), remaining), {
164
+ fg: detailColor,
165
+ bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
166
+ dim: !isSelected,
167
+ });
168
+ lines.push(detailLine);
169
+ }
170
+ }
171
+ } else {
172
+ lines.push(labelLine);
173
+ }
174
+ }
175
+
176
+ if (items.length > maxVisible) {
177
+ const above = startIdx;
178
+ const below = items.length - endIdx;
179
+ const scrollHint = above > 0 && below > 0
180
+ ? `(${above} above, ${below} below)`
181
+ : below > 0
182
+ ? `(${below} below)`
183
+ : `(${above} above)`;
184
+ const hintLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
185
+ putText(hintLine, layout.margin + 2, layout.innerWidth, fitDisplay(scrollHint, layout.innerWidth), { fg: MUTED_FG, dim: true });
186
+ lines.push(hintLine);
187
+ }
188
+ }
189
+
190
+ const footerLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
191
+ const selectedItem = modal.getSelected();
192
+ const primaryVerb = selectedItem?.primaryAction === 'toggle'
193
+ ? '[Enter] Toggle'
194
+ : selectedItem?.primaryAction === 'edit'
195
+ ? '[Enter] Edit'
196
+ : selectedItem?.primaryAction === 'delete'
197
+ ? '[Enter] Delete'
198
+ : '[Enter] Select';
199
+ let hints = `[Up/Down] Navigate ${primaryVerb} [Esc] Close`;
200
+ if (modal.allowSearch) hints += ' [/] Search';
201
+ if (selectedItem?.primaryAction === 'toggle' && !selectedItem.actions) hints += ' [Space] Toggle';
202
+ if (selectedItem?.actions) hints += ` ${selectedItem.actions}`;
203
+ putText(
204
+ footerLine,
205
+ layout.margin + 2,
206
+ layout.innerWidth,
207
+ fitDisplay(truncateDisplay(hints, layout.innerWidth), layout.innerWidth),
208
+ { fg: MUTED_FG, dim: true },
209
+ );
210
+ lines.push(footerLine);
211
+ lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg));
212
+
213
+ return lines;
214
+ }