@pellux/goodvibes-tui 2.0.21 → 2.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 (388) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +16 -6
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -0,0 +1,537 @@
1
+ /**
2
+ * lane-graph/paint.ts, drawing the work tree's rows into transcript lines.
3
+ *
4
+ * Columns (screen width W, gutter starting at GUTTER_X):
5
+ * - the gutter from GUTTER_X, 2 columns per lane (layout.ts)
6
+ * - ▸ / ▾ two columns left of the text column when a bead has a body
7
+ * - name and key argument from the text column
8
+ * - the result summary right-aligned so it ends 9 columns left of the time
9
+ * - the time right-aligned so it ends at column W-4
10
+ *
11
+ * An opened body is a fill from the text column to W-4 with 2 columns of side
12
+ * padding and a padding row above and below; the lanes keep running down the
13
+ * gutter beside it. The focused row gets a fill from column 1 to W-2 and the
14
+ * brand ┃ in column 0.
15
+ */
16
+
17
+ import { createEmptyLine, createStyledCell, type Line } from '@pellux/goodvibes-sdk/platform/types';
18
+ import { activeTokens } from '../theme.ts';
19
+ import { expandTabs, getDisplayWidth, truncateDisplay, wrapPreservingIndent } from '../../utils/terminal-width.ts';
20
+ import { SurfaceCanvas } from '../surface-kit.ts';
21
+ import { inset, type KitInset } from '../surface-kit-parts.ts';
22
+ import { buildDiffRows, drawDiffRow, semanticChips, type DiffRow } from '../changes-modal.ts';
23
+ import { highlightCodeLines } from '../code-block.ts';
24
+ import { languageForPath, parseChanges } from '../../input/changes-model.ts';
25
+ import type { SemanticDiff } from '../semantic-diff.ts';
26
+ import { glyphText, type LaneGlyphs } from './glyphs.ts';
27
+ import type { GutterRow } from './layout.ts';
28
+ import type { BeadBody, BeadStatus, BeadSummary, BodyTone, ReadBodyFile, SummaryTone } from './bead.ts';
29
+
30
+ /** Column where every turn's gutter starts (one right of the ┃ bar column). */
31
+ const GUTTER_X = 3;
32
+ /** Columns between the end of the summary and the end of the time. */
33
+ const SUMMARY_GAP = 9;
34
+ /** Rows an output, list or text body shows before "… N more". */
35
+ const BODY_CAP = 12;
36
+ /** Rows a diff body shows before "… N more lines". */
37
+ const DIFF_CAP = 40;
38
+
39
+ export interface GraphPaint {
40
+ readonly width: number;
41
+ /** Columns from GUTTER_X to the turn's text column (layout.gutterWidth). */
42
+ readonly gutterWidth: number;
43
+ readonly glyphs: LaneGlyphs;
44
+ /** Spinner frame for running beads. */
45
+ readonly frame: number;
46
+ }
47
+
48
+ /** The turn's text column. */
49
+ export function textColumn(p: Pick<GraphPaint, 'gutterWidth'>): number {
50
+ return GUTTER_X + p.gutterWidth;
51
+ }
52
+
53
+ /** Column the time's last character sits on. */
54
+ function timeEnd(width: number): number {
55
+ return width - 4;
56
+ }
57
+
58
+ /** Added to every lane index while an agent view draws (its spine takes the agent's color). */
59
+ let laneColorBase = 0;
60
+
61
+ /** A lane's color: the theme's `lanes` tokens in rotation. */
62
+ export function laneColor(index: number): string {
63
+ const lanes = activeTokens().lanes;
64
+ if (lanes.length === 0) return activeTokens().brand;
65
+ const at = index + laneColorBase;
66
+ return lanes[((at % lanes.length) + lanes.length) % lanes.length]!;
67
+ }
68
+
69
+ /**
70
+ * Draw with every lane shifted by `base`: an agent opened full screen draws
71
+ * its own spine in the color its lane has in main, and its children in the
72
+ * colors after it. Synchronous; the base is restored afterwards.
73
+ */
74
+ export function withLaneColorBase<T>(base: number, draw: () => T): T {
75
+ const previous = laneColorBase;
76
+ laneColorBase = base;
77
+ try {
78
+ return draw();
79
+ } finally {
80
+ laneColorBase = previous;
81
+ }
82
+ }
83
+
84
+ function summaryColor(tone: SummaryTone): string {
85
+ const t = activeTokens();
86
+ return tone === 'good' ? t.success : tone === 'warn' ? t.warning : tone === 'bad' ? t.error : t.textFaint;
87
+ }
88
+
89
+ function bodyColor(tone: BodyTone): string {
90
+ const t = activeTokens();
91
+ switch (tone) {
92
+ case 'good': return t.success;
93
+ case 'bad': return t.error;
94
+ case 'warn': return t.warning;
95
+ case 'muted': return t.textMuted;
96
+ case 'faint': return t.textFaint;
97
+ default: return t.text;
98
+ }
99
+ }
100
+
101
+ interface TextStyle {
102
+ readonly fg: string;
103
+ readonly bold?: boolean;
104
+ readonly strikethrough?: boolean;
105
+ readonly bg?: string;
106
+ }
107
+
108
+ /** Row text in the active glyph set (plain ASCII for the ascii set). */
109
+ function tx(p: Pick<GraphPaint, 'glyphs'>, text: string): string {
110
+ return glyphText(text, p.glyphs);
111
+ }
112
+
113
+ /** Write text from `x`, never past `endExclusive`; returns the column after it. */
114
+ function putText(line: Line, x: number, endExclusive: number, text: string, style: TextStyle): number {
115
+ let col = x;
116
+ for (const ch of text) {
117
+ const w = getDisplayWidth(ch);
118
+ if (w <= 0) continue;
119
+ if (col + w > endExclusive || col + w > line.length) break;
120
+ const prev = line[col]!;
121
+ line[col] = createStyledCell(ch, { fg: style.fg, bg: style.bg ?? prev.bg, bold: style.bold ?? false, strikethrough: style.strikethrough ?? false });
122
+ if (w > 1) line[col + 1] = createStyledCell('', { fg: style.fg, bg: style.bg ?? prev.bg });
123
+ col += w;
124
+ }
125
+ return col;
126
+ }
127
+
128
+ /** Write text so its last column is `xr`; returns its first column. */
129
+ function putRight(line: Line, xr: number, text: string, style: TextStyle): number {
130
+ const start = xr - getDisplayWidth(text) + 1;
131
+ putText(line, start, xr + 1, text, style);
132
+ return start;
133
+ }
134
+
135
+ /** The character a bead's status draws as, and its color. */
136
+ function beadMark(status: BeadStatus, glyphs: LaneGlyphs, lane: string, frame: number): { char: string; fg: string } {
137
+ const t = activeTokens();
138
+ switch (status) {
139
+ case 'ok': return { char: glyphs.ok, fg: t.success };
140
+ case 'warn': return { char: glyphs.warn, fg: t.warning };
141
+ case 'err': return { char: glyphs.err, fg: t.error };
142
+ case 'run': return { char: glyphs.run[frame % glyphs.run.length]!, fg: lane };
143
+ case 'wait': return { char: glyphs.wait, fg: t.warning };
144
+ case 'cancel': return { char: glyphs.cancel, fg: t.textFaint };
145
+ case 'bg': return { char: glyphs.background, fg: t.success };
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Draw a gutter row onto `line` from GUTTER_X. The bead cell (if any) draws
151
+ * as `beadStatus`'s mark; lane lines keep their lane's color.
152
+ */
153
+ export function paintGutter(line: Line, row: GutterRow, p: GraphPaint, beadStatus?: BeadStatus): void {
154
+ for (let col = 0; col < row.length; col++) {
155
+ const cell = row[col];
156
+ const x = GUTTER_X + col;
157
+ if (!cell || x >= line.length) continue;
158
+ const lane = laneColor(cell.color);
159
+ const bg = line[x]!.bg;
160
+ if (cell.role === 'bead') {
161
+ const mark = beadMark(beadStatus ?? 'ok', p.glyphs, lane, p.frame);
162
+ line[x] = createStyledCell(mark.char, { fg: mark.fg, bg, bold: true });
163
+ continue;
164
+ }
165
+ const isMark = cell.role === 'folded' || cell.role === 'turn';
166
+ line[x] = createStyledCell(p.glyphs.role[cell.role], { fg: lane, bg, bold: isMark });
167
+ }
168
+ }
169
+
170
+ /** The fill and ┃ of the focused row (and of every row of its opened body's frame). */
171
+ export function applyFocus(line: Line, width: number, glyphs: LaneGlyphs): void {
172
+ const t = activeTokens();
173
+ for (let x = 1; x <= width - 2 && x < line.length; x++) {
174
+ const cell = line[x]!;
175
+ if (cell.bg === '') line[x] = { ...cell, bg: t.backgroundPanel };
176
+ }
177
+ if (line.length > 0) line[0] = createStyledCell(glyphs.focus, { fg: t.brand, bold: true });
178
+ }
179
+
180
+ /** Right side of a row: the summary ending SUMMARY_GAP columns left of the time, and the time. Returns the column the left text must stop before. */
181
+ function paintRight(line: Line, p: GraphPaint, summary: BeadSummary | null, summaryMark: string | undefined, time: string | undefined, timeFg: string): number {
182
+ const end = timeEnd(p.width);
183
+ let limit = end + 1;
184
+ if (time) limit = putRight(line, end, tx(p, time), { fg: timeFg }) - 2;
185
+ if (summary) {
186
+ const sumEnd = end - SUMMARY_GAP;
187
+ const room = Math.max(0, sumEnd - textColumn(p) - 12);
188
+ const text = tx(p, truncateDisplay(summaryMark ? `${summaryMark} ${summary.text}` : summary.text, Math.max(8, room), p.glyphs.ellipsis));
189
+ limit = Math.min(limit, putRight(line, sumEnd, text, { fg: summaryColor(summary.tone) }) - 2);
190
+ }
191
+ return limit;
192
+ }
193
+
194
+ export interface BeadRowSpec {
195
+ readonly status: BeadStatus;
196
+ readonly name: string;
197
+ readonly arg: string;
198
+ readonly summary: BeadSummary | null;
199
+ readonly time: string | undefined;
200
+ /** The lane's color (running beads spin and count up in it). */
201
+ readonly laneColor: string;
202
+ readonly hasBody: boolean;
203
+ readonly open: boolean;
204
+ /** WRFC: the finding this row answers ("↳ jitter can overshoot"). */
205
+ readonly answers?: string | undefined;
206
+ }
207
+
208
+ export function paintBeadRow(gutter: GutterRow, spec: BeadRowSpec, p: GraphPaint): Line {
209
+ const t = activeTokens();
210
+ const line = createEmptyLine(p.width);
211
+ paintGutter(line, gutter, p, spec.status);
212
+ const x = textColumn(p);
213
+ if (spec.hasBody) putText(line, x - 2, x - 1, spec.open ? p.glyphs.opened : p.glyphs.closed, { fg: spec.open ? t.textMuted : t.textFaint });
214
+ const limit = paintRight(line, p, spec.summary, undefined, spec.time, spec.status === 'run' ? spec.laneColor : t.textFaint);
215
+ const cancelled = spec.status === 'cancel';
216
+ const nameFg = spec.status === 'run' || spec.status === 'wait' ? t.text : spec.status === 'err' ? t.error : t.textMuted;
217
+ let cx = putText(line, x, limit, tx(p, spec.name), { fg: nameFg, bold: spec.status === 'run', strikethrough: cancelled });
218
+ if (spec.arg) {
219
+ const room = limit - (cx + 1);
220
+ const answers = spec.answers ? ` ${p.glyphs.answers} ${spec.answers}` : '';
221
+ const argRoom = Math.max(0, room - Math.min(getDisplayWidth(answers), Math.floor(room / 2)));
222
+ cx = putText(line, cx + 1, limit, tx(p, truncateDisplay(spec.arg, argRoom, p.glyphs.ellipsis)), { fg: cancelled ? t.textFaint : t.text, strikethrough: cancelled });
223
+ if (answers) putText(line, cx, limit, tx(p, truncateDisplay(answers, Math.max(0, limit - cx), p.glyphs.ellipsis)), { fg: t.warning });
224
+ }
225
+ return line;
226
+ }
227
+
228
+ export interface LaneRowSpec {
229
+ readonly name: string;
230
+ readonly arg: string;
231
+ readonly laneColor: string;
232
+ }
233
+
234
+ /** A lane opening (├─╮): the agent's name in its lane color, its task muted. */
235
+ export function paintSpawnRow(gutter: GutterRow, spec: LaneRowSpec, p: GraphPaint): Line {
236
+ const line = createEmptyLine(p.width);
237
+ paintGutter(line, gutter, p);
238
+ const x = textColumn(p);
239
+ const limit = timeEnd(p.width) + 1;
240
+ const cx = putText(line, x, limit, tx(p, spec.name), { fg: spec.laneColor, bold: true });
241
+ putText(line, cx + 1, limit, tx(p, truncateDisplay(spec.arg, Math.max(0, limit - cx - 1), p.glyphs.ellipsis)), { fg: activeTokens().textMuted });
242
+ return line;
243
+ }
244
+
245
+ /** A lane merging back (├─╯) with its result line; amber with ! when it carries a failure. */
246
+ export function paintMergeRow(gutter: GutterRow, text: string, warn: boolean, p: GraphPaint): Line {
247
+ const t = activeTokens();
248
+ const line = createEmptyLine(p.width);
249
+ paintGutter(line, gutter, p);
250
+ const x = textColumn(p);
251
+ const limit = timeEnd(p.width) + 1;
252
+ if (warn) {
253
+ putText(line, x, limit, p.glyphs.warn, { fg: t.warning, bold: true });
254
+ putText(line, x + 2, limit, tx(p, truncateDisplay(text, Math.max(0, limit - x - 2), p.glyphs.ellipsis)), { fg: t.warning });
255
+ } else {
256
+ putText(line, x, limit, tx(p, truncateDisplay(text, Math.max(0, limit - x), p.glyphs.ellipsis)), { fg: t.textFaint });
257
+ }
258
+ return line;
259
+ }
260
+
261
+ export interface FoldedRowSpec extends LaneRowSpec {
262
+ /** ✓ ! ✕ before the summary. */
263
+ readonly outcome: 'ok' | 'warn' | 'err' | 'run';
264
+ readonly summary: string;
265
+ readonly time: string | undefined;
266
+ }
267
+
268
+ /** A finished lane folded to one ◉ bead on its parent's lane. */
269
+ export function paintFoldedRow(gutter: GutterRow, spec: FoldedRowSpec, p: GraphPaint): Line {
270
+ const t = activeTokens();
271
+ const line = createEmptyLine(p.width);
272
+ paintGutter(line, gutter, p);
273
+ const x = textColumn(p);
274
+ const tone: SummaryTone = spec.outcome === 'ok' ? 'good' : spec.outcome === 'warn' ? 'warn' : spec.outcome === 'err' ? 'bad' : 'faint';
275
+ const mark = spec.outcome === 'ok' ? p.glyphs.ok : spec.outcome === 'warn' ? p.glyphs.warn : spec.outcome === 'err' ? p.glyphs.err : p.glyphs.run[p.frame % p.glyphs.run.length]!;
276
+ const limit = paintRight(line, p, { text: spec.summary, tone }, mark, spec.time, spec.outcome === 'run' ? spec.laneColor : t.textFaint);
277
+ let cx = putText(line, x, limit, tx(p, spec.name), { fg: spec.laneColor, bold: true });
278
+ cx = putText(line, cx + 1, Math.max(cx + 1, limit - 2), tx(p, truncateDisplay(spec.arg, Math.max(0, limit - cx - 3), p.glyphs.ellipsis)), { fg: t.textMuted });
279
+ putText(line, cx + 1, limit, p.glyphs.closed, { fg: t.textMuted });
280
+ return line;
281
+ }
282
+
283
+ /**
284
+ * The turn header: "◆ model · 4 tools · 1 agent · 1.9s". Folded, the ◆ sits in
285
+ * the gutter and a ▸ closes the row.
286
+ */
287
+ export function paintHeadRow(gutter: GutterRow, text: string, folded: boolean, p: GraphPaint): Line {
288
+ const t = activeTokens();
289
+ const line = createEmptyLine(p.width);
290
+ paintGutter(line, gutter, p);
291
+ const x = textColumn(p);
292
+ const limit = timeEnd(p.width) + 1;
293
+ let cx = x;
294
+ if (!folded) cx = putText(line, cx, limit, `${p.glyphs.role.turn} `, { fg: t.brand, bold: true });
295
+ cx = putText(line, cx, limit - (folded ? 2 : 0), tx(p, truncateDisplay(text, Math.max(0, limit - cx - (folded ? 2 : 0)), p.glyphs.ellipsis)), { fg: t.textFaint });
296
+ if (folded) putText(line, cx + 1, limit, p.glyphs.closed, { fg: t.textMuted });
297
+ return line;
298
+ }
299
+
300
+ /**
301
+ * Move an already-rendered transcript line `shift` columns right (prose rendered
302
+ * at a narrower width so it starts at the text column) and draw `gutter` over
303
+ * its left edge.
304
+ */
305
+ export function shiftUnderGutter(source: Line, shift: number, gutter: GutterRow | null, p: GraphPaint): Line {
306
+ const line = createEmptyLine(p.width);
307
+ for (let i = 0; i < source.length; i++) {
308
+ const target = i + shift;
309
+ if (target >= p.width) break;
310
+ line[target] = source[i]!;
311
+ }
312
+ if (gutter) paintGutter(line, gutter, p);
313
+ return line;
314
+ }
315
+
316
+ // ---------------------------------------------------------------------------
317
+ // Bodies
318
+ // ---------------------------------------------------------------------------
319
+
320
+ export interface BodyPaintOptions {
321
+ /** Show every row instead of the first BODY_CAP / DIFF_CAP. */
322
+ readonly expanded: boolean;
323
+ /** The semantic summary for a diff body: undefined while it is being read, null when there is none. */
324
+ readonly semantic?: SemanticDiff | null | undefined;
325
+ }
326
+
327
+ /** The body fill's inset: from the text column to W-4, `h` rows. */
328
+ function bodyInset(canvas: SurfaceCanvas, p: GraphPaint, h: number, bg: string): KitInset {
329
+ const x = textColumn(p);
330
+ return inset(canvas, x, 0, timeEnd(p.width) - x + 1, h, bg);
331
+ }
332
+
333
+ function moreText(n: number, what: string, p: GraphPaint): string {
334
+ return `${p.glyphs.ellipsis} ${n} more${what ? ` ${what}` : ''}`;
335
+ }
336
+
337
+ /**
338
+ * Text lines wrapped in full to the inset's text width. A body is file text or
339
+ * command output, so its layout is kept: indentation and inner spacing stay,
340
+ * tabs become spaces, and wrapped rows keep the line's indent.
341
+ */
342
+ function wrapAll(lines: ReadonlyArray<{ text: string; fg: string }>, width: number): Array<{ text: string; fg: string }> {
343
+ const out: Array<{ text: string; fg: string }> = [];
344
+ for (const line of lines) {
345
+ const parts = wrapPreservingIndent(line.text, Math.max(8, width));
346
+ for (const part of parts.length > 0 ? parts : ['']) out.push({ text: part, fg: line.fg });
347
+ }
348
+ return out;
349
+ }
350
+
351
+ /** One drawn row of a read body: colored runs from the inset's left edge. */
352
+ type ReadRow = ReadonlyArray<{ readonly text: string; readonly fg: string; readonly bold?: boolean; readonly italic?: boolean }>;
353
+
354
+ /**
355
+ * A file's lines as drawn rows: syntax colored for the file's language, tabs
356
+ * as two spaces, and each line wrapped the way a command's output wraps
357
+ * (indentation kept, continuation rows indented like the first).
358
+ */
359
+ function readRows(file: ReadBodyFile, width: number): ReadRow[] {
360
+ const t = activeTokens();
361
+ const lines = file.lines.map(expandTabs);
362
+ const tokens = highlightCodeLines(lines, file.path ? languageForPath(file.path) : '');
363
+ const rows: ReadRow[] = [];
364
+ lines.forEach((line, i) => {
365
+ const chars = Array.from(line);
366
+ // Each character's style, from the line's tokens (plain when they do not spell the line).
367
+ const lineTokens = tokens[i] ?? [];
368
+ const styles: Array<{ fg: string; bold?: boolean; italic?: boolean }> = [];
369
+ if (lineTokens.map((tk) => tk.text).join('') === line) {
370
+ for (const tk of lineTokens) for (const _ of Array.from(tk.text)) styles.push({ fg: tk.fg, bold: tk.bold, italic: tk.italic });
371
+ } else {
372
+ for (let k = 0; k < chars.length; k++) styles.push({ fg: t.text });
373
+ }
374
+ let cursor = 0;
375
+ for (const part of wrapPreservingIndent(line, Math.max(8, width))) {
376
+ const pc = Array.from(part);
377
+ let lead = 0;
378
+ while (pc[lead] === ' ') lead++;
379
+ while (chars[cursor] === ' ') cursor++;
380
+ const runs: Array<{ text: string; fg: string; bold?: boolean; italic?: boolean }> = [];
381
+ if (lead > 0) runs.push({ text: ' '.repeat(lead), fg: t.text });
382
+ for (let k = lead; k < pc.length; k++) {
383
+ const style = styles[cursor] ?? { fg: t.text };
384
+ cursor++;
385
+ const last = runs[runs.length - 1];
386
+ if (last && last.fg === style.fg && last.bold === style.bold && last.italic === style.italic) last.text += pc[k];
387
+ else runs.push({ text: pc[k]!, ...style });
388
+ }
389
+ rows.push(runs);
390
+ }
391
+ });
392
+ return rows;
393
+ }
394
+
395
+ /**
396
+ * An opened read: the text the model read, drawn like a command's output
397
+ * (indented exactly, syntax colored, "… N more lines" past the same BODY_CAP).
398
+ * One file shows its text alone, since the row already names it; several
399
+ * files show one short section each, headed by the path and line count, and
400
+ * share BODY_CAP between them (at least 3 rows each).
401
+ */
402
+ function paintReadBody(files: readonly ReadBodyFile[], p: GraphPaint, options: BodyPaintOptions, innerWidth: number): { lines: Line[]; capped: boolean } {
403
+ const t = activeTokens();
404
+ const headed = files.length > 1;
405
+ const perFile = headed ? Math.max(3, Math.floor(BODY_CAP / files.length)) : BODY_CAP;
406
+ type Drawn = { kind: 'row'; row: ReadRow } | { kind: 'header'; file: ReadBodyFile } | { kind: 'more'; hidden: number } | { kind: 'error'; text: string } | { kind: 'gap' };
407
+ const drawn: Drawn[] = [];
408
+ let capped = false;
409
+ files.forEach((file, index) => {
410
+ if (headed) {
411
+ if (index > 0) drawn.push({ kind: 'gap' });
412
+ drawn.push({ kind: 'header', file });
413
+ }
414
+ if (file.error !== undefined) {
415
+ for (const part of wrapPreservingIndent(file.error, Math.max(8, innerWidth))) drawn.push({ kind: 'error', text: part });
416
+ return;
417
+ }
418
+ const rows = readRows(file, innerWidth);
419
+ // Trailing blank rows say nothing.
420
+ while (rows.length > 0 && rows[rows.length - 1]!.length === 0) rows.pop();
421
+ const cap = options.expanded ? rows.length : Math.min(rows.length, perFile);
422
+ for (let k = 0; k < cap; k++) drawn.push({ kind: 'row', row: rows[k]! });
423
+ if (rows.length > cap) {
424
+ capped = true;
425
+ drawn.push({ kind: 'more', hidden: rows.length - cap });
426
+ }
427
+ });
428
+ const height = drawn.length + 2;
429
+ const canvas = new SurfaceCanvas(p.width, height);
430
+ const pn = bodyInset(canvas, p, height, t.backgroundPanel);
431
+ const right = pn.l + innerWidth;
432
+ drawn.forEach((item, k) => {
433
+ const y = 1 + k;
434
+ if (item.kind === 'header') {
435
+ // A file that could not be read has no line count to state; its error follows.
436
+ const count = item.file.error !== undefined ? '' : `${item.file.lineCount} line${item.file.lineCount === 1 ? '' : 's'}`;
437
+ const path = truncateDisplay(item.file.path ?? 'file', Math.max(1, innerWidth - (count ? getDisplayWidth(count) + 2 : 0)), p.glyphs.ellipsis);
438
+ const cx = canvas.put(pn.l, y, path, { fg: t.text, bold: true });
439
+ if (count) canvas.put(cx + 2, y, count, { fg: t.textFaint });
440
+ } else if (item.kind === 'row') {
441
+ let cx = pn.l;
442
+ for (const run of item.row) {
443
+ const room = right - cx;
444
+ if (room <= 0) break;
445
+ cx = canvas.put(cx, y, truncateDisplay(run.text, room, p.glyphs.ellipsis), { fg: run.fg, bold: run.bold, italic: run.italic });
446
+ }
447
+ } else if (item.kind === 'more') {
448
+ canvas.put(pn.l, y, moreText(item.hidden, 'lines', p), { fg: t.textFaint });
449
+ } else if (item.kind === 'error') {
450
+ canvas.put(pn.l, y, truncateDisplay(item.text, innerWidth, p.glyphs.ellipsis), { fg: t.error });
451
+ }
452
+ });
453
+ return { lines: canvas.lines, capped };
454
+ }
455
+
456
+ /**
457
+ * Draw an opened bead's body. Returns full-width lines (the caller draws the
458
+ * lane gutter over their left edge) and whether rows were left out.
459
+ */
460
+ export function paintBody(body: BeadBody, p: GraphPaint, options: BodyPaintOptions): { lines: Line[]; capped: boolean } {
461
+ const t = activeTokens();
462
+ const x = textColumn(p);
463
+ const innerWidth = timeEnd(p.width) - x + 1 - 4;
464
+
465
+ if (body.kind === 'diff') {
466
+ const files = parseChanges(body.diff);
467
+ const codeWidth = Math.max(8, innerWidth - 8);
468
+ let { rows } = buildDiffRows(files, codeWidth, files.length > 1);
469
+ if (!body.numbered) {
470
+ // Line numbers are unknown: the "⋯ line N" separators would state one.
471
+ rows = rows.filter((row, k) => row.kind !== 'sep' || k > 0).map((row): DiffRow => (row.kind === 'sep' ? { ...row, text: p.glyphs.ellipsis } : row));
472
+ }
473
+ const semanticRows = options.semantic && options.semantic.totalChanges > 0 ? 2 : 0;
474
+ const cap = options.expanded ? rows.length : Math.min(rows.length, DIFF_CAP);
475
+ const hidden = rows.length - cap;
476
+ const height = 1 + semanticRows + cap + (hidden > 0 ? 1 : 0) + 1;
477
+ const canvas = new SurfaceCanvas(p.width, height);
478
+ const pn = bodyInset(canvas, p, height, t.backgroundPanel);
479
+ let y = 1;
480
+ if (semanticRows > 0 && options.semantic) {
481
+ canvas.put(pn.l, y, '◈', { fg: t.brandEnd });
482
+ let cx = pn.l + 2;
483
+ for (const chip of semanticChips(options.semantic)) {
484
+ if (cx + getDisplayWidth(chip.text) > pn.r) break;
485
+ cx = canvas.put(cx, y, chip.text, { fg: chip.fg }) + 2;
486
+ }
487
+ y += 2;
488
+ }
489
+ for (let k = 0; k < cap; k++) drawDiffRow(canvas, pn, y + k, rows[k]!, -1, { lineNumbers: body.numbered });
490
+ if (hidden > 0) canvas.put(pn.l, y + cap, moreText(hidden, 'lines', p), { fg: t.textFaint });
491
+ return { lines: canvas.lines, capped: hidden > 0 };
492
+ }
493
+
494
+ if (body.kind === 'error') {
495
+ const wrapped = wrapAll([{ text: body.text, fg: t.error }], innerWidth);
496
+ const height = wrapped.length + 2;
497
+ const canvas = new SurfaceCanvas(p.width, height);
498
+ const pn = bodyInset(canvas, p, height, t.backgroundError);
499
+ wrapped.forEach((row, k) => canvas.put(pn.l, 1 + k, row.text, { fg: row.fg }));
500
+ return { lines: canvas.lines, capped: false };
501
+ }
502
+
503
+ if (body.kind === 'read') return paintReadBody(body.files, p, options, innerWidth);
504
+
505
+ let rows: Array<{ text: string; fg: string }>;
506
+ let footer: string | undefined;
507
+ if (body.kind === 'output') {
508
+ rows = wrapAll(body.lines.map((l) => ({ text: l.text, fg: bodyColor(l.tone) })), innerWidth);
509
+ footer = body.footer;
510
+ } else if (body.kind === 'list') {
511
+ rows = body.items.map((item) => {
512
+ const detail = item.detail ? ` ${item.detail}` : '';
513
+ return { text: truncateDisplay(expandTabs(`${item.text}${detail}`), innerWidth, p.glyphs.ellipsis), fg: t.text };
514
+ });
515
+ } else {
516
+ rows = wrapAll(body.lines.map((text) => ({ text, fg: t.text })), innerWidth);
517
+ }
518
+ const cap = options.expanded ? rows.length : Math.min(rows.length, BODY_CAP);
519
+ const hidden = rows.length - cap;
520
+ const tail = (hidden > 0 ? 1 : 0) + (footer ? (hidden > 0 ? 1 : 2) : 0);
521
+ const height = cap + tail + 2;
522
+ const canvas = new SurfaceCanvas(p.width, height);
523
+ const pn = bodyInset(canvas, p, height, t.backgroundPanel);
524
+ for (let k = 0; k < cap; k++) canvas.put(pn.l, 1 + k, truncateDisplay(rows[k]!.text, innerWidth, p.glyphs.ellipsis), { fg: rows[k]!.fg });
525
+ let y = 1 + cap;
526
+ if (hidden > 0) canvas.put(pn.l, y++, moreText(hidden, 'lines', p), { fg: t.textFaint });
527
+ if (footer) {
528
+ if (hidden === 0) y++;
529
+ canvas.put(pn.l, y, truncateDisplay(footer, innerWidth, p.glyphs.ellipsis), { fg: t.textMuted });
530
+ }
531
+ // A list is short by design: name the items that did not fit rather than "lines".
532
+ if (body.kind === 'list' && hidden > 0) {
533
+ canvas.fill(pn.l, 1 + cap, innerWidth, 1, t.backgroundPanel);
534
+ canvas.put(pn.l, 1 + cap, moreText(hidden, '', p), { fg: t.textFaint });
535
+ }
536
+ return { lines: canvas.lines, capped: hidden > 0 };
537
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * lane-graph/semantic-memo.ts, the ◈ semantic summary of an opened edit's
3
+ * diff, computed once per diff and remembered.
4
+ *
5
+ * computeSemanticDiff parses with tree-sitter and is asynchronous, while the
6
+ * transcript renders synchronously. The first render of an opened edit starts
7
+ * the computation and draws without the summary; when it lands, every
8
+ * registered listener is told so the transcript repaints with it. The summary
9
+ * compares the diff's own before and after text (context plus removed lines
10
+ * against context plus added lines), so it names what the edit touched.
11
+ *
12
+ * Bounded: the most recent MAX_ENTRIES diffs are kept.
13
+ */
14
+
15
+ import type { SemanticDiff } from '../semantic-diff.ts';
16
+
17
+ const MAX_ENTRIES = 200;
18
+
19
+ const memo = new Map<string, SemanticDiff | null | 'pending'>();
20
+ const listeners = new Set<() => void>();
21
+ let generation = 0;
22
+
23
+ /** Bumps every time a summary lands: a render that drew an opened diff keys its cache on it. */
24
+ export function semanticSummaryGeneration(): number {
25
+ return generation;
26
+ }
27
+
28
+ /** Be told when a summary that was being computed is ready. Returns the unsubscribe. */
29
+ export function onSemanticSummaryReady(listener: () => void): () => void {
30
+ listeners.add(listener);
31
+ return () => { listeners.delete(listener); };
32
+ }
33
+
34
+ function beforeAfter(diff: string): { before: string; after: string } {
35
+ const before: string[] = [];
36
+ const after: string[] = [];
37
+ for (const line of diff.split('\n')) {
38
+ if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@') || line.startsWith('diff ') || line.startsWith('index ')) continue;
39
+ if (line.startsWith('+')) after.push(line.slice(1));
40
+ else if (line.startsWith('-')) before.push(line.slice(1));
41
+ else if (line.startsWith(' ')) { before.push(line.slice(1)); after.push(line.slice(1)); }
42
+ }
43
+ return { before: before.join('\n'), after: after.join('\n') };
44
+ }
45
+
46
+ /**
47
+ * The summary for `diff`: a SemanticDiff, null when there is none to show,
48
+ * or undefined while it is being computed (the first call starts it).
49
+ */
50
+ export function semanticSummaryFor(diff: string, path: string | undefined): SemanticDiff | null | undefined {
51
+ if (!path) return null;
52
+ const key = `${path}\u0000${diff}`;
53
+ const known = memo.get(key);
54
+ if (known === 'pending') return undefined;
55
+ if (known !== undefined) return known;
56
+ memo.set(key, 'pending');
57
+ while (memo.size > MAX_ENTRIES) {
58
+ const oldest = memo.keys().next();
59
+ if (oldest.done) break;
60
+ memo.delete(oldest.value);
61
+ }
62
+ const { before, after } = beforeAfter(diff);
63
+ void import('../semantic-diff.ts')
64
+ .then(({ computeSemanticDiff }) => computeSemanticDiff(path, before, after))
65
+ .catch(() => null)
66
+ .then((result) => {
67
+ memo.set(key, result);
68
+ generation++;
69
+ for (const listener of listeners) listener();
70
+ });
71
+ return undefined;
72
+ }
73
+
74
+ /** Seed a summary directly (tests and golden frames, which must not wait on tree-sitter). */
75
+ export function primeSemanticSummary(diff: string, path: string, summary: SemanticDiff | null): void {
76
+ memo.set(`${path}\u0000${diff}`, summary);
77
+ }