@pellux/goodvibes-tui 2.0.20 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +59 -4
  5. package/package.json +16 -6
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * work-tree-wiring.ts, connecting the conversation work tree to the live
3
+ * runtime: the timing store fed by tool and turn events, agent lanes read
4
+ * from the agent manager (WRFC chains from the chain controller, cost from
5
+ * the fleet read model), the call a permission prompt is holding, and the
6
+ * per-session fold state.
7
+ *
8
+ * One call from main.ts; everything the work tree reads at render time goes
9
+ * through the WorkTreeSources this installs.
10
+ */
11
+
12
+ import type { AgentManager } from '@pellux/goodvibes-sdk/platform/tools';
13
+ import type { WrfcChain } from '@pellux/goodvibes-sdk/platform/agents';
14
+ import type { ProcessNode } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
15
+ import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
16
+ import type { UiRuntimeEvents } from '@/runtime/index.ts';
17
+ import type { ConversationManager } from './conversation.ts';
18
+ import { WorkTreeTimingStore, userMessageFingerprint, type AgentLaneInfo, type TurnOutcome, type WrfcPhaseInfo } from './work-tree-sources.ts';
19
+ import { loadWorkTreeFolds, loadWorkTreeTurnOutcomes, saveWorkTreeFolds, sweepOrphanWorkTreeFolds } from './work-tree-fold-store.ts';
20
+ import { onSemanticSummaryReady } from '../renderer/lane-graph/semantic-memo.ts';
21
+ import { onSyntaxHighlightReady } from '../renderer/code-block.ts';
22
+
23
+ type AgentRecord = ReturnType<AgentManager['list']>[number];
24
+
25
+ export interface WorkTreeWiringDeps {
26
+ readonly conversation: ConversationManager;
27
+ readonly events: UiRuntimeEvents;
28
+ readonly agentManager: Pick<AgentManager, 'getStatus' | 'getConversationSnapshot'>;
29
+ readonly listChains: () => readonly WrfcChain[];
30
+ readonly fleetNodes: () => readonly ProcessNode[];
31
+ /** The call id a permission prompt is holding, if any. */
32
+ readonly pendingCallId: () => string | undefined;
33
+ readonly turnActive: () => boolean;
34
+ readonly sessionsDir: string;
35
+ readonly sessionId: () => string;
36
+ /** Repaint (a ◈ summary of an opened edit landed). */
37
+ readonly requestRender: () => void;
38
+ }
39
+
40
+ function firstLines(text: string | undefined, n: number): string | undefined {
41
+ if (!text) return undefined;
42
+ const lines = text.split('\n').filter((l) => l.trim().length > 0).slice(0, n);
43
+ return lines.length > 0 ? lines.join('\n') : undefined;
44
+ }
45
+
46
+ /** A finished agent's transcript never changes: read it once. */
47
+ interface SnapshotMemo { readonly stamp: string; readonly messages: readonly ConversationMessageSnapshot[] }
48
+
49
+ export function wireWorkTree(deps: WorkTreeWiringDeps): { readonly timings: WorkTreeTimingStore; readonly unsubs: Array<() => void> } {
50
+ const timings = new WorkTreeTimingStore();
51
+ const unsubs: Array<() => void> = [];
52
+ const { conversation, events } = deps;
53
+
54
+ unsubs.push(events.tools.on('TOOL_EXECUTING', (ev) => timings.callStarted(ev.callId, ev.startedAt)));
55
+ unsubs.push(events.tools.on('TOOL_SUCCEEDED', (ev) => timings.callSettled(ev.callId, ev.durationMs, Date.now())));
56
+ unsubs.push(events.tools.on('TOOL_FAILED', (ev) => timings.callSettled(ev.callId, ev.durationMs, Date.now())));
57
+ unsubs.push(events.tools.on('TOOL_CANCELLED', (ev) => timings.callSettled(ev.callId, undefined, Date.now())));
58
+ // Everything the session sidecar keeps: fold decisions and failed/cancelled turn endings.
59
+ const persistView = (): void => saveWorkTreeFolds(deps.sessionsDir, deps.sessionId(), conversation.workTree.foldState(), conversation.workTree.turnOutcomes());
60
+
61
+ unsubs.push(events.turns.on('TURN_SUBMITTED', () => {
62
+ const snapshot = conversation.getMessageSnapshot();
63
+ for (let i = snapshot.length - 1; i >= 0; i--) {
64
+ if (snapshot[i]!.role === 'user') { timings.turnStarted(i, Date.now(), userMessageFingerprint(snapshot[i])); break; }
65
+ }
66
+ }));
67
+ const endTurn = (outcome: TurnOutcome): void => {
68
+ const ended = timings.turnEnded(Date.now(), outcome);
69
+ // The header drops "working" and states a failure on the next build.
70
+ conversation.workTree.invalidate();
71
+ if (!ended || ended.timing.fingerprint === undefined) return;
72
+ if (conversation.workTree.recordTurnOutcome({ index: ended.index, fingerprint: ended.timing.fingerprint, outcome })) persistView();
73
+ };
74
+ unsubs.push(events.turns.on('TURN_COMPLETED', (ev) => endTurn(ev.stopReason === 'empty_response' ? 'failed' : 'completed')));
75
+ unsubs.push(events.turns.on('TURN_ERROR', () => endTurn('failed')));
76
+ unsubs.push(events.turns.on('TURN_CANCEL', () => endTurn('cancelled')));
77
+ unsubs.push(events.turns.on('PREFLIGHT_FAIL', () => endTurn('failed')));
78
+ // A new or resumed transcript: turn timings are keyed by message index and belong to the old one.
79
+ unsubs.push(conversation.workTree.onReset(() => timings.clear()));
80
+
81
+ const snapshots = new Map<string, SnapshotMemo>();
82
+ const messagesOf = (record: AgentRecord): readonly ConversationMessageSnapshot[] => {
83
+ const finished = record.status !== 'running' && record.status !== 'pending';
84
+ const stamp = `${record.status}:${record.completedAt ?? ''}:${record.toolCallCount}`;
85
+ const memo = snapshots.get(record.id);
86
+ if (finished && memo && memo.stamp === stamp) return memo.messages;
87
+ const messages = deps.agentManager.getConversationSnapshot(record.id);
88
+ if (finished) snapshots.set(record.id, { stamp, messages });
89
+ return messages;
90
+ };
91
+
92
+ const costOf = (agentId: string): number | undefined => {
93
+ const node = deps.fleetNodes().find((n) => n.id === agentId);
94
+ // Only a real reading: an unpriced node has no cost to state.
95
+ return node && node.costState !== 'unpriced' && typeof node.costUsd === 'number' ? node.costUsd : undefined;
96
+ };
97
+
98
+ const wrfcPhases = (record: AgentRecord): { phases: WrfcPhaseInfo[]; passed: boolean | undefined } | undefined => {
99
+ const chain = deps.listChains().find((c) => c.ownerAgentId === record.id || (c.engineerAgentId === record.id && c.ownerAgentId === c.engineerAgentId));
100
+ if (!chain) return undefined;
101
+ const phases: WrfcPhaseInfo[] = [];
102
+ for (const id of chain.allAgentIds) {
103
+ if (id === chain.ownerAgentId && id !== chain.engineerAgentId) continue;
104
+ const phase = deps.agentManager.getStatus(id);
105
+ if (!phase) continue;
106
+ const role = (phase.wrfcRole ?? (id === chain.engineerAgentId ? 'engineer' : id === chain.reviewerAgentId ? 'reviewer' : id === chain.fixerAgentId ? 'fixer' : 'engineer')) as WrfcPhaseInfo['role'];
107
+ const latestReview = role === 'reviewer' && id === chain.reviewerAgentId ? chain.reviewerReport : undefined;
108
+ phases.push({
109
+ agentId: id,
110
+ role,
111
+ task: phase.task,
112
+ status: phase.status,
113
+ startedAt: phase.startedAt,
114
+ completedAt: phase.completedAt,
115
+ findings: latestReview?.issues.map((issue) => issue.description),
116
+ passed: latestReview ? (chain.lastReviewVerdict?.passed ?? latestReview.passed) : undefined,
117
+ output: firstLines(phase.fullOutput, 6),
118
+ });
119
+ }
120
+ phases.sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
121
+ const passed = chain.state === 'passed' ? true : chain.state === 'failed' ? false : undefined;
122
+ return { phases, passed };
123
+ };
124
+
125
+ const agent = (agentId: string): AgentLaneInfo | null => {
126
+ const record = deps.agentManager.getStatus(agentId);
127
+ if (!record) return null;
128
+ const wrfc = wrfcPhases(record);
129
+ return {
130
+ id: record.id,
131
+ name: record.template,
132
+ task: record.task,
133
+ status: record.status,
134
+ startedAt: record.startedAt,
135
+ completedAt: record.completedAt,
136
+ toolCallCount: record.toolCallCount,
137
+ costUsd: costOf(record.id),
138
+ error: record.error,
139
+ messages: wrfc ? [] : messagesOf(record),
140
+ wrfcPhases: wrfc?.phases,
141
+ wrfcPassed: wrfc?.passed,
142
+ };
143
+ };
144
+
145
+ conversation.setWorkTreeSources({
146
+ callTiming: (callId) => timings.callTiming(callId),
147
+ turnTiming: (index) => timings.turnTiming(index),
148
+ agent,
149
+ waitingCallIds: () => {
150
+ const id = deps.pendingCallId();
151
+ return id ? new Set([id]) : new Set<string>();
152
+ },
153
+ turnActive: deps.turnActive,
154
+ turnOutcome: (index) => conversation.workTree.turnOutcome(index),
155
+ now: () => Date.now(),
156
+ });
157
+
158
+ // Fold decisions persist per session, beside the session file.
159
+ conversation.workTree.onFoldChange(persistView);
160
+ sweepOrphanWorkTreeFolds(deps.sessionsDir);
161
+ unsubs.push(onSemanticSummaryReady(() => { conversation.workTree.invalidate(); deps.requestRender(); }));
162
+ // A tree-sitter parse landed: code drawn with the regex placeholder is redrawn with it.
163
+ unsubs.push(onSyntaxHighlightReady(() => { conversation.workTree.invalidate(); deps.requestRender(); }));
164
+ return { timings, unsubs };
165
+ }
166
+
167
+ /**
168
+ * Restore a resumed session's fold decisions and failed/cancelled turn
169
+ * endings (called by the resume routine, after the transcript is restored).
170
+ */
171
+ export function restoreWorkTreeFolds(conversation: ConversationManager, sessionsDir: string, sessionId: string): number {
172
+ const entries = loadWorkTreeFolds(sessionsDir, sessionId);
173
+ conversation.workTree.restoreFoldState(entries);
174
+ conversation.workTree.restoreTurnOutcomes(loadWorkTreeTurnOutcomes(sessionsDir, sessionId));
175
+ return entries.length;
176
+ }
@@ -18,11 +18,17 @@
18
18
  * their reasons, which is the same principle the channel renderer follows: in
