@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
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Two pieces of state, kept out of ConversationManager (core/conversation.ts,
6
6
  * already at its line-count gate) and out of `suppressSplash`, which is a
7
- * per-frame posture the panel workspace sets both ways on every render:
7
+ * per-frame posture the modal layer sets both ways on every render:
8
8
  *
9
9
  * - `dismissed`, sticky for the run. Owner rule: the splash yields to ANY
10
10
  * submission, a chat message or a slash command alike. A slash command
@@ -37,6 +37,11 @@ export class SplashGateState {
37
37
  return true;
38
38
  }
39
39
 
40
+ /** True while the last rebuild drew the splash (the shell centers it). */
41
+ get showing(): boolean {
42
+ return this.onScreen;
43
+ }
44
+
40
45
  /** Record that this rebuild drew the splash. */
41
46
  enter(): void {
42
47
  this.onScreen = true;
@@ -47,4 +47,39 @@ export type BlockMeta = Omit<SdkBlockMeta, 'type'> & {
47
47
  * standalone tool results with no recorded name.
48
48
  */
49
49
  toolName?: string;
50
+ /**
51
+ * The collapse key that reveals each entry of groupMemberIndexes (same
52
+ * order): the bead body the result sits behind (work-tree-model.ts
53
+ * beadKeyOf). /expand opens them in the same pass that unfolds the turn,
54
+ * and search reveals a hit by opening exactly its bead.
55
+ */
56
+ groupMemberKeys?: readonly string[];
57
+ /**
58
+ * The work-tree row this block is, when it is one: 'turn' (a turn header),
59
+ * 'bead' (a tool call) or 'lane' (an agent lane's spawn or folded row). The
60
+ * keyboard moves between these rows (see ConversationManager focus methods).
61
+ */
62
+ workTree?: {
63
+ readonly kind: 'turn' | 'bead' | 'lane';
64
+ /** Focus identity, stable across rebuilds. */
65
+ readonly id: string;
66
+ /** The bead has a body to open (then ▸ shows). */
67
+ readonly hasBody: boolean;
68
+ /** The body is open. */
69
+ readonly open: boolean;
70
+ /** The body shows its first rows only ("… N more"). */
71
+ readonly capped: boolean;
72
+ /** Collapse key of the body's "… N more" (bead only). */
73
+ readonly moreKey?: string;
74
+ /** Whether the lane has finished (lane only). */
75
+ readonly finished?: boolean;
76
+ /** The lane this row sits on (bead rows inside an agent lane carry that lane's collapse key). */
77
+ readonly laneKey?: string;
78
+ /** The agent a lane row draws (Enter opens it full screen). */
79
+ readonly agentId?: string;
80
+ /** A lane row's color index (lane-graph/paint.ts laneColor): the agent's own color. */
81
+ readonly colorIndex?: number;
82
+ /** The background process a ▶ bead started (Enter opens its output). */
83
+ readonly processId?: string;
84
+ };
50
85
  };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * conversation-usage.ts, usage totals folded from a transcript's assistant
3
+ * messages (re-exported by conversation.ts).
4
+ *
5
+ * The fold is the SDK's (platform/core sumConversationUsage): a resumed
6
+ * session hydrates its token counters and its context figure from it. The
7
+ * context figure is the latest real turn request's; a follow-up
8
+ * acknowledgement (sent without tool definitions) counts toward the totals
9
+ * only, so the meter never drops to that smaller request's size.
10
+ */
11
+
12
+ export { sumConversationUsage } from '@pellux/goodvibes-sdk/platform/core';
@@ -12,22 +12,25 @@ import {
12
12
  type BlockMeta as SdkBlockMeta,
13
13
  } from '@pellux/goodvibes-sdk/platform/core';
14
14
  import type { BlockMeta } from './conversation-types.ts';
15
+ import type { NoticeSink } from './notices.ts';
15
16
  import { MessageLineCache } from './conversation-line-cache.ts';
16
- import { UserReceiptIndices } from './conversation-user-receipts.ts';
17
17
  import { SplashGateState } from './conversation-splash-state.ts';
18
- import { resolveTranscriptEventLine } from './conversation-event-navigation.ts';
18
+ import { nearestBlock, resolveTranscriptEventLine, wrapToLine } from './conversation-event-navigation.ts';
19
+ import { DisplayOnlyOutput } from './conversation-display-only.ts';
19
20
  import { SearchExpansionTracker } from './conversation-search-expansion.ts';
20
21
  import {
21
22
  addConversationSplashScreen,
22
23
  conversationTextToLines,
23
24
  logConversationText,
24
25
  logConversationToolResult,
25
- renderConversationAssistantMessage,
26
- renderConversationSystemMessage,
27
- renderConversationToolMessage,
28
- renderConversationUserMessage,
29
26
  } from './conversation-rendering.ts';
30
27
  import { renderMarkdown } from '../renderer/markdown.ts';
28
+ import type { WorkTreeSources } from './work-tree-sources.ts';
29
+ import { renderStreamingContinuation } from './work-tree-render.ts';
30
+ import { WorkTreeController } from './work-tree-focus.ts';
31
+ import { isWorkTreeFoldKey } from './work-tree-fold-store.ts';
32
+ import { resolveTreeGlyphSet, type TreeGlyphSetName } from '../renderer/lane-graph/glyphs.ts';
33
+ import { probeUnicodeSupport } from '../renderer/term-caps.ts';
31
34
 
32
35
  /**
33
36
  * ConversationManager - TUI subclass of the SDK's ConversationManager.
@@ -62,39 +65,12 @@ type Message = ConversationMessageSnapshot;
62
65
  import type { OrchestratorUsageTotals } from '@pellux/goodvibes-sdk/platform/core';
63
66
  export type { OrchestratorUsageTotals };
64
67
 
65
- /**
66
- * sumConversationUsage - Fold every assistant message's per-turn usage into
67
- * running totals, plus the *last* assistant message's own input-token figure
68
- * (context-window occupancy, not a running sum).
69
- *
70
- * TUI-side counterpart to SDK Orchestrator.usage: after a session resume
71
- * replays historical messages into a freshly-constructed Orchestrator (whose
72
- * `usage` starts at all zeros, bootstrap.ts always constructs a fresh
73
- * instance, see), this lets the caller hydrate the footer's token
74
- * counters from messages that already carry real usage data instead of
75
- * waiting for the first new turn to populate them.
76
- */
77
- export function sumConversationUsage(
78
- messages: readonly ConversationMessageSnapshot[],
79
- ): { usage: OrchestratorUsageTotals; lastInputTokens: number } {
80
- const usage: OrchestratorUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
81
- let lastInputTokens = 0;
82
- for (const message of messages) {
83
- if (message.role !== 'assistant' || !message.usage) continue;
84
- usage.input += message.usage.inputTokens;
85
- usage.output += message.usage.outputTokens;
86
- usage.cacheRead += message.usage.cacheReadTokens ?? 0;
87
- usage.cacheWrite += message.usage.cacheWriteTokens ?? 0;
88
- // Context-window occupancy tracks the most recent turn only, not a sum.
89
- lastInputTokens = message.usage.inputTokens
90
- + (message.usage.cacheReadTokens ?? 0)
91
- + (message.usage.cacheWriteTokens ?? 0);
92
- }
93
- return { usage, lastInputTokens };
94
- }
68
+ export { sumConversationUsage } from './conversation-usage.ts';
95
69
 
96
70
  export class ConversationManager extends SdkConversationManager {
97
71
  public history = new InfiniteBuffer();
72
+ /** Display-only output kept for redraw (conversation-display-only.ts). */
73
+ private readonly displayOnly = new DisplayOnlyOutput();
98
74
  private _getWidth: () => number;
99
75
  /** Tracks the rendered width; a change invalidates the full history. */
100
76
  private lastRenderedWidth = 0;
@@ -143,8 +119,6 @@ export class ConversationManager extends SdkConversationManager {
143
119
  private errorLineRegistry: number[] = [];
144
120
  /** Maps message index → SystemMessageKind for system-role messages. */
145
121
  private messageKindRegistry: Map<number, SystemMessageKind> = new Map();
146
- /** See conversation-user-receipts.ts. */
147
- private userReceiptIndices = new UserReceiptIndices();
148
122
  /** Streaming block start line in history buffer (for incremental streaming update). */
149
123
  private streamingStartLine = -1;
150
124
  /**
@@ -169,9 +143,24 @@ export class ConversationManager extends SdkConversationManager {
169
143
  private _displayFromMessageIndex = 0;
170
144
 
171
145
  public suppressSplash: boolean = false;
146
+ private noticeSink: NoticeSink | null = null;
172
147
  public splashOptions: SplashOptions = {};
173
148
  /** Run-sticky dismissal + the splash→transcript edge (conversation-splash-state.ts). */
174
149
  private readonly splashGate = new SplashGateState();
150
+ /** Live facts the work tree reads (timings, agent lanes, waiting calls); see work-tree-sources.ts. */
151
+ private workTreeSources: WorkTreeSources = {};
152
+ /** Turn activity at the last build: a change forces a rebuild (a finished turn's header must drop "working"). */
153
+ private builtWhileActive = false;
154
+ /** Whether the terminal draws unicode (term-caps.ts); ascii work-tree glyphs when not. */
155
+ private unicodeCapable = probeUnicodeSupport();
156
+ /** Keyboard focus, live repaint and fold state of the work tree (work-tree-focus.ts). */
157
+ public readonly workTree: WorkTreeController = new WorkTreeController({
158
+ blocks: () => { this.flushHistory(); return this.blockRegistry; },
159
+ collapseState: () => this.collapseState,
160
+ markDirty: () => this.markDirty(),
161
+ noteUserTouch: (key) => this.searchExpansion.noteUserTouch(key),
162
+ drewLive: () => this.lineCache.live,
163
+ });
175
164
 
176
165
  constructor(
177
166
  getWidth: () => number = () => process.stdout.columns || 80,
@@ -229,23 +218,27 @@ export class ConversationManager extends SdkConversationManager {
229
218
  // stale kind (e.g. 'operational') and silently mis-classify the new message.
230
219
  const nextIndex = this.getMessageSnapshot().length;
231
220
  this.messageKindRegistry.delete(nextIndex);
232
- this.userReceiptIndices.delete(nextIndex);
233
221
  super.addSystemMessage(content);
234
222
  this.markDirty();
223
+ this.noticeSink?.(content, { restored: false });
224
+ }
225
+
226
+ /** Where system notices go (core/notices.ts); the transcript draws none. Absent in bare test conversations. */
227
+ public setNoticeSink(sink: NoticeSink | null): void {
228
+ this.noticeSink = sink;
235
229
  }
236
230
 
237
231
  /**
238
232
  * addTypedSystemMessage - System message with an explicit kind tag, stored
239
- * in messageKindRegistry. `isUserReceipt` (conversation-user-receipts.ts)
240
- * additionally marks it for rebuildHistory()'s splash check, only for a
241
- * direct receipt to an explicit user action, never ambient boot chatter.
233
+ * in messageKindRegistry. Like every system message it is a notice: the
234
+ * notice sink shows it, the transcript does not.
242
235
  */
243
- public addTypedSystemMessage(content: string, kind: SystemMessageKind, opts?: { isUserReceipt?: boolean }): void {
236
+ public addTypedSystemMessage(content: string, kind: SystemMessageKind): void {
244
237
  const nextIndex = this.getMessageSnapshot().length;
245
238
  this.messageKindRegistry.set(nextIndex, kind);
246
- if (opts?.isUserReceipt) this.userReceiptIndices.add(nextIndex); else this.userReceiptIndices.delete(nextIndex);
247
239
  super.addSystemMessage(content);
248
240
  this.markDirty();
241
+ this.noticeSink?.(content, { restored: false });
249
242
  }
250
243
 
251
244
  public override undo(): boolean {
@@ -260,7 +253,6 @@ export class ConversationManager extends SdkConversationManager {
260
253
  for (const key of this.messageKindRegistry.keys()) {
261
254
  if (key >= postUndoCount) this.messageKindRegistry.delete(key);
262
255
  }
263
- this.userReceiptIndices.purgeFrom(postUndoCount);
264
256
  this.markDirty();
265
257
  }
266
258
  return result;
@@ -295,8 +287,13 @@ export class ConversationManager extends SdkConversationManager {
295
287
  this.markDirty();
296
288
  // Record the line where the streaming block starts so updates can be incremental.
297
289
  // Reset the render throttle so the first delta always renders immediately.
290
+ // The rebuild already treats the placeholder as streaming (its turn ends
291
+ // without a blank row, the text continues under its spine), the same as a
292
+ // rebuild later in the stream does.
293
+ this.streamingStartLine = 0;
298
294
  this.flushHistory();
299
- this.streamingStartLine = this.history.getLineCount();
295
+ // The streamed text starts above any display-only draws kept below it.
296
+ this.streamingStartLine = this.history.getLineCount() - this.displayOnly.trailingLineCount;
300
297
  this._streamWidth = this._getWidth();
301
298
  this._lastStreamRenderMs = 0;
302
299
  }
@@ -324,8 +321,8 @@ export class ConversationManager extends SdkConversationManager {
324
321
  this._lastStreamRenderMs = now;
325
322
  const width = this._getWidth();
326
323
  this.history.truncateToLine(this.streamingStartLine);
327
- const rendered = renderMarkdown(content, width, { isStreaming: true });
328
- this.history.addLines(rendered);
324
+ this.history.addLines(this.streamingLines(content, width));
325
+ this.displayOnly.drawTrailing(width, () => this.history.getLineCount());
329
326
  }
330
327
  }
331
328
  }
@@ -337,6 +334,7 @@ export class ConversationManager extends SdkConversationManager {
337
334
  public override finalizeStreamingBlock(): void {
338
335
  super.finalizeStreamingBlock();
339
336
  this.streamingStartLine = -1;
337
+ this.displayOnly.resetTrailing();
340
338
  this._streamWidth = -1;
341
339
  this._lastStreamRenderMs = 0;
342
340
  this.markDirty();
@@ -362,18 +360,13 @@ export class ConversationManager extends SdkConversationManager {
362
360
  this.messageLineRegistry = [];
363
361
  this.errorLineRegistry = [];
364
362
  this.messageKindRegistry = new Map();
365
- this.userReceiptIndices.clear();
366
363
  this.streamingStartLine = -1;
364
+ this.displayOnly.drop();
365
+ this.workTree.reset();
367
366
  this._displayFromMessageIndex = 0; // full reset, show everything on next render
368
367
  }
369
368
 
370
- /**
371
- * replaceMessagesForLLM - Replace the conversation's LLM-visible messages with a new set.
372
- * Used by small-window compaction to swap in truncated messages without an LLM call.
373
- * System messages are always preserved at the front.
374
- *
375
- * @param newMessages - Replacement ProviderMessage array (user/assistant/tool roles only)
376
- */
369
+ /** Compaction's replace: the SDK keeps every kept message whole (tool calls, model, reasoning); the display rebuilds. */
377
370
  public override replaceMessagesForLLM(newMessages: ProviderMessage[]): void {
378
371
  super.replaceMessagesForLLM(newMessages);
379
372
  this.history.clear();
@@ -415,6 +408,10 @@ export class ConversationManager extends SdkConversationManager {
415
408
  titleSource?: import('@pellux/goodvibes-sdk/platform/core').ConversationTitleSource;
416
409
  }): void {
417
410
  super.fromJSON(data);
411
+ // A restored session's notices go back into the history (not toasted).
412
+ for (const message of data.messages) if (message.role === 'system') this.noticeSink?.(message.content, { restored: true });
413
+ // Output printed over the conversation this one replaces is not about it.
414
+ this.displayOnly.drop();
418
415
  this.history.clear();
419
416
  this.lineCache.clear();
420
417
  this.appendedUpTo = 0;
@@ -466,6 +463,8 @@ export class ConversationManager extends SdkConversationManager {
466
463
  const width = this._getWidth();
467
464
  this.lastRenderedWidth = width;
468
465
  this.dirty = false;
466
+ this.builtWhileActive = this.workTreeSources.turnActive?.() ?? false;
467
+ this.displayOnly.resetTrailing();
469
468
 
470
469
  const snapshot = this.getMessageSnapshot();
471
470
  // During streaming, the in-progress placeholder (always the last message) is
@@ -484,14 +483,9 @@ export class ConversationManager extends SdkConversationManager {
484
483
  const displayStart = this._displayFromMessageIndex;
485
484
  const visibleSnapshot = displayStart > 0 ? renderSnapshot.slice(displayStart) : renderSnapshot;
486
485
 
487
- // Tool/system messages aren't visible splash-purposes content, except a
488
- // system message in userReceiptIndices. visibleSnapshot mirrors the
489
- // snapshot 1:1 (only front-sliced), so index i is absolute displayStart+i.
490
- const displayMessages = visibleSnapshot.filter((m, i) => {
491
- if (m.role === 'tool') return false;
492
- if (m.role === 'system') return this.userReceiptIndices.has(displayStart + i);
493
- return true;
494
- });
486
+ // Tool and system messages are not transcript content: a system message is
487
+ // a notice (a toast and the notification history, see core/notices.ts).
488
+ const displayMessages = visibleSnapshot.filter((m) => m.role !== 'tool' && m.role !== 'system');
495
489
 
496
490
  if (displayMessages.length === 0 && displayStart === 0 && !this.suppressSplash && !this.splashGate.dismissed) {
497
491
  this.addSplashScreen(width);
@@ -504,8 +498,14 @@ export class ConversationManager extends SdkConversationManager {
504
498
  // renderSnapshot) is left uncached: its content mutates in place per delta and
505
499
  // the incremental streaming path (updateStreamingBlock) owns it.
506
500
  const streamingPlaceholderAbsIdx = isStreaming ? snapshot.length - 1 : -1;
507
- this.appendMessages(visibleSnapshot, width, displayStart, streamingPlaceholderAbsIdx);
501
+ // Display-only draws go back where they were printed: after the unit that
502
+ // holds the last message that existed then. The turn still streaming is
503
+ // last and its text continues below it, so what follows it waits.
504
+ const kept = this.displayOnly.cursor(width);
505
+ kept.through(displayStart - 1);
506
+ this.appendMessages(visibleSnapshot, width, displayStart, streamingPlaceholderAbsIdx, (last, streaming) => { if (!streaming) kept.through(last); });
508
507
  this.appendedUpTo = snapshot.length;
508
+ if (!isStreaming) kept.through(Number.POSITIVE_INFINITY);
509
509
 
510
510
  if (isStreaming) {
511
511
  // Re-anchor the streaming block to the freshly rebuilt buffer and re-render
@@ -515,17 +515,31 @@ export class ConversationManager extends SdkConversationManager {
515
515
  this._lastStreamRenderMs = 0;
516
516
  const streamingContent = lastMsg?.content;
517
517
  if (typeof streamingContent === 'string' && streamingContent.length > 0) {
518
- this.history.addLines(renderMarkdown(streamingContent, width, { isStreaming: true }));
518
+ this.history.addLines(this.streamingLines(streamingContent, width));
519
519
  }
520
+ kept.trailStreamed(() => this.history.getLineCount()); // printed this turn: below the streamed text
520
521
  }
521
522
  }
522
523
 
524
+ /** Keep a display-only draw and draw it now (below streamed text while streaming; over the splash, it retires the splash). */
525
+ private recordDisplayOnly(draw: (width: number) => void): void {
526
+ const seq = this.displayOnly.keep(this.getMessageSnapshot().length, draw);
527
+ if (this.splashGate.showing && this.splashGate.dismiss()) return this.rebuildHistory();
528
+ const before = this.history.getLineCount();
529
+ draw(this._getWidth());
530
+ this.displayOnly.noteDrawn(seq, this.history.getLineCount() - before, this.streamingStartLine >= 0);
531
+ }
532
+
533
+ /** Display-only draws kept for redraw (bounded; for memory-hygiene assertions). */
534
+ public getDisplayOnlyCount(): number { return this.displayOnly.count; }
535
+
523
536
  /**
524
537
  * flushHistory - Incremental update. Appends only newly added messages.
525
538
  * Falls back to a full rebuild when the terminal width has changed.
526
539
  */
527
540
  public flushHistory(): void {
528
541
  const currentWidth = this._getWidth();
542
+ if ((this.workTreeSources.turnActive?.() ?? false) !== this.builtWhileActive) this.dirty = true;
529
543
  if (!this.dirty && currentWidth === this.lastRenderedWidth) return;
530
544
  this.rebuildHistory();
531
545
  }
@@ -543,29 +557,24 @@ export class ConversationManager extends SdkConversationManager {
543
557
  messageKindRegistry: this.messageKindRegistry as ReadonlyMap<number, SystemMessageKind>,
544
558
  configManager: this._configManager,
545
559
  splashOptions: this.splashOptions,
560
+ systemNotices: 'elsewhere' as const,
561
+ workTreeSources: this.workTreeSources,
562
+ treeGlyphSet: this.treeGlyphSet(),
563
+ focusId: this.workTree.focus,
564
+ frame: this.workTree.frame,
546
565
  };
547
566
  }
548
567
 
549
- private renderUserMessage(message: Extract<Message, { role: 'user' }>, width: number): void {
550
- renderConversationUserMessage(this.renderingContext(), message, width);
551
- }
552
-
553
- private renderAssistantMessage(
554
- message: Extract<Message, { role: 'assistant' }>,
555
- width: number,
556
- lineNumberMode: 'all' | 'code' | 'off',
557
- collapseThreshold: number,
558
- msgIdx: number,
559
- ): void {
560
- renderConversationAssistantMessage(this.renderingContext(), message, width, lineNumberMode, collapseThreshold, msgIdx);
561
- }
562
-
563
- private renderSystemMessage(message: Extract<Message, { role: 'system' }>, width: number, msgIdx: number): void {
564
- renderConversationSystemMessage(this.renderingContext(), message, width, msgIdx);
568
+ /** The work tree's glyph set: display.treeGlyphs, ascii on a terminal without unicode. */
569
+ private treeGlyphSet(): TreeGlyphSetName {
570
+ return resolveTreeGlyphSet(this._configManager?.get('display.treeGlyphs'), this.unicodeCapable);
565
571
  }
566
572
 
567
- private renderToolMessage(message: Extract<Message, { role: 'tool' }>, width: number, msgIdx: number): void {
568
- renderConversationToolMessage(this.renderingContext(), message, width, msgIdx);
573
+ /** Streaming text: under the spine of the turn it belongs to when there is one, else full width. */
574
+ private streamingLines(content: string, width: number): Line[] {
575
+ const tail = this.lineCache.lastTail;
576
+ if (!tail) return renderMarkdown(content, width, { isStreaming: true });
577
+ return renderStreamingContinuation(content, width, tail, this.treeGlyphSet());
569
578
  }
570
579
 
571
580
  /**
@@ -581,6 +590,7 @@ export class ConversationManager extends SdkConversationManager {
581
590
  width: number,
582
591
  msgIndexOffset = 0,
583
592
  streamingPlaceholderAbsIdx = -1,
593
+ afterUnit?: (lastIndex: number, streaming: boolean) => void,
584
594
  ): void {
585
595
  this.lineCache.renderInto(
586
596
  this.renderingContext(),
@@ -589,34 +599,35 @@ export class ConversationManager extends SdkConversationManager {
589
599
  this.messageLineRegistry,
590
600
  msgIndexOffset,
591
601
  streamingPlaceholderAbsIdx,
602
+ afterUnit,
592
603
  );
593
604
  }
594
605
 
606
+ // -------------------------------------------------------------------------
607
+ // Work tree (see work-tree-model.ts / work-tree-focus.ts)
608
+ // -------------------------------------------------------------------------
609
+
610
+ /** Live facts the work tree draws: timings, agent lanes, the call a prompt is holding. */
611
+ public setWorkTreeSources(sources: WorkTreeSources): void { this.workTreeSources = sources; this.markDirty(); }
612
+ // An agent view (core/agent-view-render.ts) draws with main's sources, glyphs and the agent's lane color.
613
+ public getWorkTreeSources(): WorkTreeSources { return this.workTreeSources; }
614
+ public getTreeGlyphSet(): TreeGlyphSetName { return this.treeGlyphSet(); }
615
+ public laneColorOf(agentId: string): number | undefined { return this.getBlockRegistry().find((b) => b.workTree?.kind === 'lane' && b.workTree.agentId === agentId)?.workTree?.colorIndex; }
616
+ /** Override the unicode probe (tests, and a terminal the caller knows better). */
617
+ public setUnicodeCapable(capable: boolean): void { this.unicodeCapable = capable; this.markDirty(); }
595
618
  /** Find the nearest block to a given line index, optionally filtered by type. */
596
- public findNearestBlock(lineIndex: number, typeFilter?: string): BlockMeta | null {
597
- let nearest: BlockMeta | null = null;
598
- let nearestDist = Infinity;
599
- for (const block of this.blockRegistry) {
600
- if (typeFilter !== undefined && block.type !== typeFilter) continue;
601
- if (lineIndex >= block.startLine && lineIndex < block.startLine + block.lineCount) {
602
- return block;
603
- }
604
- const dist = Math.abs(block.startLine - lineIndex);
605
- if (dist < nearestDist) {
606
- nearestDist = dist;
607
- nearest = block;
608
- }
609
- }
610
- return nearest;
611
- }
619
+ public findNearestBlock(lineIndex: number, typeFilter?: string): BlockMeta | null { return nearestBlock(this.blockRegistry, lineIndex, typeFilter); }
612
620
 
613
- /**
614
- * isCollapsed - Returns whether the block at blockIndex is collapsed.
615
- */
621
+ /** isCollapsed - Returns whether the block at blockIndex is collapsed. */
616
622
  public isCollapsed(blockIndex: number): boolean {
617
623
  const block = this.blockRegistry[blockIndex];
618
- if (!block) return false;
619
- return this.collapseState.get(block.collapseKey) ?? false;
624
+ return block ? this.collapsedNow(block) : false;
625
+ }
626
+
627
+ /** Current collapse of a block: an unset bead key is a closed bead; everything else defaults open. */
628
+ private collapsedNow(block: BlockMeta): boolean {
629
+ const stored = this.collapseState.get(block.collapseKey);
630
+ return block.workTree?.kind === 'bead' ? stored !== false : stored ?? false;
620
631
  }
621
632
 
622
633
  /**
@@ -634,11 +645,7 @@ export class ConversationManager extends SdkConversationManager {
634
645
  public getDiffAtLine(lineIndex: number): { filePath: string; original: string; updated: string } | null {
635
646
  const nearest = this.findNearestBlock(lineIndex, 'diff');
636
647
  if (!nearest || !nearest.filePath) return null;
637
- return {
638
- filePath: nearest.filePath,
639
- original: nearest.diffOriginal ?? '',
640
- updated: nearest.diffUpdated ?? '',
641
- };
648
+ return { filePath: nearest.filePath, original: nearest.diffOriginal ?? '', updated: nearest.diffUpdated ?? '' };
642
649
  }
643
650
 
644
651
  /**
@@ -648,10 +655,11 @@ export class ConversationManager extends SdkConversationManager {
648
655
  public toggleCollapseAtLine(lineIndex: number): number {
649
656
  const nearest = this.findNearestBlock(lineIndex);
650
657
  if (!nearest) return -1;
651
- const current = this.collapseState.get(nearest.collapseKey) ?? false;
658
+ const current = this.collapsedNow(nearest);
652
659
  this.collapseState.set(nearest.collapseKey, !current);
653
660
  this.searchExpansion.noteUserTouch(nearest.collapseKey);
654
661
  this.markDirty();
662
+ if (isWorkTreeFoldKey(nearest.collapseKey)) this.workTree.notifyFoldChange();
655
663
  return nearest.blockIndex;
656
664
  }
657
665
 
@@ -692,23 +700,13 @@ export class ConversationManager extends SdkConversationManager {
692
700
  * nextErrorLine - Find the next error line after currentLine (wraps around).
693
701
  * Returns -1 if there are no error lines.
694
702
  */
695
- public nextErrorLine(currentLine: number): number {
696
- const errors = this.getErrorLines();
697
- if (errors.length === 0) return -1;
698
- const after = errors.find(l => l > currentLine);
699
- return after ?? errors[0];
700
- }
703
+ public nextErrorLine(currentLine: number): number { return wrapToLine(this.getErrorLines(), currentLine, 'next'); }
701
704
 
702
705
  /**
703
706
  * prevErrorLine - Find the previous error line before currentLine (wraps around).
704
707
  * Returns -1 if there are no error lines.
705
708
  */
706
- public prevErrorLine(currentLine: number): number {
707
- const errors = this.getErrorLines();
708
- if (errors.length === 0) return -1;
709
- const before = [...errors].reverse().find(l => l < currentLine);
710
- return before ?? errors[errors.length - 1];
711
- }
709
+ public prevErrorLine(currentLine: number): number { return wrapToLine(this.getErrorLines(), currentLine, 'prev'); }
712
710
 
713
711
  public nextTranscriptEventLine(currentLine: number, kind: TranscriptEventKind | 'all' = 'all'): number {
714
712
  this.flushHistory();
@@ -727,14 +725,13 @@ export class ConversationManager extends SdkConversationManager {
727
725
  }
728
726
 
729
727
  /** Retire the splash for the run, any submission does it (SplashGateState). */
730
- public dismissSplash(): void {
731
- if (this.splashGate.dismiss()) this.markDirty();
732
- }
728
+ /** Retire the splash; rebuilt now (not marked dirty) so a command's output logged in the same tick survives the next frame. */
729
+ public dismissSplash(): void { if (this.splashGate.dismiss() && this.splashGate.showing) this.rebuildHistory(); }
733
730
 
731
+ /** True while the conversation area is showing the splash (the shell centers it). */
732
+ public isSplashShowing(): boolean { return this.splashGate.showing; }
734
733
  /** True once, on the frame the splash gives way to transcript content. */
735
- public consumeSplashTransition(): boolean {
736
- return this.splashGate.consumeTransition();
737
- }
734
+ public consumeSplashTransition(): boolean { return this.splashGate.consumeTransition(); }
738
735
 
739
736
  private addSplashScreen(width: number): void {
740
737
  addConversationSplashScreen(this.renderingContext(), width);
@@ -744,8 +741,9 @@ export class ConversationManager extends SdkConversationManager {
744
741
  return conversationTextToLines(text, width, style);
745
742
  }
746
743
 
744
+ /** Print display-only text into the transcript (never a message); it survives every rebuild. */
747
745
  public log(text: string, style: Partial<Cell> = {}, indent = ' '): void {
748
- logConversationText(this.renderingContext(), this._getWidth(), text, style, indent);
746
+ this.recordDisplayOnly((width) => logConversationText(this.renderingContext(), width, text, style, indent));
749
747
  }
750
748
 
751
749
  /**
@@ -761,7 +759,7 @@ export class ConversationManager extends SdkConversationManager {
761
759
  durationMs: number,
762
760
  errorMsg?: string,
763
761
  ): void {
764
- logConversationToolResult(this.renderingContext(), this._getWidth(), toolCall, status, resultSummary, durationMs, errorMsg);
762
+ this.recordDisplayOnly((width) => logConversationToolResult(this.renderingContext(), width, toolCall, status, resultSummary, durationMs, errorMsg));
765
763
  this.markDirty();
766
764
  }
767
765
 
@@ -778,6 +776,7 @@ export class ConversationManager extends SdkConversationManager {
778
776
  */
779
777
  public clearDisplay(): void {
780
778
  this.history.clear();
779
+ this.displayOnly.drop();
781
780
  this.blockRegistry = [];
782
781
  this.messageLineRegistry = [];
783
782
  this.errorLineRegistry = [];