@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,635 @@
1
+ import { type Line, type Cell, createStyledCell } from '../types/grid.ts';
2
+ import { UIFactory } from './ui-factory.ts';
3
+ import { renderCodeBlock } from './code-block.ts';
4
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
5
+ import { LAYOUT } from './layout.ts';
6
+
7
+ export interface MarkdownRenderOptions {
8
+ codeBlockLineNumbers?: boolean;
9
+ }
10
+
11
+ /** Module-level set of inline markdown special characters (hoisted out of hot loop). */
12
+ const INLINE_SPECIAL_CHARS = new Set(['[', '`', '*', '_', '~']);
13
+
14
+ function splitTableCells(row: string): string[] {
15
+ const cells = row.trim().split('|').map((c) => c.trim());
16
+ if (cells.length > 0 && cells[0] === '') cells.shift();
17
+ if (cells.length > 0 && cells[cells.length - 1] === '') cells.pop();
18
+ return cells;
19
+ }
20
+
21
+ function isLikelyTableSeparatorRow(row: string): boolean {
22
+ if (!row.includes('|')) return false;
23
+ const cells = splitTableCells(row);
24
+ if (cells.length === 0) return false;
25
+ return cells.every((cell) => /:?-{3,}:?/.test(cell));
26
+ }
27
+
28
+ function isLikelyTableHeaderRow(row: string): boolean {
29
+ return splitTableCells(row).length >= 2;
30
+ }
31
+
32
+ /**
33
+ * renderMarkdown - Parse markdown text into styled Line[].
34
+ * Thin wrapper over renderMarkdownTracked for callers that don't need code-block metadata.
35
+ */
36
+ export function renderMarkdown(text: string, width: number, options: MarkdownRenderOptions = {}): Line[] {
37
+ return renderMarkdownTracked(text, width, options).lines;
38
+ }
39
+
40
+ export interface CodeBlockSpan {
41
+ /** Line offset from the start of renderMarkdown output where this block begins. */
42
+ startOffset: number;
43
+ /** Number of rendered lines occupied by this code block. */
44
+ lineCount: number;
45
+ /** Raw source lines inside the fence (no fence markers). */
46
+ rawContent: string;
47
+ }
48
+
49
+ /**
50
+ * renderMarkdownTracked - Same as renderMarkdown but also returns metadata
51
+ * about every code block encountered, keyed by their line offset in the output.
52
+ * Used by ConversationManager to register code blocks in the blockRegistry.
53
+ */
54
+ export function renderMarkdownTracked(
55
+ text: string,
56
+ width: number,
57
+ options: MarkdownRenderOptions = {},
58
+ ): { lines: ReturnType<typeof renderMarkdown>; codeBlocks: CodeBlockSpan[] } {
59
+ const lines: ReturnType<typeof renderMarkdown> = [];
60
+ const codeBlocks: CodeBlockSpan[] = [];
61
+ const rawLines = text.split('\n');
62
+
63
+ let inCodeBlock = false;
64
+ let codeBlockLang = '';
65
+ let codeBlockLines: string[] = [];
66
+ const indent = LAYOUT.LEFT_MARGIN;
67
+ const contentWidth = LAYOUT.contentWidth(width);
68
+
69
+ for (let i = 0; i < rawLines.length; i++) {
70
+ const raw = rawLines[i];
71
+
72
+ const fenceMatch = raw.match(/^```(\w*)/);
73
+ if (fenceMatch && !inCodeBlock) {
74
+ inCodeBlock = true;
75
+ codeBlockLang = fenceMatch[1] || '';
76
+ codeBlockLines = [];
77
+ continue;
78
+ }
79
+ if (inCodeBlock) {
80
+ if (raw.trimStart().startsWith('```')) {
81
+ const blockStart = lines.length;
82
+ const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
83
+ showLineNumbers: options.codeBlockLineNumbers ?? true,
84
+ });
85
+ codeBlocks.push({
86
+ startOffset: blockStart,
87
+ lineCount: rendered.length,
88
+ rawContent: codeBlockLines.join('\n'),
89
+ });
90
+ lines.push(...rendered);
91
+ inCodeBlock = false;
92
+ codeBlockLang = '';
93
+ codeBlockLines = [];
94
+ } else {
95
+ codeBlockLines.push(raw);
96
+ }
97
+ continue;
98
+ }
99
+
100
+ if (raw.trim() === '') {
101
+ lines.push(UIFactory.stringToLine('', width));
102
+ continue;
103
+ }
104
+
105
+ const h3 = raw.match(/^### (.+)/);
106
+ const h2 = raw.match(/^## (.+)/);
107
+ const h1 = raw.match(/^# (.+)/);
108
+ if (h1) {
109
+ lines.push(UIFactory.stringToLine(' '.repeat(indent) + h1[1].toUpperCase(), width, { fg: '#00ffff', bold: true }));
110
+ lines.push(UIFactory.stringToLine(' '.repeat(indent) + '━'.repeat(Math.min(getDisplayWidth(h1[1]), contentWidth)), width, { fg: '244' }));
111
+ continue;
112
+ }
113
+ if (h2) {
114
+ lines.push(UIFactory.stringToLine(' '.repeat(indent) + h2[1], width, { fg: '#00ffff', bold: true }));
115
+ lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(Math.min(getDisplayWidth(h2[1]), contentWidth)), width, { fg: '240' }));
116
+ continue;
117
+ }
118
+ if (h3) {
119
+ lines.push(UIFactory.stringToLine(' '.repeat(indent) + h3[1], width, { fg: '111', bold: true }));
120
+ continue;
121
+ }
122
+
123
+ const taskMatch = raw.match(/^(\s*)[-*] \[([ xX])\] (.+)/);
124
+ if (taskMatch) {
125
+ const listIndent = Math.floor(taskMatch[1].length / 2);
126
+ const checked = taskMatch[2] !== ' ';
127
+ const bulletX = indent + listIndent * 2;
128
+ const textStartX = bulletX + 4;
129
+ const checkbox = checked ? '\u2611 ' : '\u2610 ';
130
+ const rendered = renderInlineMarkdown(taskMatch[3]);
131
+ const prefix = ' '.repeat(bulletX) + checkbox;
132
+ const style = checked ? { fg: '244', strikethrough: true } : {};
133
+ lines.push(...compositeInlineLine(prefix, rendered, width, { fg: checked ? '#22c55e' : '252', ...style }, textStartX));
134
+ continue;
135
+ }
136
+
137
+ const ulMatch = raw.match(/^(\s*)[-*] (.+)/);
138
+ if (ulMatch) {
139
+ const listIndent = Math.floor(ulMatch[1].length / 2);
140
+ const bulletX = indent + listIndent * 2;
141
+ const textStartX = bulletX + 2;
142
+ const rendered = renderInlineMarkdown(ulMatch[2]);
143
+ const prefix = ' '.repeat(bulletX) + '• ';
144
+ lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '135', bold: false }, textStartX));
145
+ continue;
146
+ }
147
+
148
+ const olMatch = raw.match(/^(\s*)(\d+)\. (.+)/);
149
+ if (olMatch) {
150
+ const listIndent = Math.floor(olMatch[1].length / 2);
151
+ const numStr = olMatch[2] + '. ';
152
+ const bulletX = indent + listIndent * 2;
153
+ const textStartX = bulletX + numStr.length;
154
+ const rendered = renderInlineMarkdown(olMatch[3]);
155
+ const prefix = ' '.repeat(bulletX) + numStr;
156
+ lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '135', bold: false }, textStartX));
157
+ continue;
158
+ }
159
+
160
+ if (/^[-*_]{3,}$/.test(raw.trim())) {
161
+ lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(contentWidth), width, { fg: '240' }));
162
+ continue;
163
+ }
164
+
165
+ const bqMatch = raw.match(/^> (.*)/);
166
+ if (bqMatch) {
167
+ const rendered = renderInlineMarkdown(bqMatch[1]);
168
+ const prefix = ' '.repeat(indent) + '┃ ';
169
+ lines.push(...compositeInlineLine(prefix, rendered, width, { fg: '244', italic: true }, indent + 3));
170
+ continue;
171
+ }
172
+
173
+ if (raw.includes('|') && i + 1 < rawLines.length && isLikelyTableHeaderRow(raw) && isLikelyTableSeparatorRow(rawLines[i + 1])) {
174
+ const tableRows: string[] = [];
175
+ let j = i;
176
+ while (j < rawLines.length && rawLines[j].includes('|')) {
177
+ tableRows.push(rawLines[j]);
178
+ j++;
179
+ }
180
+ i = j - 1;
181
+ lines.push(...renderTable(tableRows, width, indent));
182
+ continue;
183
+ }
184
+
185
+ const rendered = renderInlineMarkdown(raw);
186
+ lines.push(...compositeInlineLine(' '.repeat(indent), rendered, width, {}, indent));
187
+ }
188
+
189
+ if (inCodeBlock && codeBlockLines.length > 0) {
190
+ const blockStart = lines.length;
191
+ const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
192
+ showLineNumbers: options.codeBlockLineNumbers ?? true,
193
+ });
194
+ codeBlocks.push({
195
+ startOffset: blockStart,
196
+ lineCount: rendered.length,
197
+ rawContent: codeBlockLines.join('\n'),
198
+ });
199
+ lines.push(...rendered);
200
+ }
201
+
202
+ return { lines, codeBlocks };
203
+ }
204
+
205
+ /**
206
+ * Strip markdown formatting from text for width measurement.
207
+ * Removes **, *, `, ~~ markers but keeps the inner text.
208
+ */
209
+ function stripMarkdown(text: string): string {
210
+ return text
211
+ .replace(/\*\*(.+?)\*\*/g, '$1') // bold
212
+ .replace(/\*(.+?)\*/g, '$1') // italic
213
+ .replace(/~~(.+?)~~/g, '$1') // strikethrough
214
+ .replace(/`(.+?)`/g, '$1'); // inline code
215
+ }
216
+
217
+ /**
218
+ * Render a markdown table with box-drawing borders.
219
+ * Parses inline markdown in cells, fits to terminal width,
220
+ * truncates long content, and renders with proper styling.
221
+ */
222
+ function renderTable(rows: string[], width: number, indent: number): Line[] {
223
+ const lines: Line[] = [];
224
+
225
+ // Parse rows into cells, skip separator
226
+ const parsedRows: string[][] = [];
227
+ let hasSeparator = false;
228
+ for (const row of rows) {
229
+ const trimmed = row.trim();
230
+ if (isLikelyTableSeparatorRow(trimmed)) {
231
+ hasSeparator = true;
232
+ continue;
233
+ }
234
+ const cells = splitTableCells(trimmed);
235
+ if (cells.length > 0) parsedRows.push(cells);
236
+ }
237
+
238
+ if (parsedRows.length === 0) return lines;
239
+
240
+ const colCount = Math.max(...parsedRows.map(r => r.length));
241
+ const availW = width - indent;
242
+
243
+ // Measure column widths using stripped text (no markdown markers)
244
+ const naturalWidths: number[] = new Array(colCount).fill(0);
245
+ for (const row of parsedRows) {
246
+ for (let c = 0; c < row.length; c++) {
247
+ naturalWidths[c] = Math.max(naturalWidths[c], getDisplayWidth(stripMarkdown(row[c])));
248
+ }
249
+ }
250
+
251
+ // Budget: availW minus borders (colCount+1) minus padding (2 per col)
252
+ const overhead = (colCount + 1) + (colCount * 2);
253
+ const contentBudget = Math.max(colCount, availW - overhead);
254
+ const totalNatural = naturalWidths.reduce((a, b) => a + b, 0);
255
+
256
+ // Compute final column widths
257
+ const colWidths: number[] = new Array(colCount).fill(0);
258
+ if (totalNatural <= contentBudget) {
259
+ // Everything fits
260
+ for (let c = 0; c < colCount; c++) colWidths[c] = naturalWidths[c];
261
+ } else {
262
+ // Proportionally shrink, with minimum of 4 chars per column
263
+ const minW = 4;
264
+ for (let c = 0; c < colCount; c++) {
265
+ colWidths[c] = Math.max(minW, Math.floor((naturalWidths[c] / totalNatural) * contentBudget));
266
+ }
267
+ // Distribute leftover from rounding
268
+ let used = colWidths.reduce((a, b) => a + b, 0);
269
+ for (let c = 0; c < colCount && used < contentBudget; c++) {
270
+ colWidths[c]++;
271
+ used++;
272
+ }
273
+ }
274
+
275
+ const bc = '240'; // border color
276
+
277
+ // Helper: build a border line
278
+ const makeBorder = (left: string, mid: string, right: string, horiz: string): Line => {
279
+ let s = ' '.repeat(indent) + left;
280
+ for (let c = 0; c < colCount; c++) {
281
+ s += horiz.repeat(colWidths[c] + 2) + (c < colCount - 1 ? mid : right);
282
+ }
283
+ return UIFactory.stringToLine(s, width, { fg: bc });
284
+ };
285
+
286
+ // Helper: render a cell's content with inline markdown into Cell[]
287
+ const renderCellContent = (raw: string, maxW: number, isHdr: boolean): Cell[] => {
288
+ const cells: Cell[] = [];
289
+ const tokens = renderInlineMarkdown(raw);
290
+ let w = 0;
291
+
292
+ for (const token of tokens) {
293
+ const text = token.text;
294
+ for (const ch of text) {
295
+ const cw = getDisplayWidth(ch);
296
+ if (w + cw > maxW) {
297
+ // Truncate with ellipsis
298
+ if (cells.length > 0) cells[cells.length - 1] = createStyledCell('\u2026', cells[cells.length - 1]);
299
+ return cells;
300
+ }
301
+ let style: Partial<Cell> = {};
302
+ if (token.type === 'code') {
303
+ style = { fg: '#ffcc00', bg: '#1a1a1a' };
304
+ } else if (token.type === 'link') {
305
+ style = { fg: '#00aaff', underline: true };
306
+ } else {
307
+ style = { ...token.style };
308
+ }
309
+ if (isHdr) {
310
+ style.fg = style.fg || '#00ffff';
311
+ style.bold = true;
312
+ } else {
313
+ style.fg = style.fg || '252';
314
+ }
315
+ cells.push(createStyledCell(ch, style));
316
+ if (cw === 2) cells.push(createStyledCell('', style)); // wide char placeholder
317
+ w += cw;
318
+ }
319
+ }
320
+
321
+ // Pad remaining space
322
+ while (w < maxW) {
323
+ cells.push(createStyledCell(' ', isHdr ? { fg: '#00ffff' } : { fg: '252' }));
324
+ w++;
325
+ }
326
+ return cells;
327
+ };
328
+
329
+ // Top border
330
+ lines.push(makeBorder('\u250c', '\u252c', '\u2510', '\u2500'));
331
+
332
+ // Rows
333
+ for (let r = 0; r < parsedRows.length; r++) {
334
+ const row = parsedRows[r];
335
+ const isHeader = hasSeparator && r === 0;
336
+
337
+ // Build the row line cell-by-cell
338
+ const line = new Array(width).fill(null).map(() => createStyledCell(' ')) as Cell[];
339
+ let x = indent;
340
+
341
+ // Left border
342
+ if (x < width) line[x] = createStyledCell('\u2502', { fg: bc });
343
+ x++;
344
+
345
+ for (let c = 0; c < colCount; c++) {
346
+ // Space before content
347
+ if (x < width) line[x] = createStyledCell(' ');
348
+ x++;
349
+
350
+ // Cell content
351
+ const raw = c < row.length ? row[c] : '';
352
+ const cellContent = renderCellContent(raw, colWidths[c], isHeader);
353
+ for (const cell of cellContent) {
354
+ if (x < width) line[x] = cell;
355
+ x++;
356
+ }
357
+
358
+ // Space after content
359
+ if (x < width) line[x] = createStyledCell(' ');
360
+ x++;
361
+
362
+ // Column separator
363
+ if (x < width) line[x] = createStyledCell('\u2502', { fg: bc });
364
+ x++;
365
+ }
366
+
367
+ lines.push(line);
368
+
369
+ // Header separator
370
+ if (isHeader) {
371
+ lines.push(makeBorder('\u251c', '\u253c', '\u2524', '\u2500'));
372
+ }
373
+ }
374
+
375
+ // Bottom border
376
+ lines.push(makeBorder('\u2514', '\u2534', '\u2518', '\u2500'));
377
+
378
+ return lines;
379
+ }
380
+
381
+ /**
382
+ * Inline markdown token types.
383
+ */
384
+ type InlineToken =
385
+ | { type: 'text'; text: string; style: Partial<Cell> }
386
+ | { type: 'code'; text: string }
387
+ | { type: 'link'; text: string; url: string };
388
+
389
+ /**
390
+ * renderInlineMarkdown - Parse inline markdown (bold, italic, inline code, links)
391
+ * and return a flat array of tokens with style info.
392
+ */
393
+ export function renderInlineMarkdown(text: string): InlineToken[] {
394
+ const tokens: InlineToken[] = [];
395
+ let i = 0;
396
+
397
+ while (i < text.length) {
398
+ // Link: [text](url)
399
+ if (text[i] === '[') {
400
+ const closeB = text.indexOf(']', i);
401
+ if (closeB !== -1 && text[closeB + 1] === '(') {
402
+ const closeP = text.indexOf(')', closeB + 2);
403
+ if (closeP !== -1) {
404
+ const linkText = text.slice(i + 1, closeB);
405
+ const url = text.slice(closeB + 2, closeP);
406
+ tokens.push({ type: 'link', text: linkText, url });
407
+ i = closeP + 1;
408
+ continue;
409
+ }
410
+ }
411
+ }
412
+
413
+ // Inline code: `code`
414
+ if (text[i] === '`') {
415
+ const end = text.indexOf('`', i + 1);
416
+ if (end !== -1) {
417
+ tokens.push({ type: 'code', text: text.slice(i + 1, end) });
418
+ i = end + 1;
419
+ continue;
420
+ }
421
+ }
422
+
423
+ // Bold+italic: ***text***
424
+ if (text.slice(i, i + 3) === '***') {
425
+ const end = text.indexOf('***', i + 3);
426
+ if (end !== -1) {
427
+ tokens.push({ type: 'text', text: text.slice(i + 3, end), style: { bold: true, italic: true } });
428
+ i = end + 3;
429
+ continue;
430
+ }
431
+ // No closing *** found — emit the leading * as plain text so the ** bold
432
+ // check can handle the remaining ** on the next iteration.
433
+ tokens.push({ type: 'text', text: '*', style: {} });
434
+ i += 1;
435
+ continue;
436
+ }
437
+
438
+ // Bold: **text**
439
+ if (text.slice(i, i + 2) === '**') {
440
+ const end = text.indexOf('**', i + 2);
441
+ if (end !== -1) {
442
+ tokens.push({ type: 'text', text: text.slice(i + 2, end), style: { bold: true } });
443
+ i = end + 2;
444
+ continue;
445
+ }
446
+ }
447
+
448
+ // Italic: *text* or _text_
449
+ // Guard: text[i - 1] !== '*' prevents the second * of ** from starting italic.
450
+ // Guard: text[i + 1] !== text[i] prevents the first * of ** (or _ of __) from
451
+ // starting italic when bold/underscore-bold detection failed (e.g. unclosed **).
452
+ if (
453
+ (text[i] === '*' || text[i] === '_') &&
454
+ text[i - 1] !== text[i] &&
455
+ text[i + 1] !== text[i]
456
+ ) {
457
+ const closer = text[i];
458
+ const end = text.indexOf(closer, i + 1);
459
+ if (end !== -1 && end > i + 1) {
460
+ tokens.push({ type: 'text', text: text.slice(i + 1, end), style: { italic: true } });
461
+ i = end + 1;
462
+ continue;
463
+ }
464
+ }
465
+
466
+ // Strikethrough: ~~text~~
467
+ if (text.slice(i, i + 2) === '~~') {
468
+ const end = text.indexOf('~~', i + 2);
469
+ if (end !== -1) {
470
+ tokens.push({ type: 'text', text: text.slice(i + 2, end), style: { strikethrough: true, fg: '244' } });
471
+ i = end + 2;
472
+ continue;
473
+ }
474
+ }
475
+
476
+ // Plain text — accumulate until next special char, detect bare URLs and file paths
477
+ let end = i + 1;
478
+ while (end < text.length && !INLINE_SPECIAL_CHARS.has(text[end])) {
479
+ const code = text.charCodeAt(end);
480
+ end += (code >= 0xD800 && code <= 0xDBFF) ? 2 : 1;
481
+ }
482
+ const plainText = text.slice(i, end);
483
+
484
+ // Detect http/https URLs in plain text
485
+ const urlMatch = plainText.match(/^(https?:\/\/[^\s,)>"]+)/);
486
+ if (urlMatch) {
487
+ const url = urlMatch[1];
488
+ tokens.push({ type: 'link', text: url, url });
489
+ i += url.length;
490
+ continue;
491
+ }
492
+
493
+ // Detect absolute file paths in plain text (require at least one directory separator)
494
+ const fileMatch = plainText.match(/^(\/[^\s,)>"]+\/[^\s,)>"]+(?:\.[a-zA-Z0-9]+)?)/);
495
+ if (fileMatch) {
496
+ const filePath = fileMatch[1];
497
+ const fileUrl = `file://${filePath}`;
498
+ tokens.push({ type: 'link', text: filePath, url: fileUrl });
499
+ i += filePath.length;
500
+ continue;
501
+ }
502
+
503
+ tokens.push({ type: 'text', text: plainText, style: {} });
504
+ i = end;
505
+ }
506
+
507
+ return tokens;
508
+ }
509
+
510
+ /**
511
+ * compositeInlineLine - Convert a prefix + InlineTokens into Line[], applying word wrap.
512
+ * Builds cells directly from token styles.
513
+ */
514
+ function compositeInlineLine(
515
+ prefix: string,
516
+ tokens: InlineToken[],
517
+ width: number,
518
+ prefixStyle: Partial<Cell>,
519
+ textStartX: number
520
+ ): Line[] {
521
+ const lines: Line[] = [];
522
+
523
+ // Flatten tokens to [char, style] pairs
524
+ type StyledChar = { char: string; style: Partial<Cell> };
525
+ const chars: StyledChar[] = [];
526
+
527
+ for (const token of tokens) {
528
+ if (token.type === 'text') {
529
+ for (const ch of token.text) chars.push({ char: ch, style: token.style });
530
+ } else if (token.type === 'code') {
531
+ for (const ch of token.text) chars.push({ char: ch, style: { fg: '#ffcc00', bg: '#1a1a1a' } });
532
+ } else if (token.type === 'link') {
533
+ // Resolve URL: if url is empty or relative, treat as text; if it's a file path, use file:// protocol
534
+ let resolvedUrl = token.url;
535
+ if (resolvedUrl && !resolvedUrl.startsWith('http') && !resolvedUrl.startsWith('file://') && resolvedUrl.startsWith('/')) {
536
+ resolvedUrl = `file://${resolvedUrl}`;
537
+ }
538
+ for (const ch of token.text) chars.push({ char: ch, style: { fg: '#00aaff', underline: true, link: resolvedUrl || undefined } });
539
+ }
540
+ }
541
+
542
+ // Render with simple line-breaking at width
543
+ const availW = width - textStartX;
544
+ if (availW <= 0) return lines;
545
+
546
+ let lineChars: StyledChar[] = [];
547
+ let lineW = 0;
548
+
549
+ const flushLine = (isFirst: boolean) => {
550
+ const line = new Array(width).fill(null).map(() => createStyledCell(' ')) as Cell[];
551
+ // Write prefix on first line
552
+ if (isFirst) {
553
+ let px = 0;
554
+ for (const ch of prefix) {
555
+ if (px >= width) break;
556
+ const cw = getDisplayWidth(ch);
557
+ line[px] = createStyledCell(ch, { fg: prefixStyle.fg, bg: prefixStyle.bg, bold: prefixStyle.bold, dim: prefixStyle.dim, underline: prefixStyle.underline, italic: prefixStyle.italic, strikethrough: prefixStyle.strikethrough });
558
+ if (cw === 2 && px + 1 < width) line[px + 1] = { ...line[px], char: '' };
559
+ px += cw;
560
+ }
561
+ } else {
562
+ // indent-only for continuation lines
563
+ for (let x = 0; x < textStartX && x < width; x++) {
564
+ line[x] = createStyledCell(' ');
565
+ }
566
+ }
567
+ // Write content chars
568
+ let cx = textStartX;
569
+ for (const sc of lineChars) {
570
+ if (cx >= width) break;
571
+ const cw = getDisplayWidth(sc.char);
572
+ line[cx] = createStyledCell(sc.char, sc.style);
573
+ if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
574
+ cx += cw;
575
+ }
576
+ lines.push(line);
577
+ lineChars = [];
578
+ lineW = 0;
579
+ };
580
+
581
+ // Word-aware line breaking: accumulate words, break at spaces
582
+ let isFirstLine = true;
583
+ let wordChars: StyledChar[] = [];
584
+ let wordW = 0;
585
+
586
+ const flushWord = () => {
587
+ // If the word doesn't fit on the current line, wrap first
588
+ if (lineW > 0 && lineW + wordW > availW) {
589
+ flushLine(isFirstLine);
590
+ isFirstLine = false;
591
+ }
592
+ // If a single word is wider than availW, force-break it character by character
593
+ if (wordW > availW) {
594
+ for (const sc of wordChars) {
595
+ const cw = getDisplayWidth(sc.char);
596
+ if (lineW + cw > availW && lineW > 0) {
597
+ flushLine(isFirstLine);
598
+ isFirstLine = false;
599
+ }
600
+ lineChars.push(sc);
601
+ lineW += cw;
602
+ }
603
+ } else {
604
+ lineChars.push(...wordChars);
605
+ lineW += wordW;
606
+ }
607
+ wordChars = [];
608
+ wordW = 0;
609
+ };
610
+
611
+ for (const sc of chars) {
612
+ const cw = getDisplayWidth(sc.char);
613
+ if (sc.char === ' ') {
614
+ // Space: flush current word, then add the space
615
+ flushWord();
616
+ if (lineW + cw > availW && lineW > 0) {
617
+ flushLine(isFirstLine);
618
+ isFirstLine = false;
619
+ }
620
+ lineChars.push(sc);
621
+ lineW += cw;
622
+ } else {
623
+ // Non-space: accumulate into current word
624
+ wordChars.push(sc);
625
+ wordW += cw;
626
+ }
627
+ }
628
+ // Flush remaining word
629
+ if (wordChars.length > 0) flushWord();
630
+ if (lineChars.length > 0 || isFirstLine) {
631
+ flushLine(isFirstLine);
632
+ }
633
+
634
+ return lines;
635
+ }