@pellux/goodvibes-tui 2.0.21 → 2.1.1

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 (391) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -29
  3. package/docs/README.md +8 -5
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +28 -19
  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-reference.ts +2 -2
  52. package/src/input/command-registry.ts +77 -35
  53. package/src/input/commands/acp-runtime.ts +19 -9
  54. package/src/input/commands/channel-runtime.ts +2 -2
  55. package/src/input/commands/ci-runtime.ts +1 -1
  56. package/src/input/commands/codebase-runtime.ts +1 -1
  57. package/src/input/commands/config.ts +7 -1
  58. package/src/input/commands/context-window.ts +25 -19
  59. package/src/input/commands/control-room-runtime.ts +18 -18
  60. package/src/input/commands/conversation-runtime.ts +0 -1
  61. package/src/input/commands/cost-runtime.ts +29 -20
  62. package/src/input/commands/diff-runtime.ts +35 -181
  63. package/src/input/commands/eval.ts +1 -1
  64. package/src/input/commands/experience-runtime.ts +3 -7
  65. package/src/input/commands/fresh-conversation.ts +86 -0
  66. package/src/input/commands/git-runtime.ts +5 -22
  67. package/src/input/commands/graph-runtime.ts +1 -1
  68. package/src/input/commands/guidance-runtime.ts +1 -1
  69. package/src/input/commands/health-runtime.ts +8 -9
  70. package/src/input/commands/hooks-runtime.ts +2 -2
  71. package/src/input/commands/hosted-runtime.ts +3 -3
  72. package/src/input/commands/incident-runtime.ts +3 -3
  73. package/src/input/commands/intelligence-runtime.ts +6 -9
  74. package/src/input/commands/knowledge.ts +2 -2
  75. package/src/input/commands/legacy-panel-command.ts +57 -0
  76. package/src/input/commands/local-auth-runtime.ts +8 -9
  77. package/src/input/commands/local-provider-runtime.ts +7 -4
  78. package/src/input/commands/local-runtime.ts +12 -15
  79. package/src/input/commands/local-setup-review.ts +1 -1
  80. package/src/input/commands/local-setup.ts +13 -19
  81. package/src/input/commands/memory.ts +1 -1
  82. package/src/input/commands/notify-runtime.ts +9 -0
  83. package/src/input/commands/operator-runtime.ts +10 -12
  84. package/src/input/commands/permissions-provenance.ts +3 -3
  85. package/src/input/commands/planning-runtime.ts +7 -7
  86. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  87. package/src/input/commands/platform-services-runtime.ts +1 -1
  88. package/src/input/commands/policy-dispatch.ts +2 -2
  89. package/src/input/commands/policy.ts +3 -3
  90. package/src/input/commands/provider.ts +1 -1
  91. package/src/input/commands/remote-runtime.ts +1 -1
  92. package/src/input/commands/review-runtime.ts +39 -94
  93. package/src/input/commands/rewind-runtime.ts +81 -108
  94. package/src/input/commands/runtime-services.ts +15 -17
  95. package/src/input/commands/services-runtime.ts +2 -2
  96. package/src/input/commands/session-content.ts +21 -5
  97. package/src/input/commands/session-workflow.ts +13 -38
  98. package/src/input/commands/settings-sync-runtime.ts +2 -2
  99. package/src/input/commands/shell-core.ts +19 -4
  100. package/src/input/commands/skills-runtime.ts +2 -2
  101. package/src/input/commands/status-runtime.ts +13 -0
  102. package/src/input/commands/subscription-runtime.ts +1 -1
  103. package/src/input/commands/tasks-runtime.ts +2 -8
  104. package/src/input/commands/work-plan-runtime.ts +4 -4
  105. package/src/input/commands/workstream-attempts.ts +24 -47
  106. package/src/input/commands/workstream-runtime.ts +2 -2
  107. package/src/input/commands/worktree-runtime.ts +3 -3
  108. package/src/input/commands.ts +3 -1
  109. package/src/input/composer-editor.ts +44 -0
  110. package/src/input/concealed-input.ts +1 -1
  111. package/src/input/config-modal-types.ts +20 -8
  112. package/src/input/config-modal.ts +106 -105
  113. package/src/input/confirm-dialog-types.ts +28 -0
  114. package/src/input/confirm-dialog.ts +70 -0
  115. package/src/input/delete-key-policy.ts +3 -3
  116. package/src/input/feed-context-factory.ts +15 -16
  117. package/src/input/handler-command-route.ts +7 -42
  118. package/src/input/handler-content-actions.ts +16 -15
  119. package/src/input/handler-feed-routes.ts +26 -211
  120. package/src/input/handler-feed.ts +68 -95
  121. package/src/input/handler-interactions.ts +5 -3
  122. package/src/input/handler-modal-routes.ts +215 -216
  123. package/src/input/handler-modal-stack.ts +81 -11
  124. package/src/input/handler-modal-token-routes.ts +4 -0
  125. package/src/input/handler-onboarding.ts +0 -1
  126. package/src/input/handler-picker-routes.ts +65 -107
  127. package/src/input/handler-session-view-route.ts +149 -0
  128. package/src/input/handler-shortcuts.ts +16 -154
  129. package/src/input/handler-types.ts +4 -2
  130. package/src/input/handler-ui-state.ts +45 -15
  131. package/src/input/handler-work-tree-route.ts +123 -0
  132. package/src/input/handler.ts +25 -70
  133. package/src/input/keybindings.ts +16 -71
  134. package/src/input/masked-entry-modal.ts +117 -0
  135. package/src/input/mcp-workspace.ts +48 -17
  136. package/src/input/model-picker-filter.ts +8 -0
  137. package/src/input/model-picker-open.ts +99 -0
  138. package/src/input/model-picker-types.ts +1 -1
  139. package/src/input/model-picker.ts +92 -9
  140. package/src/input/notifications-modal.ts +123 -0
  141. package/src/input/overlay-filter.ts +26 -0
  142. package/src/input/profile-picker-modal.ts +37 -8
  143. package/src/input/search.ts +3 -3
  144. package/src/input/selection-modal-queue.ts +2 -0
  145. package/src/input/selection-modal.ts +28 -16
  146. package/src/input/session-picker-modal.ts +33 -8
  147. package/src/input/settings-modal-activation.ts +7 -2
  148. package/src/input/settings-modal-data.ts +11 -6
  149. package/src/input/settings-modal-types.ts +1 -1
  150. package/src/input/settings-modal.ts +20 -20
  151. package/src/input/submission-intent.ts +1 -1
  152. package/src/input/submission-router.ts +4 -9
  153. package/src/input/surface-modal-host.ts +110 -0
  154. package/src/input/theme-settings-actions.ts +82 -0
  155. package/src/input/usage-modal.ts +183 -0
  156. package/src/input/views.ts +97 -0
  157. package/src/main.ts +175 -165
  158. package/src/permissions/hunk-selection.ts +1 -1
  159. package/src/permissions/prompt-card.ts +511 -0
  160. package/src/permissions/prompt.ts +59 -622
  161. package/src/renderer/agents-modal.ts +310 -0
  162. package/src/renderer/autocomplete-overlay.ts +42 -220
  163. package/src/renderer/back-to-bottom.ts +80 -0
  164. package/src/renderer/block-actions-overlay.ts +30 -48
  165. package/src/renderer/bookmark-modal.ts +48 -79
  166. package/src/renderer/changes-modal.ts +325 -0
  167. package/src/renderer/code-block.ts +184 -106
  168. package/src/renderer/command-palette.ts +93 -0
  169. package/src/renderer/composer-fragments.ts +4 -4
  170. package/src/renderer/composer.ts +146 -0
  171. package/src/renderer/compositor.ts +26 -160
  172. package/src/renderer/config-modal.ts +185 -81
  173. package/src/renderer/confirm-dialog.ts +50 -0
  174. package/src/renderer/context-inspector.ts +116 -154
  175. package/src/renderer/context-status-hint.ts +4 -4
  176. package/src/renderer/conversation-layout.ts +20 -0
  177. package/src/renderer/conversation-overlays.ts +94 -84
  178. package/src/renderer/conversation-surface.ts +19 -38
  179. package/src/renderer/diff-tint.ts +98 -0
  180. package/src/renderer/file-picker-overlay.ts +63 -90
  181. package/src/renderer/footer-targets.ts +38 -0
  182. package/src/renderer/git-status.ts +3 -4
  183. package/src/renderer/header-line.ts +140 -117
  184. package/src/renderer/help-overlay.ts +133 -219
  185. package/src/renderer/history-search-overlay.ts +15 -12
  186. package/src/renderer/lane-graph/bead.ts +800 -0
  187. package/src/renderer/lane-graph/glyphs.ts +134 -0
  188. package/src/renderer/lane-graph/layout.ts +248 -0
  189. package/src/renderer/lane-graph/paint.ts +537 -0
  190. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  191. package/src/renderer/layout-engine.ts +2 -57
  192. package/src/renderer/layout.ts +7 -13
  193. package/src/renderer/markdown-inline.ts +2 -1
  194. package/src/renderer/markdown-table.ts +17 -12
  195. package/src/renderer/markdown.ts +92 -34
  196. package/src/renderer/masked-entry-modal.ts +78 -0
  197. package/src/renderer/mcp-workspace.ts +166 -203
  198. package/src/renderer/model-workspace.ts +381 -436
  199. package/src/renderer/notifications-modal.ts +116 -0
  200. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  201. package/src/renderer/process-view.ts +192 -0
  202. package/src/renderer/profile-picker-modal.ts +58 -110
  203. package/src/renderer/search-overlay.ts +6 -3
  204. package/src/renderer/selection-modal-overlay.ts +87 -327
  205. package/src/renderer/semantic-diff.ts +2 -2
  206. package/src/renderer/session-chips.ts +102 -0
  207. package/src/renderer/session-picker-modal.ts +177 -297
  208. package/src/renderer/settings-modal-connections.ts +14 -23
  209. package/src/renderer/settings-modal-context.ts +367 -0
  210. package/src/renderer/settings-modal-helpers.ts +6 -11
  211. package/src/renderer/settings-modal.ts +322 -726
  212. package/src/renderer/shell-surface.ts +277 -175
  213. package/src/renderer/shortcuts-overlay.ts +217 -0
  214. package/src/renderer/status-line.ts +475 -0
  215. package/src/renderer/surface-compose.ts +174 -0
  216. package/src/renderer/surface-kit-extra.ts +303 -0
  217. package/src/renderer/surface-kit-list.ts +366 -0
  218. package/src/renderer/surface-kit-parts.ts +360 -0
  219. package/src/renderer/surface-kit.ts +568 -0
  220. package/src/renderer/syntax-highlighter.ts +280 -197
  221. package/src/renderer/syntax-theme.ts +51 -0
  222. package/src/renderer/system-message.ts +0 -1
  223. package/src/renderer/term-caps.ts +42 -1
  224. package/src/renderer/terminal-bg-probe.ts +116 -19
  225. package/src/renderer/terminal-palette-probe.ts +322 -0
  226. package/src/renderer/terminal-palette-reader.ts +183 -0
  227. package/src/renderer/terminal-palette.ts +58 -0
  228. package/src/renderer/theme-mode-config.ts +16 -1
  229. package/src/renderer/theme.ts +234 -207
  230. package/src/renderer/thinking.ts +0 -1
  231. package/src/renderer/throbber.ts +188 -0
  232. package/src/renderer/toast-center.ts +119 -0
  233. package/src/renderer/ui-factory.ts +39 -602
  234. package/src/renderer/ui-primitives.ts +8 -25
  235. package/src/renderer/usage-modal.ts +353 -0
  236. package/src/renderer/user-message.ts +58 -0
  237. package/src/renderer/voice-capture-chip.ts +53 -0
  238. package/src/runtime/bootstrap-command-context.ts +0 -5
  239. package/src/runtime/bootstrap-command-parts.ts +38 -89
  240. package/src/runtime/bootstrap-core.ts +13 -15
  241. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  242. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  243. package/src/runtime/bootstrap-shell.ts +16 -50
  244. package/src/runtime/bootstrap.ts +14 -14
  245. package/src/runtime/client/fleet-union.ts +5 -5
  246. package/src/runtime/client/hosted-exit.ts +2 -2
  247. package/src/runtime/client/hosted-sessions.ts +1 -1
  248. package/src/runtime/client/spine-adoption.ts +1 -1
  249. package/src/runtime/context-accounting-source.ts +5 -4
  250. package/src/runtime/index.ts +1 -0
  251. package/src/runtime/notification-dispatch.ts +102 -29
  252. package/src/runtime/recovery-offer-wiring.ts +4 -1
  253. package/src/runtime/recovery-prompt.ts +31 -15
  254. package/src/runtime/runtime-services-types.ts +10 -6
  255. package/src/runtime/services.ts +17 -12
  256. package/src/runtime/session-continuity-hints.ts +2 -5
  257. package/src/runtime/session-inbound-inputs.ts +1 -1
  258. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  259. package/src/runtime/startup-typeahead-gate.ts +103 -0
  260. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  261. package/src/runtime/store/domains/index.ts +0 -8
  262. package/src/runtime/store/selectors/index.ts +0 -28
  263. package/src/runtime/store/state.ts +0 -8
  264. package/src/runtime/ui-services.ts +4 -6
  265. package/src/runtime/usage-tracker.ts +348 -0
  266. package/src/runtime/wrfc-persistence.ts +4 -4
  267. package/src/shell/blocking-input.ts +92 -2
  268. package/src/shell/recovery-input-helpers.ts +1 -1
  269. package/src/shell/session-views.ts +539 -0
  270. package/src/shell/status-report.ts +86 -0
  271. package/src/shell/transcript-scroll.ts +68 -0
  272. package/src/shell/ui-openers.ts +84 -172
  273. package/src/shell/view-openers.ts +205 -0
  274. package/src/tools/wrfc-agent-guard.ts +22 -2
  275. package/src/utils/format-duration.ts +4 -8
  276. package/src/utils/splash-lines.ts +9 -1
  277. package/src/utils/terminal-width.ts +56 -0
  278. package/src/verification/live-verifier.ts +0 -10
  279. package/src/version.ts +1 -1
  280. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  281. package/src/{panels → views}/builtin-modals.ts +22 -21
  282. package/src/views/builtin-views.ts +135 -0
  283. package/src/{panels → views}/confirm-state.ts +9 -9
  284. package/src/{panels → views}/diff-review-model.ts +10 -3
  285. package/src/{panels → views}/eval-registry.ts +1 -1
  286. package/src/{panels → views}/fleet-acts.ts +49 -85
  287. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  288. package/src/{panels → views}/fleet-gateway.ts +5 -5
  289. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  290. package/src/{panels → views}/fleet-read-model.ts +9 -9
  291. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  292. package/src/{panels → views}/fleet-spawn.ts +33 -49
  293. package/src/{panels → views}/fleet-steer.ts +12 -12
  294. package/src/{panels → views}/fleet-stop.ts +4 -4
  295. package/src/{panels → views}/fleet-transcript.ts +26 -27
  296. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  297. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  298. package/src/views/modal-surface-registry.ts +41 -0
  299. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  300. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  301. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  302. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  303. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  304. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  305. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  306. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  307. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  308. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  309. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  310. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  311. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  312. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  313. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  314. package/src/{panels → views}/modals/security-modal.ts +22 -16
  315. package/src/{panels → views}/modals/services-modal.ts +3 -3
  316. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  317. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  318. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  319. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  320. package/src/views/notifications-feed.ts +314 -0
  321. package/src/{panels → views}/polish-core.ts +38 -36
  322. package/src/{panels → views}/polish-tables.ts +35 -26
  323. package/src/{panels → views}/polish.ts +101 -95
  324. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  325. package/src/{panels → views}/provider-health-routes.ts +14 -14
  326. package/src/{panels → views}/search-focus.ts +8 -8
  327. package/src/{panels → views}/session-maintenance.ts +11 -11
  328. package/src/views/skills-discovery.ts +159 -0
  329. package/src/views/view-deps.ts +80 -0
  330. package/src/views/view-panel-adapter.ts +53 -0
  331. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  332. package/src/core/conversation-fold.ts +0 -101
  333. package/src/core/conversation-turn-structure.ts +0 -464
  334. package/src/core/conversation-user-receipts.ts +0 -42
  335. package/src/input/commands/operator-panel-runtime.ts +0 -186
  336. package/src/input/panel-integration-actions.ts +0 -33
  337. package/src/input/panel-mouse-geometry.ts +0 -97
  338. package/src/input/panel-paste-flood-guard.ts +0 -86
  339. package/src/panels/base-panel.ts +0 -301
  340. package/src/panels/builtin/agent.ts +0 -27
  341. package/src/panels/builtin/development.ts +0 -92
  342. package/src/panels/builtin/knowledge.ts +0 -12
  343. package/src/panels/builtin/operations.ts +0 -177
  344. package/src/panels/builtin/session.ts +0 -81
  345. package/src/panels/builtin/shared.ts +0 -357
  346. package/src/panels/builtin-panels.ts +0 -28
  347. package/src/panels/cost-tracker-panel.ts +0 -746
  348. package/src/panels/diff-panel.ts +0 -797
  349. package/src/panels/diff-review-panel.ts +0 -395
  350. package/src/panels/expandable-list-panel.ts +0 -189
  351. package/src/panels/fleet-deep-link.ts +0 -31
  352. package/src/panels/fleet-panel.ts +0 -799
  353. package/src/panels/git-panel.ts +0 -800
  354. package/src/panels/hosted-session-panel.ts +0 -221
  355. package/src/panels/index.ts +0 -21
  356. package/src/panels/local-auth-panel.ts +0 -478
  357. package/src/panels/notifications-feed.ts +0 -127
  358. package/src/panels/notifications-panel.ts +0 -160
  359. package/src/panels/panel-confirm-overlay.ts +0 -72
  360. package/src/panels/panel-manager.ts +0 -791
  361. package/src/panels/plugins-panel.ts +0 -371
  362. package/src/panels/scrollable-list-panel.ts +0 -498
  363. package/src/panels/skills-panel.ts +0 -454
  364. package/src/panels/token-budget-panel.ts +0 -688
  365. package/src/panels/types.ts +0 -191
  366. package/src/panels/wrfc-panel-format.ts +0 -133
  367. package/src/renderer/diff-view.ts +0 -134
  368. package/src/renderer/fleet-tab-strip.ts +0 -56
  369. package/src/renderer/footer-tips.ts +0 -53
  370. package/src/renderer/fullscreen-primitives.ts +0 -146
  371. package/src/renderer/fullscreen-workspace.ts +0 -194
  372. package/src/renderer/hint-grammar.ts +0 -52
  373. package/src/renderer/modal-factory.ts +0 -480
  374. package/src/renderer/overlay-box.ts +0 -154
  375. package/src/renderer/overlay-viewport.ts +0 -104
  376. package/src/renderer/panel-composite.ts +0 -145
  377. package/src/renderer/panel-workspace-bar.ts +0 -54
  378. package/src/renderer/process-indicator.ts +0 -163
  379. package/src/renderer/tab-strip.ts +0 -222
  380. package/src/renderer/text-layout.ts +0 -31
  381. package/src/renderer/tool-call.ts +0 -276
  382. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  383. package/src/renderer/tool-result-summary.ts +0 -219
  384. package/src/runtime/diagnostics/panels/index.ts +0 -21
  385. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  386. package/src/runtime/perf/panel-contracts.ts +0 -32
  387. package/src/runtime/store/domains/panels.ts +0 -117
  388. package/src/verification/verification-ledger.ts +0 -707
  389. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  390. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  391. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -1,41 +1,20 @@
