@pellux/goodvibes-tui 2.0.20 → 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 +70 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +59 -4
  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,157 @@
1
+ /**
2
+ * agent-view-render.ts, one agent opened full screen.
3
+ *
4
+ * The agent becomes the session: its own transcript is the spine (drawn by
5
+ * the same lane-graph path as main, conversation-rendering.ts), the agents
6
+ * it spawned branch off it as lanes, and steers you sent that it has not
7
+ * picked up yet sit at the bottom marked as queued. Its lanes are drawn with
8
+ * the color base shifted (lane-graph/paint.ts withLaneColorBase) so its spine
9
+ * keeps the color its lane has in main.
10
+ *
11
+ * row 0 ◆ engineer · the task · started by main 2m 14s ago
12
+ * row 1 (blank)
13
+ * rows the agent's transcript
14
+ *
15
+ * A WRFC chain owner takes no turns of its own: its view is its chain, each
16
+ * phase agent a lane off its spine drawn from that agent's own transcript
17
+ * (work-tree-model.ts buildWrfcOwnerModel).
18
+ *
19
+ * The thin bar down column 0, the blank rows above and below the body, and
20
+ * the scroll window are the caller's (shell/session-views.ts); this returns
21
+ * every line.
22
+ */
23
+
24
+ import { createEmptyLine, type Line } from '@pellux/goodvibes-sdk/platform/types';
25
+ import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
26
+ import { appendConversationMessages, drawConversationTurn, type ConversationRenderContext } from './conversation-rendering.ts';
27
+ import { buildWrfcOwnerModel } from './work-tree-model.ts';
28
+ import type { AgentLaneInfo, WorkTreeSources } from './work-tree-sources.ts';
29
+ import { laneColor, withLaneColorBase } from '../renderer/lane-graph/paint.ts';
30
+ import type { TreeGlyphSetName } from '../renderer/lane-graph/glyphs.ts';
31
+ import { UIFactory } from '../renderer/ui-factory.ts';
32
+ import { activeTokens } from '../renderer/theme.ts';
33
+ import { formatElapsed } from '../utils/format-elapsed.ts';
34
+ import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
35
+
36
+ export interface AgentViewInput {
37
+ readonly width: number;
38
+ readonly agent: AgentLaneInfo;
39
+ /** The agent's transcript (its lane info carries none for a WRFC owner). */
40
+ readonly messages: readonly ConversationMessageSnapshot[];
41
+ /** The agent's lane color index in main. */
42
+ readonly colorIndex: number;
43
+ /** Who started it: "main" or the parent agent's name. */
44
+ readonly parentName: string;
45
+ /** Main's work-tree sources (timings, spawned agents, waiting calls). */
46
+ readonly sources: WorkTreeSources;
47
+ /** Fold state for this view's beads; kept by the caller across frames. */
48
+ readonly collapse: Map<string, boolean>;
49
+ readonly glyphSet: TreeGlyphSetName;
50
+ readonly frame: number;
51
+ readonly now: number;
52
+ /** Steers sent from this view that the agent has not picked up yet, oldest first. */
53
+ readonly queuedSteers: readonly string[];
54
+ }
55
+
56
+ const TEXT_X = 3;
57
+
58
+ function put(line: Line, x: number, endX: number, text: string, style: { fg: string; bold?: boolean }): number {
59
+ let cx = x;
60
+ for (const ch of text) {
61
+ const w = getDisplayWidth(ch);
62
+ if (w <= 0) continue;
63
+ if (cx + w > endX || cx >= line.length) break;
64
+ line[cx] = { char: ch, fg: style.fg, bg: '', bold: style.bold ?? false, dim: false, underline: false, italic: false, strikethrough: false };
65
+ if (w === 2 && cx + 1 < line.length) line[cx + 1] = { ...line[cx]!, char: '' };
66
+ cx += w;
67
+ }
68
+ return cx;
69
+ }
70
+
71
+ function stateText(agent: AgentLaneInfo, parentName: string, now: number): { text: string; tone: 'faint' | 'good' | 'bad' | 'warn' } {
72
+ const started = agent.startedAt !== undefined ? `started by ${parentName} ${formatElapsed(Math.max(0, now - agent.startedAt))} ago` : `started by ${parentName}`;
73
+ switch (agent.status) {
74
+ case 'running': return { text: started, tone: 'faint' };
75
+ case 'pending': return { text: `waiting to start · ${started}`, tone: 'faint' };
76
+ case 'completed': {
77
+ const took = agent.startedAt !== undefined && agent.completedAt !== undefined ? ` in ${formatElapsed(agent.completedAt - agent.startedAt)}` : '';
78
+ return { text: `finished${took} · ${agent.toolCallCount} tool${agent.toolCallCount === 1 ? '' : 's'}`, tone: 'good' };
79
+ }
80
+ case 'failed': return { text: `failed${agent.error ? `: ${agent.error.split('\n')[0]}` : ''}`, tone: 'bad' };
81
+ case 'cancelled': return { text: 'stopped', tone: 'warn' };
82
+ }
83
+ }
84
+
85
+ /** The ◆ head row. */
86
+ function headRow(input: AgentViewInput): Line {
87
+ const t = activeTokens();
88
+ const line = createEmptyLine(input.width);
89
+ for (const cell of line) cell.bg = '';
90
+ const end = input.width - 3;
91
+ const color = laneColor(input.colorIndex);
92
+ let x = put(line, TEXT_X, end, '◆', { fg: color, bold: true });
93
+ // A chain owner is named for what it is, as its lane in main is.
94
+ x = put(line, x + 1, end, input.agent.wrfcPhases ? 'WRFC chain' : input.agent.hostedLabel ?? input.agent.name, { fg: color, bold: true });
95
+ const state = stateText(input.agent, input.parentName, input.now);
96
+ const stateFg = state.tone === 'good' ? t.success : state.tone === 'bad' ? t.error : state.tone === 'warn' ? t.warning : t.textFaint;
97
+ const task = input.agent.task.split('\n').find((l) => l.trim().length > 0)?.trim() ?? '';
98
+ // The state is kept whole; the task yields its room first.
99
+ const stateW = getDisplayWidth(state.text) + 3;
100
+ const taskRoom = end - x - 3 - stateW;
101
+ if (task && taskRoom >= 8) {
102
+ x = put(line, x + 1, end, '·', { fg: t.textFaint });
103
+ x = put(line, x + 1, end, truncateDisplay(task, taskRoom), { fg: t.textMuted });
104
+ }
105
+ x = put(line, x + 1, end, '·', { fg: t.textFaint });
106
+ put(line, x + 1, end, truncateDisplay(state.text, Math.max(0, end - x - 1)), { fg: stateFg });
107
+ return line;
108
+ }
109
+
110
+ /** Every line of the agent view, top to bottom. */
111
+ export function renderAgentView(input: AgentViewInput): Line[] {
112
+ const t = activeTokens();
113
+ const lines: Line[] = [headRow(input), createEmptyLine(input.width)];
114
+ const running = input.agent.status === 'running' || input.agent.status === 'pending';
115
+ const context: ConversationRenderContext = {
116
+ history: { addLine: (l) => { lines.push(l); }, addLines: (ls) => { lines.push(...ls); }, getLineCount: () => lines.length },
117
+ blockRegistry: [],
118
+ collapseState: input.collapse,
119
+ errorLineRegistry: [],
120
+ messageKindRegistry: new Map(),
121
+ configManager: null,
122
+ splashOptions: {},
123
+ // The agent's own turns are live while the agent runs, not while main does.
124
+ workTreeSources: { ...input.sources, turnActive: () => running, turnTiming: undefined },
125
+ treeGlyphSet: input.glyphSet,
126
+ focusId: null,
127
+ frame: input.frame,
128
+ };
129
+ const phases = input.agent.wrfcPhases;
130
+ if (phases) {
131
+ if (phases.length > 0) {
132
+ const model = buildWrfcOwnerModel({ info: input.agent, sources: context.workTreeSources ?? {}, collapse: input.collapse });
133
+ withLaneColorBase(input.colorIndex, () => { lines.push(...drawConversationTurn(context, model, input.width).lines); });
134
+ }
135
+ } else {
136
+ withLaneColorBase(input.colorIndex, () => appendConversationMessages(context, [...input.messages], input.width, []));
137
+ }
138
+ const empty = phases ? phases.length === 0 : input.messages.length === 0;
139
+ if (empty) {
140
+ const note = createEmptyLine(input.width);
141
+ for (const cell of note) cell.bg = '';
142
+ const text = phases
143
+ ? (running ? 'The chain has not started a phase yet.' : 'This chain ran no phases.')
144
+ : running ? 'No transcript yet: the agent has not taken a turn.' : 'This agent left no transcript.';
145
+ put(note, TEXT_X + 2, input.width - 3, text, { fg: t.textFaint });
146
+ lines.push(note);
147
+ }
148
+ for (const steer of input.queuedSteers) {
149
+ lines.push(createEmptyLine(input.width));
150
+ lines.push(...UIFactory.createMessageBar(input.width, steer));
151
+ const note = createEmptyLine(input.width);
152
+ for (const cell of note) cell.bg = '';
153
+ put(note, TEXT_X + 2, input.width - 3, '⧗ queued · it arrives at the agent\'s next turn', { fg: t.textFaint });
154
+ lines.push(note);
155
+ }
156
+ return lines;
157
+ }
@@ -14,16 +14,30 @@
14
14
  * wrapped promise settles) so it reflects "a prompt just appeared", not