19
19
  * plain words, never by an opaque identifier.
20
20
  *
21
+ * The title names the work (owner ruling 2026-09-29): the workstream's task,
22
+ * trimmed at a word boundary, and the body adds the last review score when
23
+ * one was reached. behavior.notificationsMetadataOnly (default off) drops the
24
+ * task, the reason and the score, leaving what kind of stop it was.
25
+ *
21
26
  * Split out of turn-event-wiring.ts as a pure function so the text is testable
22
27
  * on its own, the wiring itself cannot be asserted against without mocking a
23
28
  * process-global notifier.
24
29
  */
25
30
  import { formatTurnBudgetOutcome } from './turn-budget-outcome.ts';
31
+ import { NOTIFICATION_TEXT_LIMITS, trimAtWordBoundary } from '@pellux/goodvibes-sdk/platform/runtime/operations';
26
32
 
27
33
  /** The fields of WORKFLOW_CHAIN_FAILED this narration reads. */
28
34
  export interface WorkstreamFailureNarrationInput {
@@ -32,11 +38,32 @@ export interface WorkstreamFailureNarrationInput {
32
38
  readonly turnLimitSource?: 'default' | 'spawn-override' | 'policy-bound' | undefined;
33
39
  }
34
40
 
41
+ /** What the host remembers about the workstream, and the privacy setting. */
42
+ export interface WorkstreamNotificationContext {
43
+ /** The workstream's task, from WORKFLOW_CHAIN_CREATED. */
44
+ readonly task?: string | null | undefined;
45
+ /** The last review score (out of 10), from WORKFLOW_REVIEW_COMPLETED. */
46
+ readonly reviewScore?: number | null | undefined;
47
+ /** behavior.notificationsMetadataOnly */
48
+ readonly metadataOnly?: boolean | undefined;
49
+ }
50
+
35
51
  export interface WorkstreamNotification {
36
52
  readonly title: string;
37
53
  readonly body: string;
38
54
  }
39
55
 
56
+ function titled(outcome: string, context: WorkstreamNotificationContext): string {
57
+ const prefix = `Workstream ${outcome}: `;
58
+ const task = context.metadataOnly ? '' : trimAtWordBoundary(context.task ?? '', NOTIFICATION_TEXT_LIMITS.desktopTitle - prefix.length);
59
+ return task ? `${prefix}${task}` : `GoodVibes: workstream ${outcome}`;
60
+ }
61
+
62
+ function withScore(body: string, context: WorkstreamNotificationContext): string {
63
+ if (context.metadataOnly || typeof context.reviewScore !== 'number' || !Number.isFinite(context.reviewScore)) return body;
64
+ return `${body} (last review ${Math.round(context.reviewScore * 10) / 10}/10)`;
65
+ }
66
+
40
67
  /**
41
68
  * Title and body for a workstream that reached a terminal state.
42
69
  *
@@ -49,21 +76,24 @@ export interface WorkstreamNotification {
49
76
  */
50
77
  export function workstreamFailureNotification(
51
78
  payload: WorkstreamFailureNarrationInput,
79
+ context: WorkstreamNotificationContext = {},
52
80
  ): WorkstreamNotification {
81
+ const metadataOnly = context.metadataOnly === true;
53
82
  if (payload.failureKind === 'cancelled') {
54
83
  return {
55
- title: 'GoodVibes: workstream cancelled',
56
- body: `Cancelled: ${payload.reason}`,
84
+ title: titled('cancelled', context),
85
+ body: metadataOnly ? 'Cancelled' : withScore(`Cancelled: ${payload.reason}`, context),
57
86
  };
58
87
  }
59
88
  if (payload.failureKind === 'max_turns') {
60
89
  return {
61
- title: 'GoodVibes: workstream hit its turn budget',
62
- body: `The workstream ${formatTurnBudgetOutcome({ limit: payload.turnLimit, source: payload.turnLimitSource })}`,
90
+ title: titled('hit its turn budget', context),
91
+ body: withScore(`The workstream ${formatTurnBudgetOutcome({ limit: payload.turnLimit, source: payload.turnLimitSource })}`, context),
63
92
  };
64
93
  }
94
+ const reason = payload.failureKind === 'transport' ? 'transient transport error' : payload.reason;
65
95
  return {
66
- title: 'GoodVibes: workstream failed',
67
- body: `Failed: ${payload.failureKind === 'transport' ? 'transient transport error' : payload.reason}`,
96
+ title: titled('failed', context),
97
+ body: metadataOnly ? (payload.failureKind === 'transport' ? `Failed: ${reason}` : 'Failed') : withScore(`Failed: ${reason}`, context),
68
98
  };
69
99
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * wrfc-notice-titles.ts, the plain title of each review-chain line this
3
+ * terminal writes itself.
4
+ *
5
+ * runtime/bootstrap-core.ts and runtime/wrfc-persistence.ts write a few
6
+ * "[WRFC] …" lines of their own (the SDK's lines are titled by its
7
+ * runtimeEventOfNotice). The notification history keeps each such event as
8
+ * one entry under a plain title, with the line's detail as the body
9
+ * (core/notices.ts). The test feeds every producer's template through
10
+ * shellChainNoticeOf so the lines and this table cannot drift apart.
11
+ */
12
+
13
+ export interface ShellChainNotice {
14
+ readonly title: string;
15
+ readonly level: 'info' | 'warning';
16
+ /** The line without its bracket tag or status mark. */
17
+ readonly detail: string;
18
+ }
19
+
20
+ const SHELL_CHAIN_LINES: ReadonlyArray<{ readonly pattern: RegExp; readonly title: string; readonly level: ShellChainNotice['level'] }> = [
21
+ { pattern: /^\[WRFC\] Engineer enumerated \d+ constraints? for chain \S+/, title: 'Review chain constraints listed', level: 'info' },
22
+ { pattern: /^\[WRFC\] Fix #\d+ targeting \d+ constraints? on chain \S+/, title: 'Review chain fix started', level: 'info' },
23
+ { pattern: /^\[WRFC\] ✗ Chain \S+: \d+ constraint violations? forced failure/, title: 'Review chain failed its constraints', level: 'warning' },
24
+ { pattern: /^\[WRFC\] Guard: .*\(spawn-forced-wrfc\)$/s, title: 'Agent request sent through a review chain', level: 'info' },
25
+ { pattern: /^\[WRFC\] Guard: .*\(spawn-suppressed-wrfc\)$/s, title: 'Agent request kept out of a review chain', level: 'info' },
26
+ { pattern: /^\[WRFC\] Guard: .*\(batch-collapsed-to-wrfc\)$/s, title: 'Agent requests combined into one review chain', level: 'info' },
27
+ { pattern: /^\[WRFC\] Chain \S+ \(.*\) was interrupted by a restart; /s, title: 'Review chain interrupted by a restart', level: 'warning' },
28
+ { pattern: /^\[WRFC\] Pre-router buffer overflowed: /, title: 'Early review chain messages dropped', level: 'warning' },
29
+ ];
30
+
31
+ /** The plain title of a review-chain line this terminal writes, or undefined for any other line. */
32
+ export function shellChainNoticeOf(text: string): ShellChainNotice | undefined {
33
+ const line = text.trim();
34
+ const match = SHELL_CHAIN_LINES.find((entry) => entry.pattern.test(line));
35
+ if (!match) return undefined;
36
+ return { title: match.title, level: match.level, detail: line.replace(/^\[WRFC\]\s*/, '').replace(/^✗\s*/, '') };
37
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * agents-modal-text.ts, the plain-text pieces the Agents modal shows: the
3
+ * hosted session's header facts and rows, and a compact live tail of an
4
+ * agent's conversation for the side column.
5
+ *
6
+ * Every hosted fact is a field of the daemon's own record; nothing is derived
7
+ * here. The tail shows what an agent said and did most recently: its words,
8
+ * the tools it called (→) and the first line of each result (←).
9
+ */
10
+
11
+ import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
12
+ import { createEmptyLine, type Line } from '@pellux/goodvibes-sdk/platform/types';
13
+ import type { HostedSessionFeedState, HostedSessionRow } from '../views/hosted-session-feed.ts';
14
+ import type { AgentsText } from '../renderer/agents-modal.ts';
15
+ import { activeTokens } from '../renderer/theme.ts';
16
+ import { wrapText, getDisplayWidth } from '../utils/terminal-width.ts';
17
+
18
+ /** What the hosted session is, what it is doing, and what leaving it would do. */
19
+ export function hostedHeaderTexts(state: HostedSessionFeedState): AgentsText[] {
20
+ const record = state.record;
21
+ if (!record) return [];
22
+ const lines: AgentsText[] = [
23
+ { text: `${record.status} · ${record.turnCount} turn${record.turnCount === 1 ? '' : 's'} · ${record.messageCount} message${record.messageCount === 1 ? '' : 's'}`, tone: 'text' },
24
+ { text: `workspace ${record.workspaceRoot}`, tone: 'faint' },
25
+ {
26
+ text: `${record.effectiveDetachPolicy === 'survive' ? 'leaving keeps it running (survive)' : 'leaving ends it (kill)'}, ${record.detachPolicy === null ? 'from the setting' : 'set for this session'}`,
27
+ tone: record.effectiveDetachPolicy === 'survive' ? 'success' : 'warning',
28
+ },
29
+ { text: `attached: ${record.attachedClients.length > 0 ? record.attachedClients.join(', ') : 'nobody'}`, tone: 'muted' },
30
+ ];
31
+ if (record.status === 'terminated') lines.push({ text: `ended: ${record.terminatedReason ?? 'no reason recorded'}`, tone: 'error' });
32
+ if (record.restoredFromDisk) lines.push({ text: 'restored from disk after a daemon restart', tone: 'warning' });
33
+ lines.push(state.streaming
34
+ ? { text: 'live event stream open', tone: 'success' }
35
+ : { text: `no live stream: ${state.streamNote ?? 'not subscribed'}`, tone: 'warning' });
36
+ if (state.runningToolCalls.length > 0) {
37
+ lines.push({ text: `running: ${state.runningToolCalls.map((call) => call.tool).join(', ')}`, tone: 'brand' });
38
+ }
39
+ if (state.droppedRows > 0) lines.push({ text: `${state.droppedRows} earlier row(s) dropped; reattach to backfill`, tone: 'faint' });
40
+ return lines;
41
+ }
42
+
43
+ /** The hosted conversation's rows as text. */
44
+ export function hostedRowTexts(rows: readonly HostedSessionRow[]): AgentsText[] {
45
+ return rows.map((row) => {
46
+ const prefix = row.kind === 'user' ? 'you: ' : row.kind === 'tool' ? '' : row.kind === 'error' ? 'error: ' : '';
47
+ const text = `${prefix}${row.text}${row.streaming ? ' …' : ''}`;
48
+ if (row.kind === 'tool') return { text, tone: 'brand', glyph: '→' };
49
+ if (row.kind === 'error') return { text, tone: 'error' };
50
+ if (row.kind === 'user') return { text, tone: 'text', bold: true };
51
+ if (row.kind === 'system') return { text, tone: 'faint' };
52
+ return { text, tone: 'text' };
53
+ });
54
+ }
55
+
56
+ /** The hosted transcript as lines at a width (text from column 5, like the transcript). */
57
+ export function hostedBody(rows: readonly HostedSessionRow[], width: number, height: number): Line[] {
58
+ const t = activeTokens();
59
+ const out: Line[] = [];
60
+ const textWidth = Math.max(8, width - 9);
61
+ for (const item of hostedRowTexts(rows)) {
62
+ const fg = item.tone === 'error' ? t.error : item.tone === 'brand' ? t.brand : item.tone === 'faint' ? t.textFaint : t.text;
63
+ const text = item.glyph ? `${item.glyph} ${item.text}` : item.text;
64
+ for (const part of wrapText(text, textWidth)) {
65
+ const line = createEmptyLine(width);
66
+ let x = 5;
67
+ for (const ch of part) {
68
+ const w = getDisplayWidth(ch);
69
+ if (w <= 0 || x + w > width - 4) break;
70
+ line[x] = { ...line[x]!, char: ch, fg, bold: item.bold === true };
71
+ if (w === 2) line[x + 1] = { ...line[x]!, char: '' };
72
+ x += w;
73
+ }
74
+ out.push(line);
75
+ }
76
+ }
77
+ return out.slice(-Math.max(1, height));
78
+ }
79
+
80
+ function contentText(content: string | ReadonlyArray<{ type: string; text?: string }>): string {
81
+ if (typeof content === 'string') return content;
82
+ return content.map((part) => (part.type === 'text' ? part.text ?? '' : `[${part.type}]`)).join(' ');
83
+ }
84
+
85
+ /** The one argument that says what a tool call is about (a path, a command, a query). */
86
+ function callSubject(args: Record<string, unknown>): string {
87
+ for (const key of ['path', 'file_path', 'filePath', 'command', 'cmd', 'pattern', 'query', 'url', 'name']) {
88
+ const value = args[key];
89
+ if (typeof value === 'string' && value.trim()) return value.trim();
90
+ }
91
+ return '';
92
+ }
93
+
94
+ function firstLine(text: string): string {
95
+ return text.split('\n').find((line) => line.trim().length > 0)?.trim() ?? '';
96
+ }
97
+
98
+ /** The most recent `limit` entries of an agent's conversation, as tail lines. */
99
+ export function transcriptTail(snapshot: readonly ConversationMessageSnapshot[], limit: number): AgentsText[] {
100
+ const out: AgentsText[] = [];
101
+ for (const message of snapshot) {
102
+ if (message.role === 'user') {
103
+ const text = firstLine(contentText(message.content as string));
104
+ if (text) out.push({ text: `you: ${text}`, tone: 'text', bold: true });
105
+ } else if (message.role === 'assistant') {
106
+ const text = message.content.trim();
107
+ if (text) for (const paragraph of text.split(/\n\s*\n/).slice(-2)) out.push({ text: paragraph.replace(/\s+/g, ' ').trim(), tone: 'text' });
108
+ for (const call of message.toolCalls ?? []) {
109
+ const subject = callSubject(call.arguments);
110
+ out.push({ text: subject ? `${call.name} ${subject}` : call.name, tone: 'muted', glyph: '→' });
111
+ }
112
+ } else if (message.role === 'tool') {
113
+ const text = firstLine(message.content);
114
+ out.push({ text: `${message.toolName ?? 'result'}${text ? `: ${text}` : ''}`, tone: 'faint', glyph: '←' });
115
+ }
116
+ }
117
+ if (out.length === 0) out.push({ text: 'No messages yet.', tone: 'faint' });
118
+ return out.slice(-limit);
119
+ }