@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,132 @@
1
+ import { type Line, type Cell, createStyledCell } from '../types/grid.ts';
2
+ import { UIFactory } from './ui-factory.ts';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
+
5
+ /**
6
+ * renderDiffView - Render a unified diff string as styled Line[].
7
+ * '+' lines in green, '-' lines in red, '@@' hunks in cyan.
8
+ */
9
+ export function renderDiffView(diffText: string, width: number, filename?: string): Line[] {
10
+ const lines: Line[] = [];
11
+ const BG = '#0a0a0a';
12
+
13
+ // Filename header
14
+ if (filename) {
15
+ const header = ` ≡ ${filename} `;
16
+ lines.push(UIFactory.stringToLine(header.padEnd(width), width, { fg: '#1a1a1a', bg: '#569cd6', bold: true }));
17
+ }
18
+
19
+ const diffLines = diffText.split('\n');
20
+ let oldLineNo = 0;
21
+ let newLineNo = 0;
22
+
23
+ for (const raw of diffLines) {
24
+ if (raw === '') {
25
+ const emptyLine = makeFilledLine(width, BG);
26
+ lines.push(emptyLine);
27
+ continue;
28
+ }
29
+
30
+ // Hunk header: @@ -old,count +new,count @@
31
+ if (raw.startsWith('@@')) {
32
+ const hunkMatch = raw.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
33
+ if (hunkMatch) {
34
+ oldLineNo = parseInt(hunkMatch[1], 10) - 1;
35
+ newLineNo = parseInt(hunkMatch[2], 10) - 1;
36
+ }
37
+ lines.push(makeStyledLine(raw, width, '#00bcd4', '#0f1f1f', false));
38
+ continue;
39
+ }
40
+
41
+ // File headers: --- and +++
42
+ if (raw.startsWith('--- ') || raw.startsWith('+++ ')) {
43
+ lines.push(makeStyledLine(raw, width, '244', BG, false));
44
+ continue;
45
+ }
46
+
47
+ // Added line
48
+ if (raw.startsWith('+')) {
49
+ newLineNo++;
50
+ const lineLabel = `${String(newLineNo).padStart(4)} `;
51
+ const content = raw.slice(1);
52
+ lines.push(makeGutterLine('+', lineLabel, content, width, '#22c55e', '#0a1a0a'));
53
+ continue;
54
+ }
55
+
56
+ // Removed line
57
+ if (raw.startsWith('-')) {
58
+ oldLineNo++;
59
+ const lineLabel = `${String(oldLineNo).padStart(4)} `;
60
+ const content = raw.slice(1);
61
+ lines.push(makeGutterLine('-', lineLabel, content, width, '#ef4444', '#1a0a0a'));
62
+ continue;
63
+ }
64
+
65
+ // Context line
66
+ if (raw.startsWith(' ') || (!raw.startsWith('\\') && raw.length > 0)) {
67
+ oldLineNo++;
68
+ newLineNo++;
69
+ const lineLabel = `${String(oldLineNo).padStart(4)} `;
70
+ const content = raw.startsWith(' ') ? raw.slice(1) : raw;
71
+ lines.push(makeGutterLine(' ', lineLabel, content, width, '244', BG));
72
+ }
73
+ }
74
+
75
+ return lines;
76
+ }
77
+
78
+ /** Build a line with gutter indicator, line number, and content. */
79
+ function makeGutterLine(
80
+ gutter: string,
81
+ lineLabel: string,
82
+ content: string,
83
+ width: number,
84
+ fg: string,
85
+ bg: string
86
+ ): Line {
87
+ const line = makeFilledLine(width, bg);
88
+ let cx = 0;
89
+
90
+ // Gutter char
91
+ line[cx++] = createStyledCell(gutter, { fg, bg, bold: gutter !== ' ' });
92
+
93
+ // Line number
94
+ for (const ch of lineLabel) {
95
+ if (cx >= width) break;
96
+ line[cx++] = createStyledCell(ch, { fg: '238', bg, dim: true });
97
+ }
98
+
99
+ // Content
100
+ for (const ch of content) {
101
+ if (cx >= width) break;
102
+ const cw = getDisplayWidth(ch);
103
+ const code = ch.charCodeAt(0);
104
+ if (code < 32) { cx++; continue; }
105
+ line[cx] = createStyledCell(ch, { fg, bg });
106
+ if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
107
+ cx += cw;
108
+ }
109
+
110
+ return line;
111
+ }
112
+
113
+ /** Build a simple styled line from text. */
114
+ function makeStyledLine(text: string, width: number, fg: string, bg: string, bold: boolean): Line {
115
+ const line = makeFilledLine(width, bg);
116
+ let cx = 0;
117
+ for (const ch of text) {
118
+ if (cx >= width) break;
119
+ const cw = getDisplayWidth(ch);
120
+ const code = ch.charCodeAt(0);
121
+ if (code < 32) { cx++; continue; }
122
+ line[cx] = createStyledCell(ch, { fg, bg, bold });
123
+ if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
124
+ cx += cw;
125
+ }
126
+ return line;
127
+ }
128
+
129
+ /** Create a line filled with bg color. */
130
+ function makeFilledLine(width: number, bg: string): Cell[] {
131
+ return Array.from({ length: width }, () => createStyledCell(' ', { bg }));
132
+ }
@@ -0,0 +1,130 @@
1
+ import { TerminalBuffer } from './buffer.ts';
2
+ import { type Cell } from '../types/grid.ts';
3
+
4
+ /**
5
+ * DiffEngine - Generates minimal ANSI updates between two buffers.
6
+ */
7
+ export class DiffEngine {
8
+ private lastFg = '';
9
+ private lastBg = '';
10
+ private lastBold = false;
11
+ private lastDim = false;
12
+ private lastUnderline = false;
13
+ private lastItalic = false;
14
+ private lastStrikethrough = false;
15
+ private lastLink = '';
16
+
17
+ public reset(): void {
18
+ this.lastFg = '';
19
+ this.lastBg = '';
20
+ this.lastBold = false;
21
+ this.lastDim = false;
22
+ this.lastUnderline = false;
23
+ this.lastItalic = false;
24
+ this.lastStrikethrough = false;
25
+ this.lastLink = '';
26
+ }
27
+
28
+ public diff(oldBuffer: TerminalBuffer | null, newBuffer: TerminalBuffer): string {
29
+ let output = '';
30
+
31
+ for (let y = 0; y < newBuffer.height; y++) {
32
+ // Skip rows that were not written in either the old or new buffer.
33
+ // If neither side touched the row, both must match the prior frame:
34
+ // old row was never written this frame (clean) and new row is also
35
+ // clean, so the on-screen content is still correct. No diff needed.
36
+ const newDirty = newBuffer.dirtyRows[y] ?? false;
37
+ const oldDirty = oldBuffer ? (oldBuffer.dirtyRows[y] ?? false) : true;
38
+ if (!newDirty && !oldDirty) continue;
39
+
40
+ for (let x = 0; x < newBuffer.width; x++) {
41
+ const oldCell = oldBuffer?.getCell(x, y);
42
+ const newCell = newBuffer.cells[y]?.[x];
43
+ if (!newCell || newCell.char === '') continue;
44
+
45
+ if (this.isCellDifferent(oldCell, newCell)) {
46
+ output += `\x1b[${y + 1};${x + 1}H`;
47
+ output += this.applyStyles(newCell);
48
+ output += newCell.char;
49
+ }
50
+ }
51
+ }
52
+
53
+ // Close any open OSC 8 hyperlink at end of frame
54
+ if (this.lastLink) {
55
+ output += '\x1b]8;;\x1b\\';
56
+ this.lastLink = '';
57
+ }
58
+
59
+ return output;
60
+ }
61
+
62
+ private isCellDifferent(a: Cell | undefined, b: Cell): boolean {
63
+ if (!a) return true;
64
+ return a.char !== b.char || a.fg !== b.fg || a.bg !== b.bg || a.bold !== b.bold || a.dim !== b.dim ||
65
+ a.underline !== b.underline || a.italic !== b.italic || a.strikethrough !== b.strikethrough ||
66
+ (a.link ?? '') !== (b.link ?? '');
67
+ }
68
+
69
+ private sanitizeColor(color: string): string {
70
+ if (color.startsWith('#')) {
71
+ const r = parseInt(color.slice(1, 3), 16);
72
+ const g = parseInt(color.slice(3, 5), 16);
73
+ const b = parseInt(color.slice(5, 7), 16);
74
+ return `${r};${g};${b}`;
75
+ }
76
+ return color;
77
+ }
78
+
79
+ private applyStyles(cell: Cell): string {
80
+ let style = '';
81
+ const fg = this.sanitizeColor(cell.fg);
82
+ const bg = this.sanitizeColor(cell.bg);
83
+ const link = cell.link ?? '';
84
+
85
+ const changed = fg !== this.lastFg || bg !== this.lastBg ||
86
+ cell.bold !== this.lastBold || cell.dim !== this.lastDim ||
87
+ cell.underline !== this.lastUnderline || cell.italic !== this.lastItalic ||
88
+ cell.strikethrough !== this.lastStrikethrough;
89
+
90
+ if (changed) {
91
+ style += '\x1b[0m'; // Reset all attributes
92
+ if (cell.bold) style += '\x1b[1m';
93
+ if (cell.dim) style += '\x1b[2m';
94
+ if (cell.italic) style += '\x1b[3m';
95
+ if (cell.underline) style += '\x1b[4m';
96
+ if (cell.strikethrough) style += '\x1b[9m';
97
+
98
+ if (fg) {
99
+ const isRgb = fg.includes(';');
100
+ style += isRgb ? `\x1b[38;2;${fg}m` : `\x1b[38;5;${fg}m`;
101
+ }
102
+ if (bg) {
103
+ const isRgb = bg.includes(';');
104
+ style += isRgb ? `\x1b[48;2;${bg}m` : `\x1b[48;5;${bg}m`;
105
+ }
106
+
107
+ this.lastFg = fg;
108
+ this.lastBg = bg;
109
+ this.lastBold = cell.bold;
110
+ this.lastDim = cell.dim;
111
+ this.lastUnderline = cell.underline;
112
+ this.lastItalic = cell.italic;
113
+ this.lastStrikethrough = cell.strikethrough;
114
+ }
115
+
116
+ // OSC 8 hyperlink: emit open/close/change sequences only when link changes
117
+ if (link !== this.lastLink) {
118
+ if (link) {
119
+ // Open new hyperlink (close previous if any was open)
120
+ style += `\x1b]8;;${link}\x1b\\`;
121
+ } else {
122
+ // Close hyperlink
123
+ style += `\x1b]8;;\x1b\\`;
124
+ }
125
+ this.lastLink = link;
126
+ }
127
+
128
+ return style;
129
+ }
130
+ }
@@ -0,0 +1,101 @@
1
+ import { type Line } from '../types/grid.ts';
2
+ import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
+ import type { FilePickerModal } from '../input/file-picker.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
+
14
+ /**
15
+ * Render the file picker modal as Line[] for overlay in the viewport.
16
+ * Shows a bordered box with fuzzy-matched file results.
17
+ */
18
+ export function renderFilePickerOverlay(
19
+ picker: FilePickerModal,
20
+ width: number,
21
+ viewportHeight = 24,
22
+ ): Line[] {
23
+ const lines: Line[] = [];
24
+ const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
25
+ chromeRows: 4,
26
+ maxWidth: 70,
27
+ minContentRows: 6,
28
+ maxContentRows: 10,
29
+ });
30
+ const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
31
+ const contentW = layout.innerWidth;
32
+ const borderFg = DEFAULT_OVERLAY_PALETTE.borderFg;
33
+ const titleFg = DEFAULT_OVERLAY_PALETTE.titleFg;
34
+ const bodyFg = DEFAULT_OVERLAY_PALETTE.bodyFg;
35
+ const mutedFg = DEFAULT_OVERLAY_PALETTE.mutedFg;
36
+ const selectedBg = DEFAULT_OVERLAY_PALETTE.selectedBg;
37
+
38
+ // Title bar
39
+ const titleLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.topLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.topRight, borderFg, DEFAULT_OVERLAY_PALETTE.titleBg);
40
+ putOverlayText(titleLine, layout.margin + 2, layout.width - 4, 'Select File', { fg: titleFg, bold: true });
41
+ lines.push(titleLine);
42
+
43
+ // Search input
44
+ const queryDisplay = picker.query || '';
45
+ const searchLine = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.inputBg);
46
+ const searchPrefix = '@ ';
47
+ const queryText = fitDisplay(`${queryDisplay}${picker.searchFocused ? OVERLAY_GLYPHS.cursor : ''}`, Math.max(0, contentW - getDisplayWidth(searchPrefix)));
48
+ putOverlayText(searchLine, layout.margin + 2, getDisplayWidth(searchPrefix), searchPrefix, { fg: picker.searchFocused ? bodyFg : mutedFg });
49
+ putOverlayText(searchLine, layout.margin + 2 + getDisplayWidth(searchPrefix), contentW - getDisplayWidth(searchPrefix), queryText, { fg: picker.query.length > 0 || picker.searchFocused ? bodyFg : mutedFg });
50
+ lines.push(searchLine);
51
+
52
+ // Separator
53
+ lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg));
54
+
55
+ // Results
56
+ if (picker.results.length === 0) {
57
+ const noResults = createOverlayContentLine(width, layout, borderFg, DEFAULT_OVERLAY_PALETTE.bodyBg);
58
+ putOverlayText(noResults, layout.margin + 2, contentW, fitDisplay('No matching files', contentW), { fg: '244', dim: true });
59
+ lines.push(noResults);
60
+ } else {
61
+ const maxVisible = metrics.contentRows;
62
+ let startIdx = 0;
63
+ if (picker.results.length > maxVisible) {
64
+ startIdx = Math.max(0, Math.min(
65
+ picker.selectedIndex - Math.floor(maxVisible / 2),
66
+ picker.results.length - maxVisible,
67
+ ));
68
+ }
69
+ const endIdx = Math.min(startIdx + maxVisible, picker.results.length);
70
+
71
+ for (let i = startIdx; i < endIdx; i++) {
72
+ const file = picker.results[i];
73
+ const isSelected = i === picker.selectedIndex;
74
+ const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
75
+ const displayFile = getDisplayWidth(file) > contentW - 2
76
+ ? truncateDisplay(file, contentW - 2)
77
+ : file;
78
+ const line = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
79
+ putOverlayText(
80
+ line,
81
+ layout.margin + 2,
82
+ contentW,
83
+ fitDisplay(indicator + fitDisplay(displayFile, contentW - 2), contentW),
84
+ {
85
+ fg: isSelected ? titleFg : file.endsWith('/') ? titleFg : bodyFg,
86
+ bg: isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg,
87
+ bold: isSelected,
88
+ },
89
+ );
90
+ lines.push(line);
91
+ }
92
+ }
93
+
94
+ // Bottom border with hints
95
+ const hints = '[Up/Down] Navigate [/] Search [Enter] Select [Esc] Cancel';
96
+ const bottomLine = createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, borderFg, DEFAULT_OVERLAY_PALETTE.sectionBg);
97
+ putOverlayText(bottomLine, layout.margin + 2, layout.width - 4, truncateDisplay(hints, layout.width - 4), { fg: mutedFg, dim: true });
98
+ lines.push(bottomLine);
99
+
100
+ return lines;
101
+ }
@@ -0,0 +1,153 @@
1
+ import { type Line, createStyledCell } from '../types/grid.ts';
2
+ import { UIFactory } from './ui-factory.ts';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
+
5
+ /** Color by file extension category. */
6
+ function getFileColor(name: string): string {
7
+ if (name.endsWith('/')) return '#00ffff'; // directory
8
+ const ext = name.split('.').pop()?.toLowerCase() ?? '';
9
+ if (['ts', 'tsx', 'js', 'jsx', 'mjs'].includes(ext)) return '#dcdcaa';
10
+ if (['json', 'yaml', 'yml', 'toml'].includes(ext)) return '#ce9178';
11
+ if (['md', 'txt', 'rst'].includes(ext)) return '252';
12
+ if (['sh', 'bash', 'zsh'].includes(ext)) return '#22c55e';
13
+ if (['css', 'scss', 'less'].includes(ext)) return '#569cd6';
14
+ if (['html', 'htm', 'xml', 'svg'].includes(ext)) return '#f97316';
15
+ if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'ico'].includes(ext)) return '#a855f7';
16
+ if (['lock', 'env', 'gitignore'].includes(name)) return '238';
17
+ return '252';
18
+ }
19
+
20
+ export interface FileTreeEntry {
21
+ name: string;
22
+ isDir: boolean;
23
+ size?: number;
24
+ depth: number;
25
+ isLast: boolean;
26
+ isLastAtDepth: boolean[]; // tracks if each ancestor was last child
27
+ }
28
+
29
+ /**
30
+ * renderFileTree - Render a directory listing as a tree with colored file types.
31
+ * Returns Line[] for the cell-based pipeline.
32
+ */
33
+ export function renderFileTree(
34
+ entries: FileTreeEntry[],
35
+ width: number,
36
+ title?: string
37
+ ): Line[] {
38
+ const lines: Line[] = [];
39
+
40
+ // Optional header
41
+ if (title) {
42
+ lines.push(UIFactory.stringToLine(` [dir] ${title}`, width, { fg: '#00ffff', bold: true }));
43
+ lines.push(UIFactory.stringToLine(' ' + '-'.repeat(width - 2), width, { fg: '240' }));
44
+ }
45
+
46
+ for (const entry of entries) {
47
+ const { name, isDir, size, depth, isLast, isLastAtDepth } = entry;
48
+
49
+ // Build tree prefix
50
+ let prefix = '';
51
+ for (let d = 0; d < depth; d++) {
52
+ if (d < depth - 1) {
53
+ prefix += isLastAtDepth[d] ? ' ' : '| ';
54
+ }
55
+ }
56
+ if (depth > 0) {
57
+ prefix += isLast ? '`-- ' : '|-- ';
58
+ } else {
59
+ prefix += '';
60
+ }
61
+
62
+ const displayName = isDir ? name + '/' : name;
63
+ const fg = getFileColor(isDir ? displayName : name);
64
+
65
+ // Size info (dimmed)
66
+ let sizeStr = '';
67
+ if (size !== undefined && !isDir) {
68
+ if (size < 1024) sizeStr = ` ${size}B`;
69
+ else if (size < 1024 * 1024) sizeStr = ` ${(size / 1024).toFixed(1)}K`;
70
+ else sizeStr = ` ${(size / (1024 * 1024)).toFixed(1)}M`;
71
+ }
72
+
73
+ const fullText = prefix + displayName;
74
+ const sizeW = getDisplayWidth(sizeStr);
75
+ const nameW = getDisplayWidth(fullText);
76
+ const maxNameW = width - sizeW - 1;
77
+
78
+ const truncated = nameW > maxNameW ? fullText.slice(0, Math.max(0, maxNameW - 3)) + '...' : fullText;
79
+
80
+ // Compose: name + padding + size
81
+ const paddingW = Math.max(0, width - getDisplayWidth(truncated) - sizeW);
82
+ const fullLine = truncated + ' '.repeat(paddingW);
83
+
84
+ // Build the line with per-character styles
85
+ const line = UIFactory.stringToLine(fullLine, width, { fg });
86
+
87
+ // Dim the size info
88
+ if (sizeStr && sizeW > 0) {
89
+ const sizeStartX = width - sizeW;
90
+ let cx = sizeStartX;
91
+ for (const ch of sizeStr) {
92
+ if (cx >= width) break;
93
+ const cw = getDisplayWidth(ch);
94
+ line[cx] = createStyledCell(ch, { fg: '240', dim: true });
95
+ if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
96
+ cx += cw;
97
+ }
98
+ }
99
+
100
+ lines.push(line);
101
+ }
102
+
103
+ return lines;
104
+ }
105
+
106
+ /**
107
+ * parseListDirOutput - Convert a simple list directory output string to FileTreeEntry[].
108
+ * Expects one path per line, relative paths with '/' for directories.
109
+ */
110
+ export function parseListDirOutput(output: string, rootDir: string): FileTreeEntry[] {
111
+ const rawLines = output.trim().split('\n').filter(Boolean);
112
+ const entries: FileTreeEntry[] = [];
113
+
114
+ for (const line of rawLines) {
115
+ const trimmed = line.trim();
116
+ if (!trimmed) continue;
117
+ const isDir = trimmed.endsWith('/');
118
+ const name = isDir ? trimmed.slice(0, -1).split('/').pop() + '/' : trimmed.split('/').pop() ?? trimmed;
119
+ const depth = (trimmed.match(/\//g) ?? []).length - (isDir ? 1 : 0);
120
+
121
+ entries.push({
122
+ name: name || trimmed,
123
+ isDir,
124
+ depth: Math.max(0, depth),
125
+ isLast: false,
126
+ isLastAtDepth: [],
127
+ });
128
+ }
129
+
130
+ // Mark isLast for each entry (last sibling at same depth under same parent)
131
+ for (let i = 0; i < entries.length; i++) {
132
+ const nextSameOrLower = entries.slice(i + 1).findIndex(
133
+ e => e.depth <= entries[i].depth
134
+ );
135
+ entries[i].isLast = nextSameOrLower === 0 || nextSameOrLower === -1;
136
+
137
+ // Build isLastAtDepth[d] = true if the ancestor at depth d was the last child
138
+ // of its parent. This governs whether to draw '| ' or ' ' vertical bars.
139
+ const isLastAtDepth: boolean[] = new Array(entries[i].depth).fill(false);
140
+ for (let d = 0; d < entries[i].depth; d++) {
141
+ // Walk backwards to find the most recent ancestor at depth d
142
+ for (let j = i - 1; j >= 0; j--) {
143
+ if (entries[j].depth === d) {
144
+ isLastAtDepth[d] = entries[j].isLast;
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ entries[i].isLastAtDepth = isLastAtDepth;
150
+ }
151
+
152
+ return entries;
153
+ }
@@ -0,0 +1,130 @@
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 { GLYPHS, UI_TONES } from './ui-primitives.ts';
5
+
6
+ export const FULLSCREEN_PALETTE = {
7
+ border: '#64748b',
8
+ title: '#67e8f9',
9
+ subtitle: '#93c5fd',
10
+ text: '#e2e8f0',
11
+ muted: '#94a3b8',
12
+ dim: '#64748b',
13
+ selectedBg: '#223049',
14
+ categoryBg: '#141b25',
15
+ contextBg: '#121923',
16
+ controlsBg: '#0f141d',
17
+ footerBg: '#111827',
18
+ good: UI_TONES.state.good,
19
+ warn: UI_TONES.state.warn,
20
+ bad: UI_TONES.state.bad,
21
+ info: UI_TONES.state.info,
22
+ } as const;
23
+
24
+ export type FullscreenTextStyle = Partial<Omit<Line[number], 'char'>>;
25
+
26
+ export function clamp(value: number, min: number, max: number): number {
27
+ return Math.max(min, Math.min(max, value));
28
+ }
29
+
30
+ export function fillRange(line: Line, startX: number, endX: number, bg: string): void {
31
+ for (let x = Math.max(0, startX); x <= Math.min(line.length - 1, endX); x += 1) {
32
+ const cell = line[x] ?? createStyledCell(' ');
33
+ line[x] = createStyledCell(cell.char, {
34
+ fg: cell.fg,
35
+ bg,
36
+ bold: cell.bold,
37
+ dim: cell.dim,
38
+ underline: cell.underline,
39
+ italic: cell.italic,
40
+ strikethrough: cell.strikethrough,
41
+ link: cell.link,
42
+ });
43
+ }
44
+ }
45
+
46
+ export function fillWidth(line: Line, startX: number, width: number, bg: string): void {
47
+ if (width <= 0) return;
48
+ fillRange(line, startX, startX + width - 1, bg);
49
+ }
50
+
51
+ export function writeText(
52
+ line: Line,
53
+ startX: number,
54
+ maxWidth: number,
55
+ text: string,
56
+ style: FullscreenTextStyle = {},
57
+ ): void {
58
+ let x = startX;
59
+ let used = 0;
60
+ for (const ch of text) {
61
+ const width = getDisplayWidth(ch);
62
+ if (width <= 0) continue;
63
+ if (used + width > maxWidth || x >= line.length) break;
64
+ line[x] = createStyledCell(ch, style);
65
+ if (width > 1 && x + 1 < line.length) line[x + 1] = createStyledCell(' ', style);
66
+ x += width;
67
+ used += width;
68
+ }
69
+ }
70
+
71
+ export function makeLine(width: number, bg = ''): Line {
72
+ const line = createEmptyLine(width);
73
+ if (bg) fillRange(line, 0, width - 1, bg);
74
+ return line;
75
+ }
76
+
77
+ export function borderLine(width: number, left: string, fill: string, right: string, fg: string = FULLSCREEN_PALETTE.border): Line {
78
+ const line = makeLine(width);
79
+ if (width <= 0) return line;
80
+ line[0] = createStyledCell(left, { fg });
81
+ for (let x = 1; x < width - 1; x += 1) line[x] = createStyledCell(fill, { fg });
82
+ if (width > 1) line[width - 1] = createStyledCell(right, { fg });
83
+ return line;
84
+ }
85
+
86
+ export function contentLine(width: number, bg: string, borderFg: string = FULLSCREEN_PALETTE.border): Line {
87
+ const line = makeLine(width, bg);
88
+ if (width > 0) line[0] = createStyledCell(GLYPHS.frame.vertical, { fg: borderFg });
89
+ if (width > 1) line[width - 1] = createStyledCell(GLYPHS.frame.vertical, { fg: borderFg });
90
+ return line;
91
+ }
92
+
93
+ export function drawVerticalRule(line: Line, x: number, fg: string = FULLSCREEN_PALETTE.border, bg = ''): void {
94
+ if (x < 0 || x >= line.length) return;
95
+ line[x] = createStyledCell(GLYPHS.frame.vertical, { fg, bg });
96
+ }
97
+
98
+ export function drawHorizontalRule(line: Line, startX: number, endX: number, fg: string = FULLSCREEN_PALETTE.border, bg = ''): void {
99
+ for (let x = Math.max(0, startX); x <= Math.min(line.length - 1, endX); x += 1) {
100
+ line[x] = createStyledCell(GLYPHS.frame.horizontal, { fg, bg });
101
+ }
102
+ }
103
+
104
+ export function clipDisplay(text: string, width: number): string {
105
+ if (width <= 0) return '';
106
+ let used = 0;
107
+ let output = '';
108
+ for (const ch of text) {
109
+ const chWidth = getDisplayWidth(ch);
110
+ if (chWidth <= 0) continue;
111
+ if (used + chWidth > width) break;
112
+ output += ch;
113
+ used += chWidth;
114
+ }
115
+ return output;
116
+ }
117
+
118
+ export function padDisplay(text: string, width: number): string {
119
+ const clipped = clipDisplay(text, width);
120
+ return `${clipped}${' '.repeat(Math.max(0, width - getDisplayWidth(clipped)))}`;
121
+ }
122
+
123
+ export function stableWindow(total: number, selectedIndex: number, visibleCount: number): { start: number; end: number } {
124
+ if (total <= 0 || visibleCount <= 0) return { start: 0, end: 0 };
125
+ if (total <= visibleCount) return { start: 0, end: total };
126
+ const selected = clamp(selectedIndex, 0, total - 1);
127
+ const half = Math.floor(visibleCount / 2);
128
+ const start = clamp(selected - half, 0, total - visibleCount);
129
+ return { start, end: start + visibleCount };
130
+ }