@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,473 @@
1
+ import { type Line } from '../types/grid.ts';
2
+ import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
+ import type { ModelPickerModal } from '../input/model-picker.ts';
4
+ import { EFFORT_DESCRIPTIONS } from '@pellux/goodvibes-sdk/platform/providers';
5
+ import { getQualityTier, getQualityTierFromScore } from '@pellux/goodvibes-sdk/platform/providers';
6
+ import {
7
+ createOverlayBoxLayout,
8
+ createOverlayContentLine,
9
+ createOverlayFilledBorderLine,
10
+ DEFAULT_OVERLAY_PALETTE,
11
+ OVERLAY_GLYPHS,
12
+ putOverlayText,
13
+ } from './overlay-box.ts';
14
+ import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
15
+
16
+ /** Format a context window number into a short human-readable string. */
17
+ function fmtContext(n: number): string {
18
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(0)}M`;
19
+ if (n >= 1000) return `${Math.round(n / 1000)}k`;
20
+ return String(n);
21
+ }
22
+
23
+ /** Title text per picker mode. */
24
+ const MODE_TITLES: Record<string, string> = {
25
+ model: 'Select Model',
26
+ provider: 'Select Provider',
27
+ effort: 'Select Effort Level',
28
+ contextCap: 'Set Context Window',
29
+ };
30
+
31
+ /**
32
+ * Number of fixed chrome lines in the model-picker overlay (title + search + divider + detail×2 + footer).
33
+ * Used by callers to compute maxVisible item rows.
34
+ */
35
+ export const MODEL_PICKER_CHROME_LINES = 7;
36
+
37
+ const renderCache = new WeakMap<ModelPickerModal, { key: string; lines: Line[] }>();
38
+ const objectIds = new WeakMap<object, number>();
39
+ let nextObjectId = 1;
40
+
41
+ function putRowText(line: Line, startX: number, maxWidth: number, text: string, fg: string, bg = '', bold = false, dim = false): void {
42
+ putOverlayText(line, startX, maxWidth, text, { fg, bg, bold, dim });
43
+ }
44
+
45
+ /**
46
+ * Render the model picker modal as Line[] for overlay in the viewport.
47
+ * Handles model, provider, and effort modes.
48
+ *
49
+ * @param maxVisible - Maximum number of item rows to show (controls the scroll window).
50
+ * Derived from viewport height minus chrome lines. Defaults to 20.
51
+ */
52
+ export function renderModelPickerOverlay(
53
+ picker: ModelPickerModal,
54
+ width: number,
55
+ maxVisible = 20,
56
+ viewportHeight?: number,
57
+ ): Line[] {
58
+ const cacheKey = getRenderCacheKey(picker, width, maxVisible, viewportHeight);
59
+ const cached = renderCache.get(picker);
60
+ if (cached?.key === cacheKey) return cached.lines;
61
+
62
+ const lines: Line[] = [];
63
+ const metrics = getOverlaySurfaceMetrics(width, viewportHeight ?? 24, {
64
+ chromeRows: MODEL_PICKER_CHROME_LINES,
65
+ maxWidth: 72,
66
+ minContentRows: 6,
67
+ maxContentRows: Math.max(10, maxVisible),
68
+ });
69
+ const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
70
+ const contentW = layout.innerWidth;
71
+ const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
72
+ const titleFg = DEFAULT_OVERLAY_PALETTE.titleFg;
73
+ const bodyFg = DEFAULT_OVERLAY_PALETTE.bodyFg;
74
+ const mutedFg = DEFAULT_OVERLAY_PALETTE.mutedFg;
75
+ const selectedBg = DEFAULT_OVERLAY_PALETTE.selectedBg;
76
+
77
+ // ── Title bar ───────────────────────────────────────────────────────────────────────
78
+ const titleLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.topLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.topRight, borderFg, DEFAULT_OVERLAY_PALETTE.titleBg);
79
+ putRowText(
80
+ titleLine,
81
+ layout.margin + 2,
82
+ layout.width - 4,
83
+ truncateDisplay((MODE_TITLES[picker.mode] ?? MODE_TITLES.model).replace(/^─\s*/, '').trim(), layout.width - 4),
84
+ titleFg,
85
+ '',
86
+ true,
87
+ );
88
+ lines.push(titleLine);
89
+
90
+ // ── Search bar (model and provider modes) ────────────────────────────────────
91
+ if (picker.mode === 'model' || picker.mode === 'provider') {
92
+ const searchLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.inputBg);
93
+ const searchPrefix = '/ ';
94
+ const queryDisplay = picker.query + (picker.searchFocused ? OVERLAY_GLYPHS.cursor : '');
95
+ let filterTag = '';
96
+ let filterTagW = 0;
97
+ if (picker.mode === 'model') {
98
+ // Category filter indicator — model mode only
99
+ const filterLabels: Record<string, string> = { all: 'All', free: 'Free', paid: 'Paid', subscription: 'Sub' };
100
+ const filterLabel = filterLabels[picker.categoryFilter] ?? 'All';
101
+ filterTag = `[${filterLabel}]`;
102
+ filterTagW = getDisplayWidth(filterTag);
103
+ }
104
+ const maxQueryW = contentW - getDisplayWidth(searchPrefix) - filterTagW - (filterTagW > 0 ? 2 : 0);
105
+ const queryTrunc = getDisplayWidth(queryDisplay) > maxQueryW
106
+ ? truncateDisplay(queryDisplay, maxQueryW)
107
+ : queryDisplay;
108
+ let rowX = layout.margin + 2;
109
+ putRowText(searchLine, rowX, getDisplayWidth(searchPrefix), searchPrefix, picker.searchFocused ? bodyFg : mutedFg);
110
+ rowX += getDisplayWidth(searchPrefix);
111
+ const queryAreaWidth = filterTag
112
+ ? Math.max(0, contentW - getDisplayWidth(searchPrefix) - filterTagW - 1)
113
+ : Math.max(0, contentW - getDisplayWidth(searchPrefix));
114
+ putRowText(searchLine, rowX, queryAreaWidth, fitDisplay(queryTrunc, queryAreaWidth), picker.query.length > 0 || picker.searchFocused ? '#ffffff' : mutedFg);
115
+ if (filterTag) {
116
+ putRowText(
117
+ searchLine,
118
+ layout.margin + 2 + contentW - filterTagW,
119
+ filterTagW,
120
+ filterTag,
121
+ mutedFg,
122
+ );
123
+ }
124
+ lines.push(searchLine);
125
+
126
+ // Thin divider under search bar
127
+ lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
128
+ } else {
129
+ lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
130
+ }
131
+
132
+ if (picker.mode === 'model') {
133
+ // ── Model list (grouped, with scroll window) ────────────────────────────────────
134
+ const filtered = picker.getFilteredModels();
135
+ if (filtered.length === 0) {
136
+ const msg = picker.query.length > 0
137
+ ? `No models match "${picker.query.length > 20 ? picker.query.slice(0, 20) + '...' : picker.query}"`
138
+ : 'No models available';
139
+ const noModels = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
140
+ putRowText(noModels, layout.margin + 2, contentW, fitDisplay(truncateDisplay(msg, contentW), contentW), '244', '', false, true);
141
+ lines.push(noModels);
142
+ } else {
143
+ // Determine the visible slice [scrollOffset, scrollOffset + maxVisible)
144
+ const scrollOffset = Math.max(0, Math.min(picker.scrollOffset, Math.max(0, filtered.length - maxVisible)));
145
+ const visibleEnd = Math.min(filtered.length, scrollOffset + maxVisible);
146
+ const visibleModels = filtered.slice(scrollOffset, visibleEnd);
147
+
148
+ // Scroll indicators
149
+ if (scrollOffset > 0) {
150
+ const upHint = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
151
+ putRowText(upHint, layout.margin + 2, contentW, fitDisplay(`${OVERLAY_GLYPHS.moreAbove} ${scrollOffset} more above`, contentW), mutedFg, '', false, true);
152
+ lines.push(upHint);
153
+ }
154
+
155
+ let lastGroupKey = '';
156
+ // Track the absolute index for group header display
157
+ // Use getModelGroupKey for synthetic sub-group support (Top Models / All Synthetic)
158
+ for (let i = 0; i < visibleModels.length; i++) {
159
+ const model = visibleModels[i];
160
+ const absIdx = scrollOffset + i; // index into filtered[] for selectedIndex comparison
161
+
162
+ // Group header — show when group key changes within the visible window
163
+ // For the first visible item, always check if header is needed
164
+ const groupKey = picker.getModelGroupKey(model);
165
+ if (groupKey !== lastGroupKey) {
166
+ const headerRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
167
+ putRowText(headerRow, layout.margin + 2, contentW, fitDisplay(`[${groupKey}]`, contentW), '#4488cc');
168
+ lines.push(headerRow);
169
+ lastGroupKey = groupKey;
170
+ }
171
+
172
+ const isSelected = absIdx === picker.selectedIndex;
173
+ const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
174
+
175
+ // Pre-compute synthetic info once per model (avoid 3 separate lookups per frame)
176
+ const synthInfo = model.provider === 'synthetic' ? picker.getSyntheticModelInfo(model.id) : null;
177
+
178
+ // Quality tier badge: [S] / [A] / [B] / [C]
179
+ let tier: string | null = null;
180
+ if (model.provider === 'synthetic') {
181
+ if (synthInfo?.bestCompositeScore != null) {
182
+ tier = getQualityTierFromScore(synthInfo.bestCompositeScore);
183
+ }
184
+ } else {
185
+ const bData = picker.getBenchmarkEntry(model);
186
+ tier = bData ? getQualityTier(bData.benchmarks) : null;
187
+ }
188
+ const tierBadge = tier ? `[${tier}]` : ' ';
189
+ // Pin marker: keep the Unicode star instead of ASCII fallback
190
+ const pinStar = picker.pinnedIds.has(model.id) ? '★ ' : ' ';
191
+ // Free badge: dot marker, not an asterisk
192
+ const freeBadge = model.tier === 'free' ? '•' : ' ';
193
+ // Provider count for synthetic models
194
+ let providerCountStr = ' '; // 5 chars wide (fixed)
195
+ if (synthInfo) {
196
+ const countLabel = `(${synthInfo.keyedBackendCount}p)`;
197
+ providerCountStr = countLabel.padEnd(5);
198
+ }
199
+
200
+ // Layout: indicator(2) + pin(2) + id(maxIdLen) + gap(2) + name(remaining) + provCount(5) + free(1) + tier(3)
201
+ const maxIdLen = 20;
202
+ const provCountW = 5;
203
+ const badgesW = 3 + 1 + 2; // tierBadge(3) + freeBadge(1) + gap(2)
204
+ const idStr = model.id.length > maxIdLen
205
+ ? model.id.slice(0, maxIdLen - 3) + '...'
206
+ : model.id.padEnd(maxIdLen);
207
+ const remaining = contentW - maxIdLen - 4 - badgesW - 2 - provCountW; // 4 = indicator+pin, 2 = gap before name
208
+ const nameStr = model.displayName.length > Math.max(0, remaining)
209
+ ? model.displayName.slice(0, Math.max(0, remaining) - 3) + '...'
210
+ : model.displayName.padEnd(Math.max(0, remaining));
211
+
212
+ const row = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
213
+ let x = layout.margin + 2;
214
+ const rowText = indicator + pinStar + idStr + ' ' + nameStr + providerCountStr + ' ' + freeBadge + tierBadge;
215
+ putRowText(row, x, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), isSelected ? titleFg : bodyFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg, isSelected);
216
+ lines.push(row);
217
+ }
218
+
219
+ if (visibleEnd < filtered.length) {
220
+ const remaining2 = filtered.length - visibleEnd;
221
+ const downHint = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
222
+ putRowText(downHint, layout.margin + 2, contentW, fitDisplay(`${OVERLAY_GLYPHS.moreBelow} ${remaining2} more below`, contentW), mutedFg, '', false, true);
223
+ lines.push(downHint);
224
+ }
225
+ }
226
+
227
+ // ── Divider ────────────────────────────────────────────────────────────────────
228
+ lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
229
+
230
+ // ── Capability detail for selected model ────────────────────────────────────────────
231
+ const selected = picker.getSelected();
232
+ if (selected) {
233
+ const providerLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
234
+ putRowText(providerLine, layout.margin + 2, contentW, fitDisplay(`Provider: ${selected.provider}`, contentW), '244');
235
+ lines.push(providerLine);
236
+
237
+ const caps = selected.capabilities ?? { reasoning: false, multimodal: false, toolCalling: false, codeEditing: false };
238
+ const ctxStr = `Context: ${fmtContext(selected.contextWindow)}`;
239
+ const capParts: string[] = [ctxStr];
240
+ if (caps.reasoning) capParts.push('Reasoning: \u2713');
241
+ if (caps.multimodal) capParts.push('Vision: \u2713');
242
+ if (caps.toolCalling) capParts.push('Tools: \u2713');
243
+ if (caps.codeEditing) capParts.push('Code: \u2713');
244
+ const capText = capParts.join(' ');
245
+ const capLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
246
+ putRowText(capLine, layout.margin + 2, contentW, fitDisplay(truncateDisplay(capText, contentW), contentW), '244');
247
+ lines.push(capLine);
248
+ } else {
249
+ lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
250
+ lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
251
+ }
252
+ } else if (picker.mode === 'provider') {
253
+ // ── Provider list (grouped: Popular / All Providers) ───────────────────────────────────
254
+ const allProviderItems = picker.getItems(); // includes group headers
255
+ const selectableCount = picker.getFilteredProviders().length;
256
+ if (selectableCount === 0) {
257
+ const msg = picker.query.length > 0
258
+ ? `No providers match "${picker.query.length > 20 ? picker.query.slice(0, 20) + '...' : picker.query}"`
259
+ : 'No providers available';
260
+ const noProviders = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
261
+ putRowText(noProviders, layout.margin + 2, contentW, fitDisplay(truncateDisplay(msg, contentW), contentW), '244', '', false, true);
262
+ lines.push(noProviders);
263
+ } else {
264
+ // Build the flat selectable index → item-list-index mapping for scroll tracking
265
+ // scrollOffset / selectedIndex track selectable items only
266
+ const providerScrollOffset = Math.max(0, Math.min(picker.scrollOffset, Math.max(0, selectableCount - maxVisible)));
267
+ const providerVisibleEnd = Math.min(selectableCount, providerScrollOffset + maxVisible);
268
+
269
+ // Scroll indicator — items above
270
+ if (providerScrollOffset > 0) {
271
+ const upHint = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
272
+ putRowText(upHint, layout.margin + 2, contentW, fitDisplay(`${OVERLAY_GLYPHS.moreAbove} ${providerScrollOffset} more above`, contentW), mutedFg, '', false, true);
273
+ lines.push(upHint);
274
+ }
275
+
276
+ // Walk all provider items (headers + selectables), rendering only selectables
277
+ // in [providerScrollOffset, providerVisibleEnd). Headers are shown when the
278
+ // first selectable item in their group is visible.
279
+ let selectableIdx = -1;
280
+ let pendingHeader: string | null = null;
281
+
282
+ for (const item of allProviderItems) {
283
+ if (item.isGroupHeader) {
284
+ pendingHeader = item.label;
285
+ continue;
286
+ }
287
+ selectableIdx++;
288
+ if (selectableIdx < providerScrollOffset) {
289
+ pendingHeader = null; // group header passed, no longer pending
290
+ continue;
291
+ }
292
+ if (selectableIdx >= providerVisibleEnd) break;
293
+
294
+ // Emit pending group header before first visible item in the group
295
+ if (pendingHeader !== null) {
296
+ const headerRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
297
+ putRowText(headerRow, layout.margin + 2, contentW, fitDisplay(`[${pendingHeader}]`, contentW), '#4488cc');
298
+ lines.push(headerRow);
299
+ pendingHeader = null;
300
+ }
301
+
302
+ const isSelected = selectableIdx === picker.selectedIndex;
303
+ const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
304
+ const checkmark = item.isConfigured ? '✓ ' : ' ';
305
+ // configuredVia badge: right-aligned short label (env/sub/anon)
306
+ const viaBadge = item.configuredVia === 'env' ? ' [env]'
307
+ : item.configuredVia === 'secrets' ? ' [key]'
308
+ : item.configuredVia === 'subscription' ? ' [sub]'
309
+ : item.configuredVia === 'anonymous' ? ' [anon]'
310
+ : '';
311
+ const badgeW = viaBadge.length;
312
+ const labelW = contentW - 2 - 2 - badgeW; // indicator(2) + checkmark(2) + badge
313
+ const labelStr = item.label.length > labelW
314
+ ? item.label.slice(0, labelW - 3) + '...'
315
+ : item.label.padEnd(labelW);
316
+ const row = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
317
+ const rowText = indicator + checkmark + labelStr + viaBadge;
318
+ putRowText(row, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), isSelected ? titleFg : bodyFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg, isSelected);
319
+ lines.push(row);
320
+ }
321
+
322
+ // Scroll indicator — items below
323
+ if (providerVisibleEnd < selectableCount) {
324
+ const remaining2 = selectableCount - providerVisibleEnd;
325
+ const downHint = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
326
+ putRowText(downHint, layout.margin + 2, contentW, fitDisplay(`${OVERLAY_GLYPHS.moreBelow} ${remaining2} more below`, contentW), mutedFg, '', false, true);
327
+ lines.push(downHint);
328
+ }
329
+ }
330
+
331
+ // ── Divider + hint ──────────────────────────────────────────────────────────────────
332
+ lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
333
+ const hintLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
334
+ putRowText(hintLine, layout.margin + 2, contentW, fitDisplay('Select a provider to browse its models', contentW), '244');
335
+ lines.push(hintLine);
336
+ lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
337
+ } else if (picker.mode === 'contextCap') {
338
+ // ── Context cap input ──────────────────────────────────────────────────────────────
339
+ const capModel = picker.contextCapPendingModel;
340
+ const modelName = capModel ? capModel.displayName : 'unknown';
341
+ const currentCtx = capModel ? fmtContext(capModel.contextWindow) : '?';
342
+ const provenance = capModel?.contextWindowProvenance ?? 'configured_cap';
343
+
344
+ const promptLabel = 'Context window (tokens):';
345
+ const cursorChar = OVERLAY_GLYPHS.cursor;
346
+ const inputDisplay = picker.contextCapQuery + cursorChar;
347
+ const promptRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.inputBg);
348
+ putRowText(promptRow, layout.margin + 2, contentW, fitDisplay(`${promptLabel} ${inputDisplay}`, contentW), '#ffffff');
349
+ lines.push(promptRow);
350
+
351
+ lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
352
+
353
+ const hintText = `Leave blank to use default (current: ${currentCtx}, source: ${provenance})`;
354
+ const hintTrunc = getDisplayWidth(hintText) > contentW
355
+ ? hintText.slice(0, contentW - 3) + '...'
356
+ : hintText;
357
+ const hintRow = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
358
+ putRowText(hintRow, layout.margin + 2, contentW, fitDisplay(hintTrunc, contentW), '244', '', false, true);
359
+ lines.push(hintRow);
360
+
361
+ // Divider + model info
362
+ lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
363
+ const modelInfoLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
364
+ putRowText(modelInfoLine, layout.margin + 2, contentW, fitDisplay(`Model: ${modelName}`, contentW), '244');
365
+ lines.push(modelInfoLine);
366
+ lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
367
+ } else {
368
+ // ── Effort list ────────────────────────────────────────────────────────────────────────
369
+ for (let i = 0; i < picker.effortLevels.length; i++) {
370
+ const level = picker.effortLevels[i];
371
+ const isSelected = i === picker.selectedIndex;
372
+ const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
373
+ const desc = EFFORT_DESCRIPTIONS[level] ?? '';
374
+ const labelW = 10;
375
+ const labelStr = level.padEnd(labelW);
376
+ const remaining = contentW - labelW - 4;
377
+ const descStr = desc.length > remaining ? desc.slice(0, remaining - 3) + '...' : desc.padEnd(remaining);
378
+ const row = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
379
+ const rowText = indicator + labelStr + ' ' + descStr;
380
+ putRowText(row, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), isSelected ? titleFg : bodyFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg, isSelected);
381
+ lines.push(row);
382
+ }
383
+
384
+ // ── Divider + model context ──────────────────────────────────────────────────────
385
+ lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
386
+ const modelName = picker.pendingModel ? picker.pendingModel.displayName : 'unknown';
387
+ const modelLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
388
+ putRowText(modelLine, layout.margin + 2, contentW, fitDisplay(`Model: ${modelName}`, contentW), '244');
389
+ lines.push(modelLine);
390
+ lines.push(createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg));
391
+ }
392
+
393
+ // ── Bottom border with hints ─────────────────────────────────────────────────────────
394
+ const filterLabelsFooter: Record<string, string> = { all: 'All', free: 'Free', paid: 'Paid', subscription: 'Sub' };
395
+ const filterLabelFooter = filterLabelsFooter[picker.categoryFilter] ?? 'All';
396
+ const groupByLabel = picker.groupBy ?? 'provider';
397
+ const selectedModel = picker.mode === 'model' ? picker.getSelected() : null;
398
+ const showContextCapHint = selectedModel != null && selectedModel.contextWindowProvenance !== undefined;
399
+ const hints = picker.mode === 'model'
400
+ ? showContextCapHint
401
+ ? `[Up/Down] [Enter] [/] Search [Space] Ctx [Esc] [Tab] Filter: ${filterLabelFooter} [G] Group: ${groupByLabel}`
402
+ : `[Up/Down] [Enter] [/] Search [Esc] [Tab] Filter: ${filterLabelFooter} [G] Group: ${groupByLabel}`
403
+ : picker.mode === 'contextCap'
404
+ ? '[Enter] Confirm [Esc] Cancel'
405
+ : '[Up/Down] Nav [Enter] Select [Esc] Cancel';
406
+ const footerLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
407
+ putRowText(footerLine, layout.margin + 2, contentW, fitDisplay(truncateDisplay(hints, contentW), contentW), mutedFg, '', false, true);
408
+ lines.push(footerLine);
409
+
410
+ renderCache.set(picker, { key: cacheKey, lines });
411
+ return lines;
412
+ }
413
+
414
+ function getRenderCacheKey(
415
+ picker: ModelPickerModal,
416
+ width: number,
417
+ maxVisible: number,
418
+ viewportHeight: number | undefined,
419
+ ): string {
420
+ const base = [
421
+ width,
422
+ maxVisible,
423
+ viewportHeight ?? '',
424
+ picker.mode,
425
+ picker.target,
426
+ picker.query,
427
+ picker.searchFocused ? 1 : 0,
428
+ picker.selectedIndex,
429
+ picker.scrollOffset,
430
+ picker.categoryFilter,
431
+ picker.capabilityFilter,
432
+ picker.availableOnly ? 1 : 0,
433
+ picker.benchmarkSort,
434
+ picker.groupBy,
435
+ keyForSet(picker.pinnedIds),
436
+ keyForSet(picker.configuredProviders),
437
+ ];
438
+
439
+ if (picker.mode === 'model') {
440
+ const filtered = picker.getFilteredModels();
441
+ const selected = filtered[picker.selectedIndex];
442
+ base.push(objectId(picker.models), objectId(filtered), filtered.length, selected?.registryKey ?? selected?.id ?? '');
443
+ } else if (picker.mode === 'provider') {
444
+ const filteredProviders = picker.getFilteredProviders();
445
+ base.push(objectId(picker.providers), objectId(filteredProviders), filteredProviders.length, keyForMap(picker.configuredViaMap));
446
+ } else if (picker.mode === 'effort') {
447
+ base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
448
+ } else if (picker.mode === 'contextCap') {
449
+ base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '');
450
+ }
451
+
452
+ return base.join('\u001e');
453
+ }
454
+
455
+ function objectId(value: object): number {
456
+ const existing = objectIds.get(value);
457
+ if (existing !== undefined) return existing;
458
+ const next = nextObjectId++;
459
+ objectIds.set(value, next);
460
+ return next;
461
+ }
462
+
463
+ function keyForSet(values: ReadonlySet<string>): string {
464
+ return values.size === 0 ? '' : [...values].sort().join('\u001f');
465
+ }
466
+
467
+ function keyForMap(values: ReadonlyMap<string, string | undefined>): string {
468
+ if (values.size === 0) return '';
469
+ return [...values.entries()]
470
+ .sort(([left], [right]) => left.localeCompare(right))
471
+ .map(([key, value]) => `${key}\u001d${value ?? ''}`)
472
+ .join('\u001f');
473
+ }