@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,373 @@
1
+ import { type Line, type Cell, createStyledCell, createEmptyLine } from '../types/grid.ts';
2
+ import { UIFactory } from './ui-factory.ts';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
+ import { LAYOUT } from './layout.ts';
5
+ import { SyntaxHighlighter, type SyntaxToken as HLToken } from './syntax-highlighter.ts';
6
+
7
+ // ─── Language Keyword Maps ───────────────────────────────────────────────────
8
+
9
+ const TS_JS_KEYWORDS = new Set([
10
+ 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while',
11
+ 'do', 'switch', 'case', 'break', 'continue', 'class', 'extends', 'import',
12
+ 'export', 'default', 'from', 'new', 'this', 'super', 'typeof', 'instanceof',
13
+ 'in', 'of', 'try', 'catch', 'finally', 'throw', 'async', 'await', 'yield',
14
+ 'null', 'undefined', 'true', 'false', 'void', 'delete', 'interface', 'type',
15
+ 'enum', 'namespace', 'module', 'declare', 'abstract', 'implements', 'static',
16
+ 'readonly', 'public', 'private', 'protected', 'as', 'satisfies',
17
+ ]);
18
+ const TS_TYPES = new Set([
19
+ 'string', 'number', 'boolean', 'any', 'unknown', 'never', 'object', 'symbol',
20
+ 'bigint', 'void', 'Record', 'Array', 'Map', 'Set', 'Promise', 'Partial',
21
+ 'Required', 'Readonly', 'Pick', 'Omit', 'Exclude', 'Extract', 'NonNullable',
22
+ ]);
23
+
24
+ const PYTHON_KEYWORDS = new Set([
25
+ 'def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'import',
26
+ 'from', 'as', 'with', 'try', 'except', 'finally', 'raise', 'pass', 'break',
27
+ 'continue', 'and', 'or', 'not', 'in', 'is', 'lambda', 'yield', 'global',
28
+ 'nonlocal', 'del', 'assert', 'True', 'False', 'None', 'async', 'await',
29
+ ]);
30
+
31
+ const BASH_KEYWORDS = new Set([
32
+ 'if', 'then', 'else', 'elif', 'fi', 'for', 'do', 'done', 'while', 'case',
33
+ 'esac', 'function', 'return', 'exit', 'echo', 'export', 'local', 'readonly',
34
+ 'source', 'set', 'unset', 'shift', 'trap', 'exec', 'eval', 'read',
35
+ ]);
36
+
37
+ // ─── Language Detection ──────────────────────────────────────────────────────
38
+
39
+ function detectLanguage(lang: string): 'ts' | 'python' | 'bash' | 'json' | 'yaml' | 'html' | 'css' | 'unknown' {
40
+ const l = lang.toLowerCase();
41
+ if (l === 'ts' || l === 'tsx' || l === 'js' || l === 'jsx' || l === 'typescript' || l === 'javascript') return 'ts';
42
+ if (l === 'py' || l === 'python') return 'python';
43
+ if (l === 'sh' || l === 'bash' || l === 'shell' || l === 'zsh') return 'bash';
44
+ if (l === 'json') return 'json';
45
+ if (l === 'yaml' || l === 'yml') return 'yaml';
46
+ if (l === 'html' || l === 'htm' || l === 'xml') return 'html';
47
+ if (l === 'css' || l === 'scss' || l === 'less') return 'css';
48
+ return 'unknown';
49
+ }
50
+
51
+ // ─── Token Types ─────────────────────────────────────────────────────────────
52
+
53
+ type SyntaxToken = { text: string; fg: string; bold?: boolean; italic?: boolean };
54
+
55
+ // ─── Tokenizers ──────────────────────────────────────────────────────────────
56
+
57
+ function tokenizeTsJs(line: string): SyntaxToken[] {
58
+ const tokens: SyntaxToken[] = [];
59
+ let i = 0;
60
+
61
+ while (i < line.length) {
62
+ // Line comment
63
+ if (line.slice(i, i + 2) === '//') {
64
+ tokens.push({ text: line.slice(i), fg: '65', italic: true });
65
+ break;
66
+ }
67
+ // String (single, double, template)
68
+ if (line[i] === '"' || line[i] === "'" || line[i] === '`') {
69
+ const q = line[i];
70
+ let j = i + 1;
71
+ while (j < line.length && line[j] !== q) {
72
+ if (line[j] === '\\') j++;
73
+ j++;
74
+ }
75
+ tokens.push({ text: line.slice(i, j + 1), fg: '#ce9178' });
76
+ i = j + 1;
77
+ continue;
78
+ }
79
+ // Number
80
+ if (/[0-9]/.test(line[i])) {
81
+ let j = i;
82
+ while (j < line.length && /[0-9._xXbBoO]/.test(line[j])) j++;
83
+ tokens.push({ text: line.slice(i, j), fg: '#b5cea8' });
84
+ i = j;
85
+ continue;
86
+ }
87
+ // Identifier or keyword
88
+ if (/[a-zA-Z_$]/.test(line[i])) {
89
+ let j = i;
90
+ while (j < line.length && /[\w$]/.test(line[j])) j++;
91
+ const word = line.slice(i, j);
92
+ if (TS_JS_KEYWORDS.has(word)) {
93
+ tokens.push({ text: word, fg: '#569cd6', bold: true });
94
+ } else if (TS_TYPES.has(word)) {
95
+ tokens.push({ text: word, fg: '#4ec9b0' });
96
+ } else if (line[j] === '(') {
97
+ tokens.push({ text: word, fg: '#dcdcaa' });
98
+ } else {
99
+ tokens.push({ text: word, fg: '' });
100
+ }
101
+ i = j;
102
+ continue;
103
+ }
104
+ // Operators and punctuation
105
+ const ch = line[i];
106
+ const isOp = '=<>!&|+-*/%^~?:'.includes(ch);
107
+ tokens.push({ text: ch, fg: isOp ? '#d4d4d4' : '' });
108
+ i++;
109
+ }
110
+
111
+ return tokens;
112
+ }
113
+
114
+ function tokenizePython(line: string): SyntaxToken[] {
115
+ const tokens: SyntaxToken[] = [];
116
+ let i = 0;
117
+
118
+ while (i < line.length) {
119
+ if (line[i] === '#') {
120
+ tokens.push({ text: line.slice(i), fg: '65', italic: true });
121
+ break;
122
+ }
123
+ if (line[i] === '"' || line[i] === "'") {
124
+ const q = line[i];
125
+ let j = i + 1;
126
+ while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
127
+ tokens.push({ text: line.slice(i, j + 1), fg: '#ce9178' });
128
+ i = j + 1;
129
+ continue;
130
+ }
131
+ if (/[0-9]/.test(line[i])) {
132
+ let j = i;
133
+ while (j < line.length && /[0-9._]/.test(line[j])) j++;
134
+ tokens.push({ text: line.slice(i, j), fg: '#b5cea8' });
135
+ i = j;
136
+ continue;
137
+ }
138
+ if (/[a-zA-Z_]/.test(line[i])) {
139
+ let j = i;
140
+ while (j < line.length && /[\w]/.test(line[j])) j++;
141
+ const word = line.slice(i, j);
142
+ if (PYTHON_KEYWORDS.has(word)) {
143
+ tokens.push({ text: word, fg: '#569cd6', bold: true });
144
+ } else if (/^[A-Z]/.test(word)) {
145
+ tokens.push({ text: word, fg: '#4ec9b0' });
146
+ } else if (line[j] === '(') {
147
+ tokens.push({ text: word, fg: '#dcdcaa' });
148
+ } else {
149
+ tokens.push({ text: word, fg: '' });
150
+ }
151
+ i = j;
152
+ continue;
153
+ }
154
+ tokens.push({ text: line[i], fg: '' });
155
+ i++;
156
+ }
157
+ return tokens;
158
+ }
159
+
160
+ function tokenizeBash(line: string): SyntaxToken[] {
161
+ const tokens: SyntaxToken[] = [];
162
+ let i = 0;
163
+
164
+ while (i < line.length) {
165
+ if (line[i] === '#') {
166
+ tokens.push({ text: line.slice(i), fg: '65', italic: true });
167
+ break;
168
+ }
169
+ if (line[i] === '"' || line[i] === "'") {
170
+ const q = line[i];
171
+ let j = i + 1;
172
+ while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
173
+ tokens.push({ text: line.slice(i, j + 1), fg: '#ce9178' });
174
+ i = j + 1;
175
+ continue;
176
+ }
177
+ if (line[i] === '$') {
178
+ let j = i + 1;
179
+ while (j < line.length && /[\w{}_]/.test(line[j])) j++;
180
+ tokens.push({ text: line.slice(i, j), fg: '#9cdcfe' });
181
+ i = j;
182
+ continue;
183
+ }
184
+ if (/[a-zA-Z_]/.test(line[i])) {
185
+ let j = i;
186
+ while (j < line.length && /[\w-]/.test(line[j])) j++;
187
+ const word = line.slice(i, j);
188
+ if (BASH_KEYWORDS.has(word)) {
189
+ tokens.push({ text: word, fg: '#569cd6', bold: true });
190
+ } else {
191
+ tokens.push({ text: word, fg: '' });
192
+ }
193
+ i = j;
194
+ continue;
195
+ }
196
+ tokens.push({ text: line[i], fg: '' });
197
+ i++;
198
+ }
199
+ return tokens;
200
+ }
201
+
202
+ function tokenizeJson(line: string): SyntaxToken[] {
203
+ const tokens: SyntaxToken[] = [];
204
+ let i = 0;
205
+
206
+ while (i < line.length) {
207
+ if (line[i] === '"') {
208
+ let j = i + 1;
209
+ while (j < line.length && line[j] !== '"') { if (line[j] === '\\') j++; j++; }
210
+ const str = line.slice(i, j + 1);
211
+ // JSON key: followed by :
212
+ const rest = line.slice(j + 1).trimStart();
213
+ if (rest.startsWith(':')) {
214
+ tokens.push({ text: str, fg: '#9cdcfe' });
215
+ } else {
216
+ tokens.push({ text: str, fg: '#ce9178' });
217
+ }
218
+ i = j + 1;
219
+ continue;
220
+ }
221
+ if (/[0-9-]/.test(line[i])) {
222
+ let j = i;
223
+ while (j < line.length && /[0-9.eE+-]/.test(line[j])) j++;
224
+ tokens.push({ text: line.slice(i, j), fg: '#b5cea8' });
225
+ i = j;
226
+ continue;
227
+ }
228
+ const boolNull = ['true', 'false', 'null'].find(k => line.startsWith(k, i));
229
+ if (boolNull) {
230
+ tokens.push({ text: boolNull, fg: '#569cd6', bold: true });
231
+ i += boolNull.length;
232
+ continue;
233
+ }
234
+ tokens.push({ text: line[i], fg: '244' });
235
+ i++;
236
+ }
237
+ return tokens;
238
+ }
239
+
240
+ function tokenizeYaml(line: string): SyntaxToken[] {
241
+ const tokens: SyntaxToken[] = [];
242
+ if (line.trimStart().startsWith('#')) {
243
+ return [{ text: line, fg: '65', italic: true }];
244
+ }
245
+ const keyMatch = line.match(/^(\s*)([^:]+)(:)(\s*.*)/);
246
+ if (keyMatch) {
247
+ if (keyMatch[1]) tokens.push({ text: keyMatch[1], fg: '' });
248
+ tokens.push({ text: keyMatch[2], fg: '#9cdcfe' });
249
+ tokens.push({ text: keyMatch[3], fg: '244' });
250
+ if (keyMatch[4]) {
251
+ const val = keyMatch[4];
252
+ const trimVal = val.trimStart();
253
+ // Differentiate YAML value types for syntax highlighting
254
+ const isStr = /^['"]/.test(trimVal);
255
+ const isBool = trimVal === 'true' || trimVal === 'false' || trimVal === 'null' || trimVal === 'yes' || trimVal === 'no';
256
+ const isNum = /^-?[0-9]/.test(trimVal);
257
+ const valFg = isStr ? '#ce9178' : isBool ? '#569cd6' : isNum ? '#b5cea8' : '';
258
+ tokens.push({ text: val, fg: valFg });
259
+ }
260
+ return tokens;
261
+ }
262
+ return [{ text: line, fg: '' }];
263
+ }
264
+
265
+ function tokenizePlain(line: string): SyntaxToken[] {
266
+ return [{ text: line, fg: '' }];
267
+ }
268
+
269
+ // ─── Main Renderer ───────────────────────────────────────────────────────────
270
+
271
+ /**
272
+ * renderCodeBlock - Render lines of code with syntax highlighting and line numbers.
273
+ * Returns Line[] for the cell-based pipeline.
274
+ */
275
+ export function renderCodeBlock(
276
+ codeLines: string[],
277
+ lang: string,
278
+ width: number,
279
+ opts: { showLineNumbers?: boolean } = {},
280
+ ): Line[] {
281
+ const lines: Line[] = [];
282
+ const language = detectLanguage(lang);
283
+ const leftMargin = LAYOUT.LEFT_MARGIN;
284
+ const showLineNumbers = opts.showLineNumbers ?? true;
285
+ const lineNumW = showLineNumbers ? String(codeLines.length).length + 1 : 0; // e.g. "10 "
286
+ const contentStartX = showLineNumbers ? leftMargin + lineNumW + 1 : leftMargin;
287
+ const BG = '#0d0d0d';
288
+ const LINE_NUM_FG = '238';
289
+ const effectiveWidth = width - LAYOUT.RIGHT_MARGIN;
290
+
291
+ // Try tree-sitter highlight cache first (populated asynchronously).
292
+ // Falls back to regex tokenizer when parser not yet ready or language unsupported.
293
+ const fullCode = codeLines.join('\n');
294
+ const hlLines = lang ? new SyntaxHighlighter().highlight(fullCode, lang) : null;
295
+
296
+ // Regex tokenizer fallback (used when tree-sitter not ready)
297
+ const regexTokenize = (line: string): SyntaxToken[] => {
298
+ switch (language) {
299
+ case 'ts': return tokenizeTsJs(line);
300
+ case 'python': return tokenizePython(line);
301
+ case 'bash': return tokenizeBash(line);
302
+ case 'json': return tokenizeJson(line);
303
+ case 'yaml': return tokenizeYaml(line);
304
+ default: return tokenizePlain(line);
305
+ }
306
+ };
307
+
308
+ // Header bar: language label
309
+ const langLabel = lang ? ` ${lang} ` : ' code ';
310
+ const headerLine = createEmptyLine(width);
311
+ const headerStr = langLabel.padEnd(effectiveWidth - leftMargin);
312
+ let hx = leftMargin;
313
+ for (const ch of headerStr) {
314
+ if (hx >= effectiveWidth) break;
315
+ headerLine[hx] = createStyledCell(ch, { fg: '#1a1a1a', bg: '#4ec9b0', bold: true });
316
+ hx++;
317
+ }
318
+ lines.push(headerLine);
319
+
320
+ // Code lines
321
+ for (let i = 0; i < codeLines.length; i++) {
322
+ const rawLine = codeLines[i];
323
+ const lineNum = String(i + 1).padStart(lineNumW);
324
+
325
+ // Select token source: tree-sitter (accurate) or regex (fallback)
326
+ const tokens: SyntaxToken[] =
327
+ hlLines && i < hlLines.length && hlLines[i].length > 0
328
+ ? (hlLines[i] as HLToken[])
329
+ : regexTokenize(rawLine);
330
+
331
+ const line: Cell[] = createEmptyLine(width);
332
+ // Paint only the code-block body band so body rows match the header/footer width.
333
+ for (let x = leftMargin; x < effectiveWidth; x++) {
334
+ line[x] = createStyledCell(' ', { bg: BG });
335
+ }
336
+
337
+ let cx = leftMargin;
338
+ if (showLineNumbers) {
339
+ for (const ch of lineNum) {
340
+ if (cx >= contentStartX) break;
341
+ line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg: BG, dim: true });
342
+ }
343
+ line[cx++] = createStyledCell(' ', { bg: BG });
344
+ }
345
+
346
+ // Syntax tokens
347
+ for (const token of tokens) {
348
+ for (const ch of token.text) {
349
+ if (cx >= effectiveWidth) break;
350
+ const cw = getDisplayWidth(ch);
351
+ const code = ch.charCodeAt(0);
352
+ if (code < 32 || code === 127) {
353
+ cx++;
354
+ continue;
355
+ }
356
+ line[cx] = createStyledCell(ch, { fg: token.fg, bg: BG, bold: token.bold, italic: token.italic });
357
+ if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
358
+ cx += cw;
359
+ }
360
+ }
361
+
362
+ lines.push(line);
363
+ }
364
+
365
+ // Footer line
366
+ const footerLine = createEmptyLine(width);
367
+ for (let fx = leftMargin; fx < effectiveWidth; fx++) {
368
+ footerLine[fx] = createStyledCell(' ', { bg: '#0d0d0d' });
369
+ }
370
+ lines.push(footerLine);
371
+
372
+ return lines;
373
+ }
@@ -0,0 +1,283 @@
1
+ import { TerminalBuffer } from './buffer.ts';
2
+ import { DiffEngine } from './diff.ts';
3
+ import { type Line, createEmptyCell, createStyledCell } from '../types/grid.ts';
4
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
5
+ import type { SearchManager } from '../input/search.ts';
6
+ import { allowTerminalWrite } from '../runtime/terminal-output-guard.ts';
7
+
8
+ export interface SelectionInfo {
9
+ isCellSelected: (col: number, absoluteRow: number) => boolean;
10
+ scrollTop: number;
11
+ lineCount: number;
12
+ }
13
+
14
+ export interface SearchInfo {
15
+ manager: SearchManager;
16
+ scrollTop: number;
17
+ viewportStartY: number;
18
+ }
19
+
20
+ export interface PanelCompositeData {
21
+ /** Workspace-level tab bar spanning all open panels. */
22
+ workspaceBar: Line;
23
+ /** Top pane: tab bar */
24
+ topTabBar?: Line;
25
+ /** Top pane: panel content lines */
26
+ topContent: Line[];
27
+ /** Whether the top pane is focused (affects separator color) */
28
+ topFocused: boolean;
29
+ /** Bottom pane tab bar. Undefined = no bottom pane. */
30
+ bottomTabBar?: Line;
31
+ /** Bottom pane content lines. Undefined = no bottom pane. */
32
+ bottomContent?: Line[];
33
+ /** Whether the bottom pane is focused */
34
+ bottomFocused?: boolean;
35
+ /** Separator between left and right panel area */
36
+ separator: boolean;
37
+ /** Ratio of panel height for the top pane (0–1). Only used when bottom pane is present. */
38
+ verticalSplitRatio: number;
39
+ }
40
+
41
+ export interface CompositeRequest {
42
+ width: number;
43
+ height: number;
44
+ header: Line[];
45
+ viewport: Line[];
46
+ footer: Line[];
47
+ selection?: SelectionInfo;
48
+ search?: SearchInfo;
49
+ panel?: PanelCompositeData;
50
+ panelWidth?: number; // width of the right panel area (0 = no panel)
51
+ }
52
+
53
+ /**
54
+ * Compositor - Authoritative TUI layout engine with Selection Overlay.
55
+ * Decoupled from global state — all needed data is passed as parameters.
56
+ */
57
+ export class Compositor {
58
+ /** Double-buffer reuse: back is written, front is the last-rendered reference. */
59
+ private frontBuffer: TerminalBuffer | null = null;
60
+ private backBuffer: TerminalBuffer | null = null;
61
+ private diffEngine = new DiffEngine();
62
+
63
+ constructor(private stdout: NodeJS.WriteStream) {}
64
+
65
+ /** Exposed for unit tests — returns the last composited buffer. */
66
+ public get lastBufferForTest(): TerminalBuffer | null {
67
+ return this.frontBuffer;
68
+ }
69
+
70
+ public resetDiff(): void {
71
+ this.diffEngine.reset();
72
+ this.frontBuffer = null;
73
+ this.backBuffer = null;
74
+ }
75
+
76
+ public composite(params: CompositeRequest): void {
77
+ const { width, height, header, viewport, footer, selection, search, panel, panelWidth } = params;
78
+ // R3: Reuse back-buffer instead of allocating each frame
79
+ if (!this.backBuffer) {
80
+ this.backBuffer = new TerminalBuffer(width, height);
81
+ } else {
82
+ this.backBuffer.reset(width, height, this.frontBuffer);
83
+ }
84
+ const newBuffer = this.backBuffer;
85
+
86
+ const hasPanel = panel !== undefined && panelWidth !== undefined && panelWidth > 0;
87
+ const leftWidth = hasPanel ? Math.max(1, width - panelWidth - 1) : width;
88
+ const sepX = hasPanel ? leftWidth : -1;
89
+
90
+ // 1. Draw Header — always full width
91
+ header.forEach((line, i) => newBuffer.blitLine(i, line));
92
+
93
+ // 2. Draw Viewport directly after the supplied header.
94
+ const viewportStartY = header.length;
95
+ const vHeight = Math.max(0, height - header.length - footer.length);
96
+
97
+ // Calculate the offset for bottom-anchored short history
98
+ const lineCount = selection?.lineCount ?? 0;
99
+ const offset = Math.max(0, vHeight - lineCount);
100
+
101
+ // --- Pre-compute panel row layout when split pane is active ---
102
+ // When both top and bottom panes are visible, the panel area is split:
103
+ // row 0: workspace tab bar
104
+ // row 1: top tab bar
105
+ // rows 2..topH+1: top content
106
+ // row topH+2: horizontal separator (───)
107
+ // row topH+3: bottom tab bar
108
+ // rows topH+4..end: bottom content
109
+ const hasBottomPane = hasPanel && panel!.bottomTabBar !== undefined;
110
+ let topPaneHeight = 0; // number of content rows in top pane
111
+ let bottomPaneHeight = 0;
112
+ let hSepRow = -1; // viewport row of the horizontal separator
113
+ if (hasPanel && hasBottomPane) {
114
+ const panelAreaRows = Math.max(0, vHeight - 1); // subtract workspace tab bar
115
+ // top: 1 (tabbar) + topContent rows; bottom: 1 (sep) + 1 (tabbar) + bottomContent
116
+ const contentRows = Math.max(0, panelAreaRows - 3); // subtract top-tabbar + h-sep + bottom-tabbar
117
+ topPaneHeight = Math.max(1, Math.floor(contentRows * panel!.verticalSplitRatio));
118
+ bottomPaneHeight = Math.max(1, contentRows - topPaneHeight);
119
+ hSepRow = 2 + topPaneHeight; // workspace bar + top tab bar + top content rows
120
+ }
121
+
122
+ const sepFg = hasPanel && panel!.separator
123
+ ? (panel!.topFocused || panel!.bottomFocused ? '244' : '238')
124
+ : '238';
125
+
126
+ viewport.forEach((line, i) => {
127
+ const screenY = viewportStartY + i;
128
+ if (screenY >= height) return;
129
+
130
+ if (!hasPanel) {
131
+ // No panel: existing fast path
132
+ newBuffer.blitLine(screenY, line);
133
+ } else {
134
+ // Panel active: write cells individually to support split layout
135
+ // Left side: viewport cells 0..leftWidth-1
136
+ for (let x = 0; x < leftWidth; x++) {
137
+ const cell = line[x];
138
+ if (cell !== undefined) {
139
+ // If this is a wide char (2-cell) at the last left-side column,
140
+ // it would bleed into the separator column visually.
141
+ // Replace with a space to keep the separator aligned.
142
+ if (x === leftWidth - 1 && cell.char && cell.char.length > 0 && getDisplayWidth(cell.char) > 1) {
143
+ newBuffer.setCell(x, screenY, { ...cell, char: ' ' });
144
+ continue;
145
+ }
146
+ newBuffer.setCell(x, screenY, cell);
147
+ }
148
+ }
149
+
150
+ const p = panel!;
151
+
152
+ // Separator column (vertical bar between left and panel area)
153
+ if (p.separator) {
154
+ newBuffer.setCell(sepX, screenY, createStyledCell('│', { fg: sepFg }));
155
+ }
156
+
157
+ const panelStartX = sepX + 1;
158
+ const clearPanelRemainder = (fromX = 0) => {
159
+ for (let x = Math.max(0, fromX); x < panelWidth; x++) {
160
+ newBuffer.setCell(panelStartX + x, screenY, createEmptyCell());
161
+ }
162
+ };
163
+ const drawPanelLine = (panelLine: Line | undefined) => {
164
+ if (panelLine === undefined) {
165
+ clearPanelRemainder();
166
+ return;
167
+ }
168
+ const limit = Math.min(panelLine.length, panelWidth);
169
+ for (let x = 0; x < limit; x++) {
170
+ const cell = panelLine[x];
171
+ if (cell !== undefined) {
172
+ newBuffer.setCell(panelStartX + x, screenY, cell);
173
+ }
174
+ }
175
+ clearPanelRemainder(limit);
176
+ };
177
+
178
+ if (!hasBottomPane) {
179
+ // --- Single pane mode ---
180
+ // viewport row 0 → workspace bar, viewport rows 1+ → panel content
181
+ const panelLine = i === 0 ? p.workspaceBar : p.topContent[i - 1];
182
+ drawPanelLine(panelLine);
183
+ } else {
184
+ // --- Two pane mode ---
185
+ // Row layout (by viewport row i):
186
+ // i = 0: workspace tab bar
187
+ // i = 1: top tab bar
188
+ // 2 <= i <= topPaneHeight+1: top content[i-2]
189
+ // i = hSepRow: horizontal separator
190
+ // i = hSepRow+1: bottom tab bar
191
+ // i >= hSepRow+2: bottom content[i - (hSepRow+2)]
192
+ let panelLine: Line | undefined;
193
+
194
+ if (i === 0) {
195
+ panelLine = p.workspaceBar;
196
+ } else if (i === 1) {
197
+ panelLine = p.topTabBar;
198
+ } else if (i <= topPaneHeight + 1) {
199
+ panelLine = p.topContent[i - 2];
200
+ } else if (i === hSepRow) {
201
+ // Horizontal separator between the two panes
202
+ // Render ─ chars across the panel width
203
+ const focusFg = p.bottomFocused ? '36' : '238'; // cyan if bottom pane focused
204
+ for (let x = 0; x < panelWidth; x++) {
205
+ newBuffer.setCell(panelStartX + x, screenY, createStyledCell('─', { fg: focusFg }));
206
+ }
207
+ // Also update the separator column char to T-junction (├):
208
+ // ├ connects the vertical left-separator with the horizontal pane divider,
209
+ // forming a clean T-shaped joint at the split point.
210
+ if (p.separator) {
211
+ newBuffer.setCell(sepX, screenY, createStyledCell('├', { fg: focusFg }));
212
+ }
213
+ } else if (i === hSepRow + 1) {
214
+ panelLine = p.bottomTabBar;
215
+ } else {
216
+ panelLine = p.bottomContent?.[i - (hSepRow + 2)];
217
+ }
218
+
219
+ if (i !== hSepRow) {
220
+ drawPanelLine(panelLine);
221
+ }
222
+ }
223
+ }
224
+
225
+ // Apply Selection Highlighting Overlay (left side only)
226
+ // Only highlight rows that actually contain history (past the bottom-anchor offset)
227
+ if (selection && i >= offset) {
228
+ const absoluteRow = selection.scrollTop + (i - offset);
229
+ for (let x = 0; x < leftWidth; x++) {
230
+ if (selection.isCellSelected(x, absoluteRow)) {
231
+ newBuffer.setCell(x, screenY, { bg: '4', fg: '0', bold: false, dim: false });
232
+ }
233
+ }
234
+ }
235
+
236
+ // Apply Search Match Highlighting Overlay (left side only)
237
+ if (search && search.manager.active && search.manager.query.length > 0 && i >= offset) {
238
+ const absoluteRow = search.scrollTop + (i - offset);
239
+ const lineMatches = search.manager.getMatchesOnLine(absoluteRow);
240
+ for (const match of lineMatches) {
241
+ const isCurrent = search.manager.isCurrentMatch(absoluteRow, match.col);
242
+ for (let x = match.col; x < match.col + match.length && x < leftWidth; x++) {
243
+ if (isCurrent) {
244
+ newBuffer.setCell(x, screenY, { bg: '#ffff00', fg: '#000000', bold: true, dim: false });
245
+ } else {
246
+ newBuffer.setCell(x, screenY, { bg: '#806600', fg: '#ffffff', bold: false, dim: false });
247
+ }
248
+ }
249
+ }
250
+ }
251
+ });
252
+
253
+ // Draw separator on remaining viewport rows past content (when panel is active)
254
+ if (hasPanel && panel!.separator) {
255
+ for (let i = viewport.length; i < vHeight; i++) {
256
+ const screenY = viewportStartY + i;
257
+ if (screenY >= height) break;
258
+ newBuffer.setCell(sepX, screenY, createStyledCell('│', { fg: sepFg }));
259
+ }
260
+ }
261
+
262
+ // 3. Draw Footer (Pinned to Bottom) — always full width
263
+ const footerStart = height - footer.length;
264
+ footer.forEach((line, i) => {
265
+ const screenY = footerStart + i;
266
+ if (screenY >= height) return;
267
+ newBuffer.blitLine(screenY, line);
268
+ });
269
+
270
+ // 4. Diff and Render
271
+ // R3: Diff against front-buffer (last-rendered), then swap front/back — no clone() needed
272
+ const diff = this.diffEngine.diff(this.frontBuffer, newBuffer);
273
+ if (diff) {
274
+ allowTerminalWrite(() => this.stdout.write(diff));
275
+ }
276
+
277
+ // Swap: back (just written) becomes the new front reference; old front becomes the next back
278
+ const swap = this.frontBuffer;
279
+ this.frontBuffer = this.backBuffer;
280
+ this.frontBuffer.clearDirty();
281
+ this.backBuffer = swap;
282
+ }
283
+ }