1
1
  import { UIFactory } from '../renderer/ui-factory.ts';
2
- import { renderMarkdownTracked } from '../renderer/markdown.ts';
3
- import { isDiffContent, renderExpandedToolResultLines } from '../renderer/tool-result-expanded-lines.ts';
4
- import { activeTheme, activeUiTones } from '../renderer/theme.ts';
5
- import { renderToolCallBlock } from '../renderer/tool-call.ts';
6
- import { summarizeToolResult } from '../renderer/tool-result-summary.ts';
2
+ import { activeTheme, activeTokens } from '../renderer/theme.ts';
7
3
  import type { ToolCall } from '@pellux/goodvibes-sdk/platform/types';
8
- import { renderThinkingBlock } from '../renderer/thinking.ts';
9
4
  import { renderSystemMessage } from '../renderer/system-message.ts';
10
5
  import { createEmptyLine, type Line, type Cell } from '@pellux/goodvibes-sdk/platform/types';
11
- import { getSplashLines, type SplashOptions } from '../utils/splash-lines.ts';
6
+ import { getSplashLines, SPLASH_GRADIENT } from '../utils/splash-lines.ts';
12
7
  import { interpolateColor, getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
13
- import { BORDERS, LAYOUT, TOOL_STATUS } from '../renderer/layout.ts';
14
- import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
15
- import { renderConversationEventLine, renderConversationFoldedRow } from '../renderer/conversation-surface.ts';
16
- import { GLYPHS } from '../renderer/ui-primitives.ts';
17
- import type { BlockMeta } from './conversation-types.ts';
18
- import {
19
- collectToolCallOutcomes,
20
- isTurnCollapsed,
21
- type ConversationRenderContext,
22
- } from './conversation-render-context.ts';
8
+ import { LAYOUT } from '../renderer/layout.ts';
9
+ import type { ConversationRenderContext } from './conversation-render-context.ts';
23
10
  import { renderCompactionContinuationMessage } from './conversation-compaction-render.ts';
24
- // Which rows fold, and the blank separator that earns, shared verbatim with
25
- // conversation-line-cache.ts so a warm cache and a cold rebuild space alike.
26
- import { isToolResultFolded, trailingBlankAfter } from './conversation-fold.ts';
27
- import { drawTreeRails, treeIndentCols, treeTextCol } from '@pellux/goodvibes-terminal-shell';
28
- import {
29
- MAX_NEST_DEPTH,
30
- buildRenderPlan,
31
- collapseKeyForNode,
32
- computeAssistantTurns,
33
- type AssistantTurnMembership,
34
- type RenderNode,
35
- } from './conversation-turn-structure.ts';
36
11
  import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
37
- import { parseDiffForApply } from '@pellux/goodvibes-sdk/platform/core';
38
12
  import { extractUserDisplayText, COMPACTION_HANDOFF_HEADER } from '@pellux/goodvibes-sdk/platform/core';
13
+ import { laneGlyphs } from '../renderer/lane-graph/glyphs.ts';
14
+ import { beadArgument, beadName, cellText, formatBeadTime } from '../renderer/lane-graph/bead.ts';
15
+ import { paintBeadRow } from '../renderer/lane-graph/paint.ts';
16
+ import { buildTurnModel, orphanResultBead, transcriptUnits, type TranscriptUnit, type TurnModel } from './work-tree-model.ts';
17
+ import { renderSingleBead, renderTurn, type TurnRender } from './work-tree-render.ts';
39
18
  // SystemMessageKind imported from runtime directly to avoid cycle:
40
19
  // conversation-rendering.ts → system-message-router.ts → conversation.ts → conversation-rendering.ts
41
20
  import type { SystemMessageKind } from '@/runtime/index.ts';
@@ -67,13 +46,13 @@ type Message = ConversationMessageSnapshot;
67
46
  export {
68
47
  collectCompletedToolCallIds,
69
48
  collectToolCallOutcomes,
70
- isTurnCollapsed,
71
49
  type ConversationRenderContext,
72
50
  type ToolCallOutcome,
73
51
  } from './conversation-render-context.ts';
74
52
 
75
- function summarizeCallId(callId: string, maxLength = 24): string {
76
- return callId.length <= maxLength ? callId : `${callId.slice(0, maxLength - 1)}…`;
53
+ /** Whether a system message at `msgIdx` is navigable for error navigation. */
54
+ export function isNavigableSystemMessage(context: Pick<ConversationRenderContext, 'messageKindRegistry'>, msgIdx: number): boolean {
55
+ return NAVIGABLE_KINDS.has(context.messageKindRegistry.get(msgIdx) ?? 'system');
77
56
  }
78
57
 
79
58
  export function renderConversationUserMessage(
@@ -85,7 +64,7 @@ export function renderConversationUserMessage(
85
64
  const T = activeTheme();
86
65
  const displayText = extractUserDisplayText(message.content);
87
66
  if (message.cancelled) {
88
- context.history.addLines(UIFactory.createMessageBar(width, displayText, T.errorBarBg, '196', ' x ', true));
67
+ context.history.addLines(UIFactory.createMessageBar(width, displayText, T.errorBarBg, activeTokens().textMuted, activeTokens().error, true));
89
68
  return;
90
69
  }
91
70
  // Compaction-continuation handoff: a user-ROLE message the compactor
@@ -100,246 +79,6 @@ export function renderConversationUserMessage(
100
79
  context.history.addLines(UIFactory.createMessageBar(width, displayText));
101
80
  }
102
81
 
103
- export function renderConversationAssistantMessage(
104
- context: ConversationRenderContext,
105
- message: Extract<Message, { role: 'assistant' }>,
106
- width: number,
107
- lineNumberMode: 'all' | 'code' | 'off',
108
- collapseThreshold: number,
109
- msgIdx: number,
110
- ): void {
111
- const T = activeTheme();
112
- const turn = context.assistantTurns?.get(msgIdx);
113
- // No membership (a standalone render, e.g. a golden-frame harness) behaves
114
- // exactly as it always did: the message owns its own header.
115
- const isHead = turn?.isHead ?? true;
116
- const turnCollapsed = isTurnCollapsed(turn, context.collapseState);
117
-
118
- if (isHead) {
119
- const assistantHeaderDetails = [];
120
- if (message.model) {
121
- assistantHeaderDetails.push({ text: ` ${message.model}${message.provider ? ` (${message.provider})` : ''} `, fg: T.modelNameDim, dim: true });
122
- }
123
- // The count spans the whole run, not just this message, that is the point
124
- // of merging. `tools:1` repeated over five headers becomes one `5 tools`.
125
- const toolCount = turn?.toolCallCount ?? message.toolCalls?.length ?? 0;
126
- if (toolCount > 0) {
127
- assistantHeaderDetails.push({ text: ` ${GLYPHS.status.pending} ${toolCount} tool${toolCount === 1 ? '' : 's'} `, fg: T.toolAccent });
128
- }
129
- // A label every call in the run shares belongs here, once, instead of on
130
- // every branch, the branches then lead with what distinguishes them.
131
- //
132
- // Hoisted ONLY when it fits whole. A label chopped mid-word ("Calling the
133
- // assistant serv") is worse than no label: it costs a third of the header
134
- // and tells you less than the rows below it already do, since each row
135
- // leads with its own distinguishing token. So it either fits or it goes,
136
- // never a truncated stub. Dropping it loses nothing reachable: the rows
137
- // still name every call, and omitToolName is unaffected either way.
138
- if (turn?.sharedToolLabel) {
139
- const usedCols = assistantHeaderDetails.reduce((sum, seg) => sum + getDisplayWidth(seg.text), 0);
140
- const availCols = width - LAYOUT.RIGHT_MARGIN - (LAYOUT.LEFT_MARGIN + 1)
141
- - getDisplayWidth(' assistant ') - usedCols;
142
- const labelText = ` ${turn.sharedToolLabel} `;
143
- if (getDisplayWidth(labelText) <= availCols) {
144
- assistantHeaderDetails.push({ text: labelText, fg: T.toolNameFg });
145
- }
146
- }
147
- // Aggregated across the run so suppressing a non-head's header never loses
148
- // the signal that reasoning happened.
149
- const hasReasoning = turn?.hasReasoning ?? Boolean(message.reasoningContent || message.reasoningSummary);
150
- if (hasReasoning) {
151
- assistantHeaderDetails.push({ text: ` ${GLYPHS.status.active} reasoning `, fg: T.reasoningAccent, dim: true });
152
- }
153
- if (turnCollapsed && toolCount > 0) {
154
- assistantHeaderDetails.push({ text: ` ${GLYPHS.navigation.collapsed} hidden `, fg: '244', dim: true });
155
- }
156
- // An empty run, no model, no tools, no reasoning, emits no header rather
157
- // than a bare `● assistant` with nothing under it.
158
- if (assistantHeaderDetails.length > 0) {
159
- const headerStartLine = context.history.getLineCount();
160
- const headerBlockIdx = context.blockRegistry.length;
161
- context.history.addLine(renderConversationEventLine(width, {
162
- marker: GLYPHS.status.active,
163
- markerFg: T.assistantHeader,
164
- label: 'assistant',
165
- labelFg: T.assistantHeader,
166
- detailFg: '244',
167
- }, assistantHeaderDetails));
168
-
169
- // Turns are collapsible as a unit and default to EXPANDED, a turn
170
- // collapsed by default would hide the activity the transcript exists to
171
- // show. Registered only when there is machinery to hide.
172
- if (turn && toolCount > 0) {
173
- if (!context.collapseState.has(turn.turnKey)) {
174
- context.collapseState.set(turn.turnKey, false);
175
- }
176
- context.blockRegistry.push({
177
- blockIndex: headerBlockIdx,
178
- collapseKey: turn.turnKey,
179
- type: 'assistant_turn',
180
- startLine: headerStartLine,
181
- lineCount: 1,
182
- rawContent: `assistant turn: ${toolCount} tool call${toolCount === 1 ? '' : 's'}${turn.sharedToolLabel ? ` (${turn.sharedToolLabel})` : ''}`,
183
- groupMemberIndexes: turn.resultIndexes,
184
- toolName: turn.sharedToolLabel,
185
- });
186
- }
187
- }
188
- }
189
-
190
- // A collapsed turn hides its machinery (tool rows, reasoning) but never its
191
- // prose: the prose IS the answer, and hiding it would make collapse
192
- // destructive rather than tidying.
193
- if (turnCollapsed) {
194
- if (message.content) renderAssistantProse(context, message, width, lineNumberMode, collapseThreshold, msgIdx);
195
- return;
196
- }
197
-
198
- const showThinking = context.configManager?.get('display.showThinking') ?? false;
199
- const showReasoningSummary = context.configManager?.get('display.showReasoningSummary') ?? false;
200
- if (showThinking && message.reasoningContent) {
201
- const thinkingStartLine = context.history.getLineCount();
202
- const thinkingBlockIdx = context.blockRegistry.length;
203
- const thinkingCollapseKey = `msg_${msgIdx}_thinking`;
204
- // Collapsed by default, like every other collapsible block, Tab (or the
205
- // block-actions menu) expands it via the same collapseKey the BlockMeta
206
- // below registers. Previously this key was registered but never
207
- // consulted: the toggle existed but did nothing.
208
- const isThinkingCollapsed = context.collapseState.has(thinkingCollapseKey)
209
- ? context.collapseState.get(thinkingCollapseKey)!
210
- : true;
211
- if (!context.collapseState.has(thinkingCollapseKey)) {
212
- context.collapseState.set(thinkingCollapseKey, true);
213
- }
214
- if (isThinkingCollapsed) {
215
- // One row: label plus size badge, no frame and no interior line.
216
- //
217
- // Deliberately NO preview, unlike a folded tool result. Reasoning text
218
- // stays behind the toggle until it is asked for, the fold is the quiet
219
- // state of an already-opt-in display, and previewing the first reasoning
220
- // line here would disclose content the collapsed form has never shown.
221
- const thinkingLineCount = message.reasoningContent.split('\n').length;
222
- context.history.addLine(renderConversationFoldedRow(width, {
223
- // The same ▌ the EXPANDED block draws (BORDERS.THINKING), so folding
224
- // changes the row count without changing what the block reads as.
225
- marker: BORDERS.THINKING.char,
226
- markerFg: T.reasoningAccent,
227
- label: 'thinking',
228
- labelFg: T.reasoningAccent,
229
- detailFg: '244',
230
- }, [
231
- { text: ` ${GLYPHS.navigation.collapsed} ${thinkingLineCount} line${thinkingLineCount === 1 ? '' : 's'} `, fg: '244', dim: true },
232
- ], ''));
233
- } else {
234
- const thinkingLines = renderThinkingBlock(message.reasoningContent, width);
235
- context.history.addLines(thinkingLines);
236
- }
237
- context.history.addLine(createEmptyLine(width));
238
- const thinkingRenderedLines = context.history.getLineCount() - thinkingStartLine;
239
- context.blockRegistry.push({
240
- blockIndex: thinkingBlockIdx,
241
- collapseKey: thinkingCollapseKey,
242
- type: 'thinking',
243
- startLine: thinkingStartLine,
244
- lineCount: thinkingRenderedLines,
245
- rawContent: message.reasoningContent,
246
- });
247
- }
248
- if (showReasoningSummary && message.reasoningSummary) {
249
- const summaryLines = renderThinkingBlock(message.reasoningSummary, width);
250
- context.history.addLines(summaryLines);
251
- context.history.addLine(createEmptyLine(width));
252
- }
253
-
254
- if (message.content) renderAssistantProse(context, message, width, lineNumberMode, collapseThreshold, msgIdx);
255
- }
256
-
257
- /**
258
- * Assistant prose, the model's actual answer, at full prominence and full
259
- * width.
260
- *
261
- * Prose is never drawn as a tree branch. It is what closes a group (see
262
- * startsNewTurn) and the content the tree exists to surround, not a child of
263
- * it; indenting it would also re-wrap markdown at a reduced width, which is
264
- * the table-drops-columns hazard.
265
- */
266
- function renderAssistantProse(
267
- context: ConversationRenderContext,
268
- message: Extract<Message, { role: 'assistant' }>,
269
- width: number,
270
- lineNumberMode: 'all' | 'code' | 'off',
271
- collapseThreshold: number,
272
- msgIdx: number,
273
- ): void {
274
- {
275
- const showAllLineNumbers = lineNumberMode === 'all';
276
- const showCodeBlockLineNumbers = lineNumberMode === 'all' ? false : lineNumberMode === 'code';
277
- // NOTE: The 'all' mode intentionally calls renderMarkdownTracked twice:
278
- // 1. Measure pass: render at full `width` to get the total line count, which
279
- // determines `numWidth` (digit count) and thus `gutterW` (gutter column width).
280
- // 2. Render pass: render at `width - gutterW` with the gutter factored in.
281
- //
282
- // Single-pass is not pursued here. It would require either a pessimistic
283
- // `numWidth=6` (fits 999,999 lines, but wastes 3-4 gutter columns on typical
284
- // messages) or rendering the numbered output into a scratch buffer and trimming.
285
- // Neither is clearly better than the current two-pass measurement approach.
286
- // The 4α commit message claim that this "eliminates double-parse when line
287
- // numbers are enabled" was inaccurate: 4α eliminated the legacy
288
- // `renderMarkdown()` duplicate used for code-block line-number mode ('code').
289
- // The 'all' mode double-call is a deliberate design choice and remains unchanged.
290
- const measureWidth = showAllLineNumbers ? width : 0;
291
- const totalLines = showAllLineNumbers
292
- ? renderMarkdownTracked(message.content, measureWidth, { codeBlockLineNumbers: false }).lines.length
293
- : 0;
294
- const numWidth = Math.max(3, String(totalLines).length);
295
- const gutterW = numWidth + 3;
296
- const contentWidth = showAllLineNumbers ? width - gutterW : width;
297
- const renderWidth = showAllLineNumbers ? contentWidth : width;
298
-
299
- const { lines: tracked, codeBlocks } = renderMarkdownTracked(message.content, renderWidth, {
300
- codeBlockLineNumbers: showCodeBlockLineNumbers,
301
- });
302
-
303
- const msgBaseLineOffset = context.history.getLineCount();
304
- for (const cb of codeBlocks) {
305
- const blockStartLine = msgBaseLineOffset + cb.startOffset;
306
- const blockIdx = context.blockRegistry.length;
307
- const collapseKey = `code_${msgIdx}_${blockIdx}`;
308
- const isAutoCollapsed = cb.rawContent.split('\n').length > collapseThreshold;
309
- if (isAutoCollapsed && !context.collapseState.has(collapseKey)) {
310
- context.collapseState.set(collapseKey, true);
311
- }
312
- context.blockRegistry.push({
313
- blockIndex: blockIdx,
314
- collapseKey,
315
- type: 'code',
316
- startLine: blockStartLine,
317
- lineCount: cb.lineCount,
318
- rawContent: cb.rawContent,
319
- });
320
- }
321
-
322
- if (showAllLineNumbers) {
323
- const numbered = tracked.map((line, i) => {
324
- const label = String(i + 1).padStart(numWidth) + ' │ ';
325
- const gutterCells = UIFactory.stringToLine(label, gutterW, { fg: '238', dim: true });
326
- const fullLine = createEmptyLine(width);
327
- for (let ci = 0; ci < gutterW && ci < gutterCells.length; ci++) {
328
- fullLine[ci] = gutterCells[ci];
329
- }
330
- for (let ci = 0; ci < line.length && gutterW + ci < width; ci++) {
331
- fullLine[gutterW + ci] = line[ci];
332
- }
333
- return fullLine;
334
- });
335
- context.history.addLines(numbered);
336
- } else {
337
- context.history.addLines(tracked);
338
- }
339
- }
340
-
341
- }
342
-
343
82
  export function renderConversationSystemMessage(
344
83
  context: ConversationRenderContext,
345
84
  message: Extract<Message, { role: 'system' }>,
@@ -357,244 +96,91 @@ export function renderConversationSystemMessage(
357
96
  }
358
97
  }
359
98
 
360
- /**
361
- * Render one tool-call row as a branch of its turn.
362
- *
363
- * The call row is its own plan node rather than part of the assistant
364
- * message's render, which is what lets a result be interleaved BETWEEN two
365
- * calls of the same message, the ordering guarantee that makes a
366
- * late-finishing call's result land inside its own subtree instead of after a
367
- * call issued later.
368
- */
369
- export function renderConversationToolCallNode(
99
+ /** Build one assistant turn's work-tree model from the render context. */
100
+ export function buildConversationTurnModel(
370
101
  context: ConversationRenderContext,
371
- node: RenderNode,
372
- width: number,
373
- ): void {
374
- const T = activeTheme();
375
- const message = node.message;
376
- if (message.role !== 'assistant') return;
377
- const call = message.toolCalls?.[node.callIndex ?? 0];
378
- if (!call) return;
379
-
380
- const turn = context.assistantTurns?.get(node.absIdx);
381
- if (isTurnCollapsed(turn, context.collapseState)) return;
382
-
383
- // The call row carries the call's OUTCOME, not merely whether it finished:
384
- // the result row below it no longer repeats a marker, so this glyph is the
385
- // only place a failed or cancelled tool says so in the status column.
386
- const outcome = call.id !== undefined ? context.toolCallOutcomes?.get(call.id) : undefined;
387
- const ran = context.completedToolCallIds === undefined
388
- || (call.id !== undefined && context.completedToolCallIds.has(call.id));
389
- const status: 'done' | 'error' | 'cancelled' | 'pending' = outcome === 'error'
390
- ? 'error'
391
- : outcome === 'cancelled'
392
- ? 'cancelled'
393
- : outcome === 'ok'
394
- ? 'done'
395
- : ran ? 'done' : 'pending';
396
- const indent = treeIndentCols(node.depth, width);
397
- const lines = renderToolCallBlock(
398
- call,
399
- status,
400
- undefined,
401
- width,
402
- undefined,
403
- undefined,
404
- undefined,
405
- undefined,
406
- { indentCols: indent, omitToolName: turn?.sharedToolLabel !== undefined },
407
- );
408
-
409
- // Recursion stopped here, say so rather than silently showing a subtree as
410
- // if it were a leaf.
411
- if (node.truncated) {
412
- const noteIndent = treeIndentCols(node.depth + 1, width);
413
- lines.push(renderConversationEventLine(width, {
414
- marker: GLYPHS.navigation.collapsed,
415
- markerFg: '244',
416
- label: '',
417
- labelFg: '244',
418
- detailFg: '244',
419
- }, [{
420
- text: node.truncated === 'depth'
421
- ? ` nested activity continues below depth ${MAX_NEST_DEPTH} `
422
- : ' nested activity repeats an agent already shown above ',
423
- fg: '244',
424
- dim: true,
425
- }], noteIndent));
426
- }
427
-
428
- // Rails last, over every line the row emitted, so the vertical run down to
429
- // the next sibling has no gap in it.
430
- drawTreeRails(lines, node.depth, node.connector, node.openAncestorDepths, width, T.toolAccent);
431
- context.history.addLines(lines);
102
+ messages: readonly Message[],
103
+ offset: number,
104
+ unit: Extract<TranscriptUnit, { kind: 'turn' }>,
105
+ streamingIndex = -1,
106
+ ): TurnModel {
107
+ const config = context.configManager;
108
+ return buildTurnModel({
109
+ messages,
110
+ offset,
111
+ unit,
112
+ sources: context.workTreeSources ?? {},
113
+ collapse: context.collapseState,
114
+ streamingIndex,
115
+ showThinking: config?.get('display.showThinking') ?? false,
116
+ showReasoningSummary: config?.get('display.showReasoningSummary') ?? false,
117
+ });
432
118
  }
433
119
 
434
120
  /**
435
- * Move an already-rendered line `cols` columns to the right on a fresh
436
- * full-width line. Used to place a tree row's expanded body under its own
437
- * indent without re-rendering it at a different width (which would change the
438
- * line count the row's badge already committed to).
121
+ * Draw a turn model (lines, blocks and error lines relative to the turn), then
122
+ * the notices that arrived during it (never in the main transcript, whose
123
+ * notices are toasts and notification-history entries: systemNotices). A notice is not a step of the turn, so it
124
+ * is drawn after the turn and outside its lanes, exactly as a notice between
125
+ * turns is (the concept's turns hold only the turn's own text, beads, lanes and
126
+ * answer; agent and WRFC state is the lanes' own spawn and merge rows).
439
127
  */
440
- function shiftLineRight(line: Line, cols: number, width: number): Line {
441
- if (cols <= 0) return line;
442
- const shifted = createEmptyLine(width);
443
- for (let i = 0; i < line.length; i++) {
444
- const target = i + cols;
445
- if (target >= width) break;
446
- shifted[target] = line[i]!;
128
+ export function drawConversationTurn(context: ConversationRenderContext, model: TurnModel, width: number): TurnRender {
129
+ const render = drawTurnRows(context, model, width);
130
+ if (model.notices.length === 0 || context.systemNotices === 'elsewhere') return render;
131
+ const lines = [...render.lines];
132
+ const errorLines = [...render.errorLines];
133
+ const messageLines = new Map(render.messageLines);
134
+ for (const notice of model.notices) {
135
+ lines.push(createEmptyLine(width));
136
+ messageLines.set(notice.messageIndex, lines.length);
137
+ if (isNavigableSystemMessage(context, notice.messageIndex)) errorLines.push(lines.length);
138
+ lines.push(...renderSystemMessage(notice.content, width));
447
139
  }
448
- return shifted;
140
+ return { ...render, lines, errorLines, messageLines };
449
141
  }
450
142
 
451
- export function renderConversationToolMessage(
143
+ function drawTurnRows(context: ConversationRenderContext, model: TurnModel, width: number): TurnRender {
144
+ const config = context.configManager;
145
+ return renderTurn(model, {
146
+ width,
147
+ glyphSet: context.treeGlyphSet ?? 'rounded',
148
+ focusId: context.focusId ?? null,
149
+ lineNumberMode: config?.get('display.lineNumbers') ?? 'off',
150
+ collapseThreshold: config?.get('display.collapseThreshold') ?? 30,
151
+ collapseState: context.collapseState,
152
+ isNavigableSystem: (index) => isNavigableSystemMessage(context, index),
153
+ frame: context.frame ?? 0,
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Render one assistant turn through the work tree (work-tree-model.ts builds
159
+ * its rows, work-tree-render.ts draws them) into the context's history.
160
+ */
161
+ export function renderConversationTurn(
452
162
  context: ConversationRenderContext,
453
- message: Extract<Message, { role: 'tool' }>,
163
+ messages: readonly Message[],
164
+ offset: number,
165
+ unit: Extract<TranscriptUnit, { kind: 'turn' }>,
454
166
  width: number,
455
- msgIdx: number,
456
- node?: RenderNode,
457
- ): void {
458
- const T = activeTheme();
459
- const depth = node?.depth ?? 0;
460
- const indent = treeIndentCols(depth, width);
461
- const turn = context.assistantTurns?.get(msgIdx);
462
- // A collapsed turn hides its machinery; the head's prose stays visible.
463
- if (isTurnCollapsed(turn, context.collapseState)) return;
464
-
465
- const collapseKey = node ? collapseKeyForNode(node) : `msg_${msgIdx}`;
466
- const blockIdx = context.blockRegistry.length;
467
- const startLine = context.history.getLineCount();
468
- const contentLines = message.content.split('\n');
469
- const isDiff = isDiffContent(message.content);
470
- const blockType: 'diff' | 'tool' = isDiff ? 'diff' : 'tool';
471
- // Parsed once, ahead of the collapse check, so it's available for the
472
- // block-registry meta merge below regardless of collapsed/expanded state.
473
- const diffParse = isDiff ? parseDiffForApply(message.content) : undefined;
474
- const inTree = depth > 0 && indent > 0;
475
- // The EXPANDED render, computed unconditionally (even while collapsed) so
476
- // the header's "N lines" badge always names what Tab would actually reveal
477
- //, a raw JSON blob that pretty-prints to 50 lines must say 50, not 1, even
478
- // while it's still folded. Reused below as the actual body when expanded,
479
- // so this is never rendered twice.
480
- //
481
- // In the tree it is rendered at the width it will actually OCCUPY (the row's
482
- // width less its indent) and then shifted into place, so the badge still
483
- // counts exactly the lines expansion produces. Rendering it full-width and
484
- // displaying it indented would desynchronise the two, the
485
- // markdown-table-drops-columns bug class, and rendering it full-width and
486
- // displaying it flush punches the row's rails out for the whole body.
487
- // The body is a continuation of its row, so it starts at the row's own text
488
- // column; the shift is that column expressed relative to the flush left
489
- // margin the body renders with internally.
490
- const bodyShift = inTree ? Math.max(0, treeTextCol(indent) - LAYOUT.LEFT_MARGIN) : 0;
491
- const bodyWidth = Math.max(LAYOUT.LEFT_MARGIN + LAYOUT.RIGHT_MARGIN + 8, width - bodyShift);
492
- const expandedLines = renderExpandedToolResultLines(message.content, bodyWidth);
493
- const lineCount = expandedLines.length;
494
-
495
- // Human one-line summary for tool results (write/read/exec/edit): shown as the
496
- // collapsed line so the transcript reads "wrote foo.txt (532 B)" instead of a
497
- // raw JSON blob; the full payload stays behind the expand toggle. Only for
498
- // 'tool' blocks (diffs render their own view). (item 3.)
499
- const resultSummary = blockType === 'tool'
500
- ? summarizeToolResult(message.toolName, message.content)
501
- : null;
502
-
503
- const isCollapsed = isToolResultFolded(message, context.collapseState, collapseKey);
504
-
505
- // The fold decision IS the stored default: with nothing stored,
506
- // isToolResultFolded() already resolved to exactly the value this key should
507
- // hold. Recomputing "is it short?" here would be a second copy of the
508
- // threshold that the policy module owns.
509
- if (!context.collapseState.has(collapseKey)) {
510
- context.collapseState.set(collapseKey, isCollapsed);
511
- }
512
-
513
- // A per-call user cancellation settles as a tool result whose content leads
514
- // with "Error: cancelled by user" (the SDK's structured cancelled shape; any
515
- // partial output the tool produced before it stopped follows on later lines).
516
- // Render it structurally as a distinct "cancelled" block in the warn tone,
517
- // not a generic tool result, and not a hard error, so the transcript reads
518
- // the user's decision honestly while the partial output stays visible below.
519
- const isCancelled = blockType === 'tool' && /^Error: cancelled by user\b/.test(contentLines[0] ?? '');
520
- const warnTone = activeUiTones().chrome.warn;
521
-
522
- // In the tree a result hangs under the call that produced it, so repeating
523
- // the generic label ("tool result") and the tool's own name, which the
524
- // parent call row already shows, is exactly the boilerplate this layout
525
- // removes. The row leads with its size badge instead. `diff` and `cancelled`
526
- // keep their labels: those carry information the parent row does not.
527
- const label = isCancelled ? 'cancelled' : (blockType === 'diff' ? 'diff' : (inTree ? '' : 'tool result'));
528
- const nameSegments = inTree
529
- ? []
530
- : (message.toolName
531
- ? [{ text: ` ${message.toolName} `, fg: T.toolNameFg }]
532
- : [{ text: ` ${summarizeCallId(message.callId || 'standalone')} `, fg: '244' as const, dim: true }]);
533
-
534
- // A FOLDED result is exactly one row: this header, with its preview riding on
535
- // the same line right after the `▸ N lines` badge (see
536
- // renderConversationFoldedRow). The badge is the count, the old fold also
537
- // carried a separate `[▸ N hidden]` marker, which restated it.
538
- const headerTone = {
539
- marker: isCancelled ? GLYPHS.status.blocked : (blockType === 'diff' ? GLYPHS.status.dualPane : GLYPHS.status.active),
540
- markerFg: isCancelled ? warnTone : (blockType === 'diff' ? T.diffAccent : T.toolAccent),
541
- label,
542
- labelFg: isCancelled ? warnTone : (blockType === 'diff' ? T.diffAccent : T.toolAccent),
543
- detailFg: '244',
544
- };
545
- const headerDetails = [
546
- ...nameSegments,
547
- { text: ` ${isCollapsed ? GLYPHS.navigation.collapsed : GLYPHS.navigation.expanded} ${lineCount} line${lineCount === 1 ? '' : 's'} `, fg: '244', dim: true },
548
- ];
549
- const headerLine = isCollapsed
550
- ? renderConversationFoldedRow(width, headerTone, headerDetails, resultSummary ?? contentLines[0] ?? '', indent)
551
- : renderConversationEventLine(width, headerTone, headerDetails, indent);
552
- // Every line this row emits, gathered before anything is committed, so the
553
- // rails can be drawn across all of them in one pass (see drawTreeRails). The
554
- // result row deliberately carries NO status marker of its own: the call row
555
- // directly above it already states the outcome in the shared bullet column,
556
- // and a second marker one row down only doubles it.
557
- const rows: Line[] = [headerLine];
558
-
559
- if (!isCollapsed) {
560
- // Diff or plain result, either way this is exactly `expandedLines`,
561
- // already computed above (and already what the header's line count is
562
- // honest about), so there is nothing left to render here.
563
- //
564
- // Deliberately NOT indented. renderExpandedToolResultLines is the single
565
- // source of truth for the "N lines" badge above; re-wrapping it at a
566
- // reduced width would desynchronise that count from what expansion
567
- // actually reveals, the markdown-table-drops-columns bug class. The body
568
- // is already visually bound to its row by the header directly above it.
569
- // In the tree it was rendered at `bodyWidth` above and is shifted into the
570
- // row's indent here, which is what leaves the rail columns to its left free
571
- // for the rails to run through.
572
- for (const line of expandedLines) rows.push(shiftLineRight(line, bodyShift, width));
573
- }
574
-
575
- if (node && inTree) {
576
- drawTreeRails(rows, node.depth, node.connector, node.openAncestorDepths, width, T.toolAccent);
577
- }
578
- context.history.addLines(rows);
579
-
580
- const renderedLineCount = context.history.getLineCount() - startLine;
581
- let meta: BlockMeta = {
582
- blockIndex: blockIdx,
583
- collapseKey,
584
- type: blockType,
585
- startLine,
586
- lineCount: renderedLineCount,
587
- rawContent: message.content,
588
- toolName: message.toolName,
589
- };
590
-
591
- if (isDiff && diffParse) {
592
- meta = { ...meta, ...diffParse };
167
+ streamingIndex = -1,
168
+ ): TurnRender {
169
+ const render = drawConversationTurn(context, buildConversationTurnModel(context, messages, offset, unit, streamingIndex), width);
170
+ const base = context.history.getLineCount();
171
+ const blockBase = context.blockRegistry.length;
172
+ context.history.addLines(render.lines);
173
+ for (const block of render.blocks) {
174
+ context.blockRegistry.push({ ...block, blockIndex: block.blockIndex + blockBase, startLine: block.startLine + base });
593
175
  }
594
-
595
- context.blockRegistry.push(meta);
176
+ for (const line of render.errorLines) context.errorLineRegistry.push(line + base);
177
+ return render;
596
178
  }
597
179
 
180
+ /**
181
+ * Render a message slice: user and system messages as themselves, every
182
+ * assistant turn as its work tree, a blank row after each unit.
183
+ */
598
184
  export function appendConversationMessages(
599
185
  context: ConversationRenderContext,
600
186
  messages: Message[],
@@ -608,71 +194,49 @@ export function appendConversationMessages(
608
194
  */
609
195
  msgIndexOffset = 0,
610
196
  ): void {
611
- const lineNumberMode = context.configManager?.get('display.lineNumbers') ?? 'off';
612
- const collapseThreshold = context.configManager?.get('display.collapseThreshold') ?? 30;
613
- // Derive pending vs done for tool calls from sibling tool-result messages,
614
- // unless the caller already supplied the set. (item 2c.)
615
- const renderContext: ConversationRenderContext = context.completedToolCallIds !== undefined
616
- ? context
617
- : (() => {
618
- const toolCallOutcomes = collectToolCallOutcomes(messages);
619
- return { ...context, toolCallOutcomes, completedToolCallIds: new Set(toolCallOutcomes.keys()) };
620
- })();
621
- const assistantTurns = renderContext.assistantTurns
622
- ?? computeAssistantTurns(messages, msgIndexOffset);
623
- const turnContext: ConversationRenderContext = renderContext.assistantTurns !== undefined
624
- ? renderContext
625
- : { ...renderContext, assistantTurns };
626
-
627
- const plan = buildRenderPlan(messages, msgIndexOffset, {
628
- resolveAgentSnapshot: context.resolveAgentSnapshot,
629
- });
630
-
631
- // Header line of each turn, so a row that renders nothing (hidden by a
632
- // collapsed turn) still anchors transcript navigation at its turn rather
633
- // than at whatever position the buffer happens to sit at.
634
- const turnHeaderLines = new Map<string, number>();
635
-
636
- for (let i = 0; i < plan.length; i++) {
637
- const node = plan[i]!;
638
- const before = turnContext.history.getLineCount();
639
-
640
- if (node.kind === 'toolcall') {
641
- renderConversationToolCallNode(turnContext, node, width);
197
+ for (const unit of transcriptUnits(messages, msgIndexOffset)) {
198
+ const before = context.history.getLineCount();
199
+ if (unit.kind === 'turn') {
200
+ const render = renderConversationTurn(context, messages, msgIndexOffset, unit, width);
201
+ for (const [index, line] of render.messageLines) messageLineRegistry[index] = before + line;
202
+ for (let index = unit.start; index <= unit.end; index++) messageLineRegistry[index] ??= before;
642
203
  } else {
643
- const message = node.message;
644
- // Nested rows index into their own agent's snapshot, so they must not
645
- // write the root transcript's line registry.
646
- const isRoot = node.scope === '';
647
- const turn = turnContext.assistantTurns?.get(node.absIdx);
648
- if (turn?.isHead) turnHeaderLines.set(turn.turnKey, before);
649
- if (isRoot) {
650
- messageLineRegistry[node.absIdx] = turn && isTurnCollapsed(turn, turnContext.collapseState)
651
- ? (turnHeaderLines.get(turn.turnKey) ?? before)
652
- : before;
653
- }
654
-
655
- if (message.role === 'user') {
656
- renderConversationUserMessage(turnContext, message, width, node.absIdx);
657
- } else if (message.role === 'assistant') {
658
- renderConversationAssistantMessage(turnContext, message, width, lineNumberMode, collapseThreshold, node.absIdx);
659
- } else if (message.role === 'system') {
660
- renderConversationSystemMessage(turnContext, message, width, node.absIdx);
661
- } else if (message.role === 'tool') {
662
- renderConversationToolMessage(turnContext, message, width, node.absIdx, node);
663
- }
204
+ messageLineRegistry[unit.index] = before;
205
+ renderConversationMessageUnit(context, messages[unit.index - msgIndexOffset]!, width, unit.index);
664
206
  }
207
+ if (context.history.getLineCount() > before) context.history.addLine(createEmptyLine(width));
208
+ }
209
+ }
665
210
 
666
- const rendered = turnContext.history.getLineCount() > before;
667
- if (!rendered) continue;
668
- // Branch rows sit tight under their parent; the blank separator lands only
669
- // after the last row of a top-level unit, which is what keeps a turn's
670
- // whole subtree reading as one block instead of a run of spaced-out rows.
671
- // A folded result followed by more tool machinery gets no blank at all,
672
- // consecutive folded results stack as adjacent single rows.
673
- if (trailingBlankAfter(node, plan[i + 1], turnContext)) {
674
- turnContext.history.addLine(createEmptyLine(width));
675
- }
211
+ /** A standalone message unit (a user message, or a system message outside any turn). */
212
+ export function renderConversationMessageUnit(
213
+ context: ConversationRenderContext,
214
+ message: Message,
215
+ width: number,
216
+ msgIdx: number,
217
+ ): void {
218
+ if (message.role === 'user') renderConversationUserMessage(context, message, width, msgIdx);
219
+ else if (message.role === 'system') {
220
+ // The main transcript's notices are toasts and history entries, not rows.
221
+ if (context.systemNotices !== 'elsewhere') renderConversationSystemMessage(context, message, width, msgIdx);
222
+ }
223
+ else if (message.role === 'tool') {
224
+ // A result whose call is outside the rendered slice (the display was
225
+ // cleared mid-turn): one bead of its own, with its block and body.
226
+ const render = renderSingleBead(orphanResultBead(message, msgIdx, context.collapseState), {
227
+ width,
228
+ glyphSet: context.treeGlyphSet ?? 'rounded',
229
+ focusId: context.focusId ?? null,
230
+ lineNumberMode: 'off',
231
+ collapseThreshold: 30,
232
+ collapseState: context.collapseState,
233
+ isNavigableSystem: () => false,
234
+ frame: context.frame ?? 0,
235
+ });
236
+ const base = context.history.getLineCount();
237
+ const blockBase = context.blockRegistry.length;
238
+ context.history.addLines(render.lines);
239
+ for (const block of render.blocks) context.blockRegistry.push({ ...block, blockIndex: blockBase + block.blockIndex, startLine: base + block.startLine });
676
240
  }
677
241
  }
678
242
 
@@ -681,9 +245,9 @@ export function addConversationSplashScreen(
681
245
  width: number,
682
246
  ): void {
683
247
  const splashStrings = getSplashLines(width, context.splashOptions);
684
- const cyan = '#00ffff';
685
- const purple = '#d000ff';
686
- const grey = '244';
248
+ // The splash gradient is protected: it never follows the theme.
249
+ const { start: cyan, end: purple } = SPLASH_GRADIENT;
250
+ const versionFg = activeTokens().textFaint;
687
251
 
688
252
  splashStrings.forEach((str, y) => {
689
253
  const line = UIFactory.stringToLine(str, width);
@@ -695,8 +259,7 @@ export function addConversationSplashScreen(
695
259
  const cell = line[x];
696
260
  if (cell.char === ' ' && (x < startX || x >= endX)) continue;
697
261
  if (isVersion) {
698
- cell.fg = grey;
699
- cell.dim = true;
262
+ cell.fg = versionFg;
700
263
  } else {
701
264
  const factor = (x - startX) / (endX - startX || 1);
702
265
  cell.fg = interpolateColor(cyan, purple, Math.max(0, Math.min(1, factor)));
@@ -705,9 +268,8 @@ export function addConversationSplashScreen(
705
268
  }
706
269
  context.history.addLine(line);
707
270
  });
708
- for (let i = 0; i < 5; i++) {
709
- context.history.addLine(createEmptyLine(width));
710
- }
271
+ // No trailing blank rows: the shell centers the splash in whatever height
272
+ // the conversation area has (conversation-layout.ts centerViewportContent).
711
273
  }
712
274
 
713
275
  export function conversationTextToLines(
@@ -735,13 +297,12 @@ export function logConversationText(
735
297
  }
736
298
 
737
299
  /**
738
- * logConversationToolResult - Append a single tool-call-style result line to the
739
- * display history only, reusing renderToolCallBlock so a display-only render (e.g.
740
- * a slash command's subprocess result) is visually indistinguishable from the
741
- * model's own tool-call results. Display-only: never touches message history.
300
+ * logConversationToolResult - Append a display-only tool result (a slash
301
+ * command's subprocess result, e.g. /test) as one bead row, the same row a
302
+ * model's own call draws in the work tree. Never touches message history.
742
303
  */
743
304
  export function logConversationToolResult(
744
- context: Pick<ConversationRenderContext, 'history'>,
305
+ context: Pick<ConversationRenderContext, 'history' | 'treeGlyphSet'>,
745
306
  width: number,
746
307
  toolCall: ToolCall,
747
308
  status: 'done' | 'error',
@@ -749,5 +310,26 @@ export function logConversationToolResult(
749
310
  durationMs: number,
750
311
  errorMsg?: string,
751
312
  ): void {
752
- context.history.addLines(renderToolCallBlock(toolCall, status, resultSummary, width, durationMs, errorMsg));
313
+ renderStandaloneBead(context, width, toolCall, status, errorMsg ? `${resultSummary ? `${resultSummary} · ` : ''}${errorMsg}` : resultSummary, durationMs);
314
+ }
315
+
316
+ function renderStandaloneBead(
317
+ context: Pick<ConversationRenderContext, 'history' | 'treeGlyphSet'>,
318
+ width: number,
319
+ toolCall: ToolCall,
320
+ status: 'done' | 'error',
321
+ summary: string,
322
+ durationMs: number | undefined,
323
+ ): void {
324
+ const glyphs = laneGlyphs(context.treeGlyphSet ?? 'rounded');
325
+ context.history.addLine(paintBeadRow([{ role: 'bead', color: 0 }], {
326
+ status: status === 'error' ? 'err' : 'ok',
327
+ name: beadName(toolCall),
328
+ arg: beadArgument(toolCall),
329
+ summary: summary ? { text: cellText(summary), tone: status === 'error' ? 'bad' : 'faint' } : null,
330
+ time: durationMs !== undefined ? formatBeadTime(durationMs) : undefined,
331
+ laneColor: activeTokens().brand,
332
+ hasBody: false,
333
+ open: false,
334
+ }, { width, gutterWidth: 6, glyphs, frame: 0 }));
753
335
  }