15
15
  * "a prompt was just resolved".
16
16
  *
17
- * PRIVACY: message content is tool name + permission category only, no
18
- * args, no file contents, no command strings (mirrors long-task-notifier's
19
- * privacy guarantee).
17
+ * Text (owner ruling 2026-09-29, SDK runtime/turn-notification.ts): the
18
+ * notice names what is waiting, the command for exec, the file for write and
19
+ * edit, the URL for fetch, and the turn that asked for it. When
20
+ * behavior.notificationsMetadataOnly is on (default off) every channel here
21
+ * (OSC 9, desktop, webhook) sends the tool name and permission category only,
22
+ * never a command, a path or the turn's name.
20
23
  */
21
24
  import { notifyCompletion } from '@pellux/goodvibes-sdk/platform/utils';
22
25
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
23
26
  import type { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
24
27
  import type { PermissionRequestHandler, PermissionPromptRequest } from '@pellux/goodvibes-sdk/platform/permissions';
25
28
  import type { FocusTracker } from '@pellux/goodvibes-sdk/platform/runtime/operations';
26
- import { shouldFireAlert, FORCE_NOTIFY_DURATION_MS, type ConfigGet } from '@pellux/goodvibes-sdk/platform/runtime/operations';
29
+ import {
30
+ shouldFireAlert,
31
+ FORCE_NOTIFY_DURATION_MS,
32
+ NOTIFICATION_TEXT_LIMITS,
33
+ buildApprovalNotification,
34
+ describeToolTarget,
35
+ formatWebhookText,
36
+ joinNotificationLine,
37
+ readNotificationsMetadataOnly,
38
+ resolveTurnName,
39
+ type ConfigGet,
40
+ } from '@pellux/goodvibes-sdk/platform/runtime/operations';
27
41
  import type { TerminalNotifier } from './terminal-notifier.ts';
28
42
 
29
43
  export interface ApprovalAlertDeps {
@@ -37,6 +51,21 @@ export interface ApprovalAlertDeps {
37
51
  * desktop-alert gate below). Absent in tests/headless.
38
52
  */
39
53
  readonly terminalNotifier?: TerminalNotifier | null;
54
+ /**
55
+ * The live conversation, read for the name of the turn that is asking: a
56
+ * title the user set, else the message that started this turn (the last
57
+ * user message while a turn runs). Absent means the notice names no turn.
58
+ */
59
+ readonly conversation?: ApprovalTurnSource | null;
60
+ /** Desktop delivery; defaults to the SDK notifyCompletion. Tests pass a spy to read the text. */
61
+ readonly notifyDesktop?: typeof notifyCompletion;
62
+ }
63
+
64
+ /** The conversation surface the approval notice names the turn from. */
65
+ export interface ApprovalTurnSource {
66
+ readonly title: string;
67
+ getTitleSource(): string;
68
+ getLastUserMessage(): string | null;
40
69
  }
41
70
 
42
71
  /**
@@ -56,19 +85,28 @@ export function wrapRequestPermissionWithAlert(
56
85
  }
57
86
 
58
87
  function fireApprovalAlert(request: PermissionPromptRequest, deps: ApprovalAlertDeps): void {
59
- // PRIVACY: tool name + category only. Never request.args.
60
- const message = `${request.tool} (${request.category}) is waiting for approval`;
88
+ const metadataOnly = readNotificationsMetadataOnly(deps.configGet);
89
+ const conversation = deps.conversation;
90
+ const titleSource = conversation?.getTitleSource();
91
+ const notice = buildApprovalNotification({
92
+ tool: request.tool,
93
+ category: request.category,
94
+ target: metadataOnly ? null : describeToolTarget(request.args, request.analysis?.target),
95
+ turnName: metadataOnly || !conversation ? null : resolveTurnName({
96
+ title: conversation.title,
97
+ titleSource: titleSource === 'user' || titleSource === 'system' ? titleSource : null,
98
+ turnText: conversation.getLastUserMessage(),
99
+ }),
100
+ }, { metadataOnly });
61
101
 
62
102
  // In-terminal (OSC 9) notification fires on its OWN gating (independent of the
63
103
  // desktop-alert gate below), so it is emitted before the early return.
64
- deps.terminalNotifier?.notify('approval-wait', message);
104
+ deps.terminalNotifier?.notify('approval-wait', joinNotificationLine(notice, NOTIFICATION_TEXT_LIMITS.terminal));
65
105
 
66
106
  if (!shouldFireAlert(deps.focusTracker, deps.configGet, 'behavior.notifyOnApprovalPending')) return;
67
107
 
68
- const title = 'GoodVibes: approval needed';
69
-
70
108
  try {
71
- notifyCompletion(title, message, FORCE_NOTIFY_DURATION_MS);
109
+ (deps.notifyDesktop ?? notifyCompletion)(notice.title, notice.body, FORCE_NOTIFY_DURATION_MS);
72
110
  } catch (err) {
73
111
  logger.debug('approval-alert: desktop notify error', { error: String(err) });
74
112
  }
@@ -77,7 +115,7 @@ function fireApprovalAlert(request: PermissionPromptRequest, deps: ApprovalAlert
77
115
  if (webhookNotifier) {
78
116
  const urls = webhookNotifier.getUrls();
79
117
  if (urls.length > 0) {
80
- webhookNotifier.send(message).catch((err: unknown) => {
118
+ webhookNotifier.send(formatWebhookText(notice)).catch((err: unknown) => {
81
119
  logger.debug('approval-alert: webhook send error', { error: String(err) });
82
120
  });
83
121
  }
@@ -25,7 +25,13 @@
25
25
 
26
26
  import type { ConversationManager } from './conversation.ts';
27
27
 
28
- const MESSAGE_BOOKMARK_KEY = /^msg_(\d+)$/;
28
+ /**
29
+ * `msg_<idx>` (a message block), or `bead_c:<idx>:<call>` (a work-tree bead of
30
+ * the main transcript: its call's message index). A bead inside a folded turn
31
+ * or lane has no block of its own; its message line is the row that stands
32
+ * for it (the folded turn's header, or the ◉ of its folded lane's spawn).
33
+ */
34
+ const MESSAGE_BOOKMARK_KEY = /^(?:msg_|bead_c:)(\d+)(?::\d+)?$/;
29
35
 
30
36
  /**
31
37
  * Resolve a bookmark key that missed a direct BlockMeta lookup. Returns the
@@ -13,18 +13,28 @@
13
13
  * (notifyCompletion) + outbound webhook (WebhookNotifier), both gated by
14
14
  * the shared focus/config gating in alert-gating.ts. Skips evaluation
15
15
  * entirely when the session model is unpriced, a $0 placeholder cost must
16
- * never be reported as a real breach (mirrors CostTrackerPanel's "unpriced"
16
+ * never be reported as a real breach (mirrors the usage modal's "unpriced"
17
17
  * display convention).
18
18
  *
19
- * PRIVACY: message text is built from cost/threshold numbers and the
20
- * session id prefix only, no conversation content.
19
+ * Text (owner ruling 2026-09-29, SDK runtime/turn-notification.ts): the
20
+ * notice names the budget that tripped (cost against the configured
21
+ * threshold) and the turn during which it tripped. When
22
+ * behavior.notificationsMetadataOnly is on (default off) the desktop and
23
+ * webhook text carries the numbers and the session id prefix only.
21
24
  */
22
25
  import { notifyCompletion } from '@pellux/goodvibes-sdk/platform/utils';
23
26
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
24
27
  import type { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
25
28
  import { calcSessionCost, computeBudgetBreach, isModelPriced } from '@pellux/goodvibes-sdk/platform/providers';
26
29
  import type { FocusTracker } from '@pellux/goodvibes-sdk/platform/runtime/operations';
27
- import { shouldFireAlert, FORCE_NOTIFY_DURATION_MS, type ConfigGet } from '@pellux/goodvibes-sdk/platform/runtime/operations';
30
+ import {
31
+ shouldFireAlert,
32
+ FORCE_NOTIFY_DURATION_MS,
33
+ buildBudgetNotification,
34
+ formatWebhookText,
35
+ readNotificationsMetadataOnly,
36
+ type ConfigGet,
37
+ } from '@pellux/goodvibes-sdk/platform/runtime/operations';
28
38
 
29
39
  export interface BudgetBreachUsageSnapshot {
30
40
  readonly input: number;
@@ -38,6 +48,10 @@ export interface BudgetBreachNotifierDeps {
38
48
  readonly configGet: ConfigGet;
39
49
  readonly webhookNotifier?: WebhookNotifier | null;
40
50
  readonly sessionId: string;
51
+ /** The name of the turn that just ended (the one during which the budget tripped). */
52
+ readonly getTurnName?: (() => string | null) | undefined;
53
+ /** Desktop delivery; defaults to the SDK notifyCompletion. Tests pass a spy to read the text. */
54
+ readonly notifyDesktop?: typeof notifyCompletion | undefined;
41
55
  }
42
56
 
43
57
  /** Stateful edge-trigger checker, construct once per session, call on every TURN_COMPLETED. */
@@ -85,11 +99,16 @@ export function createBudgetBreachNotifier(deps: BudgetBreachNotifierDeps): Budg
85
99
  function fireBudgetBreachAlert(deps: BudgetBreachNotifierDeps, sessionCost: number, budgetThresholdUsd: number): void {
86
100
  if (!shouldFireAlert(deps.focusTracker, deps.configGet, 'behavior.notifyOnBudgetBreach')) return;
87
101
 
88
- const title = 'GoodVibes: budget breach';
89
- const message = `session cost $${sessionCost.toFixed(2)} exceeded budget $${budgetThresholdUsd.toFixed(2)} · session ${deps.sessionId.slice(0, 8)}`;
102
+ const metadataOnly = readNotificationsMetadataOnly(deps.configGet);
103
+ const notice = buildBudgetNotification({
104
+ sessionCostUsd: sessionCost,
105
+ budgetUsd: budgetThresholdUsd,
106
+ sessionId: deps.sessionId,
107
+ turnName: metadataOnly ? null : deps.getTurnName?.() ?? null,
108
+ }, { metadataOnly });
90
109
 
91
110
  try {
92
- notifyCompletion(title, message, FORCE_NOTIFY_DURATION_MS);
111
+ (deps.notifyDesktop ?? notifyCompletion)(notice.title, notice.body, FORCE_NOTIFY_DURATION_MS);
93
112
  } catch (err) {
94
113
  logger.debug('budget-breach-notifier: desktop notify error', { error: String(err) });
95
114
  }
@@ -98,7 +117,7 @@ function fireBudgetBreachAlert(deps: BudgetBreachNotifierDeps, sessionCost: numb
98
117
  if (webhookNotifier) {
99
118
  const urls = webhookNotifier.getUrls();
100
119
  if (urls.length > 0) {
101
- webhookNotifier.send(message).catch((err: unknown) => {
120
+ webhookNotifier.send(formatWebhookText(notice)).catch((err: unknown) => {
102
121
  logger.debug('budget-breach-notifier: webhook send error', { error: String(err) });
103
122
  });
104
123
  }
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * The `[Compaction]` prefix is one of the FORCE_CONVERSATION_PREFIXES the
17
17
  * system-message router always surfaces inline (see system-message-router.ts),
18
- * so the receipt cannot be routed away into a panel and vanish.
18
+ * so the receipt cannot be routed away from the transcript and vanish.
19
19
  */
20
20
 
21
21
  /** The shape of the SDK's COMPACTION_RECEIPT event payload we render from. */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared context-window usage computation.
3
3
  *
4
- * Centralises the tokens/contextWindow ratio so all panels, auto-compact,
4
+ * Centralises the tokens/contextWindow ratio so all views, auto-compact,
5
5
  * and session-maintenance read from one formula rather than six diverging
6
6
  * hand-rolled copies.
7
7
  *
@@ -7,7 +7,7 @@
7
7
  * architecture line-count gate; behaviour is unchanged.
8
8
  */
9
9
 
10
- import { activeTheme } from '../renderer/theme.ts';
10
+ import { activeTheme, activeTokens } from '../renderer/theme.ts';
11
11
  import { renderMarkdownTracked } from '../renderer/markdown.ts';
12
12
  import { renderConversationEventLine, renderConversationFoldedRow } from '../renderer/conversation-surface.ts';
13
13
  import { GLYPHS } from '../renderer/ui-primitives.ts';
@@ -54,10 +54,10 @@ export function renderCompactionContinuationMessage(
54
54
  markerFg: T.toolAccent,
55
55
  label: 'compaction handoff',
56
56
  labelFg: T.toolAccent,
57
- detailFg: '244',
57
+ detailFg: activeTokens().textMuted,
58
58
  };
59
59
  const details = [
60
- { text: ` ${isCollapsed ? GLYPHS.navigation.collapsed : GLYPHS.navigation.expanded} ${lineCount} line${lineCount === 1 ? '' : 's'} `, fg: '244', dim: true },
60
+ { text: ` ${isCollapsed ? GLYPHS.navigation.collapsed : GLYPHS.navigation.expanded} ${lineCount} line${lineCount === 1 ? '' : 's'} `, fg: activeTokens().textFaint },
61
61
  ];
62
62
 
63
63
  if (isCollapsed) {
@@ -0,0 +1,86 @@
1
+ /**
2
+ * conversation-display-only.ts, the display-only output a conversation keeps.
3
+ *
4
+ * A command's printed text or a tool result row is drawn into the transcript
5
+ * but is not a message, so a rebuild (a resize, a theme change, the splash
6
+ * leaving) would drop it. ConversationManager keeps each such draw here, with
7
+ * the number of messages that existed when it was drawn, and draws it again
8
+ * in the same place on every rebuild. While a reply streams, draws made during
9
+ * the turn sit below the streamed text and are drawn again after each delta.
10
+ */
11
+
12
+ /** Display-only draws kept for redraw; the oldest leave first. */
13
+ const MAX_DISPLAY_ONLY_ENTRIES = 500;
14
+
15
+ interface DisplayOnlyEntry {
16
+ readonly seq: number;
17
+ /** Messages that existed when it was drawn: it goes right after the unit holding message `anchor - 1`. */
18
+ readonly anchor: number;
19
+ readonly draw: (width: number) => void;
20
+ }
21
+
22
+ /** One rebuild's walk over the kept draws, in order. */
23
+ export interface DisplayOnlyCursor {
24
+ /** Draw every kept entry printed while messages up to `lastIndex` existed. */
25
+ through(lastIndex: number): void;
26
+ /** During a streaming rebuild: the entries not yet drawn go below the streamed text. */
27
+ trailStreamed(lineCount: () => number): void;
28
+ }
29
+
30
+ export class DisplayOnlyOutput {
31
+ private entries: DisplayOnlyEntry[] = [];
32
+ private seq = 0;
33
+ /** While a reply streams, draws from this seq on sit below the streamed text. */
34
+ private trailingFromSeq = Number.POSITIVE_INFINITY;
35
+ /** Lines those trailing draws take, below the streamed text. */
36
+ trailingLineCount = 0;
37
+
38
+ get count(): number { return this.entries.length; }
39
+
40
+ drop(): void {
41
+ this.entries = [];
42
+ this.resetTrailing();
43
+ }
44
+
45
+ resetTrailing(): void {
46
+ this.trailingFromSeq = Number.POSITIVE_INFINITY;
47
+ this.trailingLineCount = 0;
48
+ }
49
+
50
+ /** Keep a draw; `anchor` is the message count when it was printed. */
51
+ keep(anchor: number, draw: (width: number) => void): number {
52
+ const seq = ++this.seq;
53
+ this.entries.push({ seq, anchor, draw });
54
+ if (this.entries.length > MAX_DISPLAY_ONLY_ENTRIES) this.entries.splice(0, this.entries.length - MAX_DISPLAY_ONLY_ENTRIES);
55
+ return seq;
56
+ }
57
+
58
+ /** A kept draw was just drawn at the end of the buffer; while streaming it trails the streamed text. */
59
+ noteDrawn(seq: number, lines: number, streaming: boolean): void {
60
+ if (!streaming) return;
61
+ if (this.trailingFromSeq === Number.POSITIVE_INFINITY) this.trailingFromSeq = seq;
62
+ this.trailingLineCount += lines;
63
+ }
64
+
65
+ /** Draw the entries kept below the streamed text again, and count their lines. */
66
+ drawTrailing(width: number, lineCount: () => number): void {
67
+ const before = lineCount();
68
+ for (const entry of this.entries) if (entry.seq >= this.trailingFromSeq) entry.draw(width);
69
+ this.trailingLineCount = lineCount() - before;
70
+ }
71
+
72
+ cursor(width: number): DisplayOnlyCursor {
73
+ const kept = this.entries;
74
+ let next = 0;
75
+ return {
76
+ through: (lastIndex) => {
77
+ while (next < kept.length && kept[next]!.anchor <= lastIndex + 1) kept[next++]!.draw(width);
78
+ },
79
+ trailStreamed: (lineCount) => {
80
+ if (next >= kept.length) return;
81
+ this.trailingFromSeq = kept[next]!.seq;
82
+ this.drawTrailing(width, lineCount);
83
+ },
84
+ };
85
+ }
86
+ }
@@ -40,3 +40,30 @@ export function resolveTranscriptEventLine(
40
40
  }
41
41
  return [...lines].reverse().find((line) => line < currentLine) ?? lines[lines.length - 1]!;
42
42
  }
43
+
44
+ /** The next (or previous) line in a sorted list after (or before) `currentLine`, wrapping around; -1 when the list is empty. */
45
+ export function wrapToLine(lines: readonly number[], currentLine: number, direction: 'next' | 'prev'): number {
46
+ if (lines.length === 0) return -1;
47
+ if (direction === 'next') return lines.find((line) => line > currentLine) ?? lines[0]!;
48
+ return [...lines].reverse().find((line) => line < currentLine) ?? lines[lines.length - 1]!;
49
+ }
50
+
51
+ /** The block containing `lineIndex`, else the one starting nearest it (optionally only blocks of `typeFilter`). */
52
+ export function nearestBlock<B extends { readonly type: string; readonly startLine: number; readonly lineCount: number }>(
53
+ blocks: readonly B[],
54
+ lineIndex: number,
55
+ typeFilter?: string,
56
+ ): B | null {
57
+ let nearest: B | null = null;
58
+ let nearestDist = Infinity;
59
+ for (const block of blocks) {
60
+ if (typeFilter !== undefined && block.type !== typeFilter) continue;
61
+ if (lineIndex >= block.startLine && lineIndex < block.startLine + block.lineCount) return block;
62
+ const dist = Math.abs(block.startLine - lineIndex);
63
+ if (dist < nearestDist) {
64
+ nearestDist = dist;
65
+ nearest = block;
66
+ }
67
+ }
68
+ return nearest;
69
+ }