@pellux/goodvibes-tui 2.0.21 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +16 -6
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -1,746 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // CostTrackerPanel, per-session / per-agent / per-plan cost estimates
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { Line } from '@pellux/goodvibes-sdk/platform/types';
6
- import { createStyledCell, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
7
- import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
8
- import { BasePanel } from './base-panel.ts';
9
- import type { AgentEvent, TurnEvent } from '@/runtime/index.ts';
10
- import type { UiEventFeed } from '@/runtime/index.ts';
11
- import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
12
- import {
13
- buildEmptyState,
14
- buildKeyboardHints,
15
- buildMeterLine,
16
- buildPanelLine,
17
- buildStyledPanelLine,
18
- buildTable,
19
- buildPanelWorkspace,
20
- resolveScrollablePanelSection,
21
- extendPalette,
22
- DEFAULT_PANEL_PALETTE,
23
- type PanelWorkspaceSection,
24
- } from './polish.ts';
25
- import { calcSessionCost, describePricingSource, isModelPriced, computeBudgetBreach, readBudgetAlertUsd, writeManualModelPrice, BUDGET_ALERT_USD_CONFIG_KEY, type BudgetAlertConfigAccess } from '@pellux/goodvibes-sdk/platform/providers';
26
- import { abbreviateCount } from '../utils/format-number.ts';
27
- import { isTextBackspace } from '../input/delete-key-policy.ts';
28
-
29
- // Pricing lookups are provided by ../export/cost-utils.ts (single source of truth).
30
-
31
- /** unpriced=true renders the explicit "price unknown" marker instead of a $0.00 that could be mistaken for a real zero cost. */
32
- function formatCost(usd: number, unpriced = false): string {
33
- if (unpriced) return 'price unknown';
34
- if (usd === 0) return '$0.00';
35
- if (usd < 0.0001) return '<$0.0001';
36
- if (usd < 0.01) return `$${usd.toFixed(4)}`;
37
- return `$${usd.toFixed(3)}`;
38
- }
39
-
40
- function formatTokens(n: number): string {
41
- return abbreviateCount(n);
42
- }
43
-
44
- // ---------------------------------------------------------------------------
45
- // Internal types
46
- // ---------------------------------------------------------------------------
47
-
48
- interface UsageSnapshot {
49
- input: number;
50
- output: number;
51
- cacheRead: number;
52
- cacheWrite: number;
53
- }
54
-
55
- interface AgentEntry {
56
- id: string; // short form, e.g. first 8 chars
57
- task: string;
58
- model: string;
59
- inputTokens: number;
60
- outputTokens: number;
61
- cost: number;
62
- status: 'running' | 'done' | 'failed';
63
- }
64
-
65
- // Sparkline history (rolling last N cost-per-turn values)
66
- const SPARKLINE_BARS = '._-:=+*#';
67
- const SPARKLINE_LEN = 16;
68
-
69
- function buildSparkline(history: number[]): string {
70
- if (history.length === 0) return '';
71
- const max = Math.max(...history);
72
- if (max === 0) return '.'.repeat(history.length);
73
- return history
74
- .map(v => {
75
- const idx = Math.round((v / max) * (SPARKLINE_BARS.length - 1));
76
- return SPARKLINE_BARS[idx] ?? '.';
77
- })
78
- .join('');
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // Colour palette
83
- // ---------------------------------------------------------------------------
84
-
85
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
86
- cost: '#ffdd44',
87
- model: '#88aaff',
88
- running: '#88aaff',
89
- separator: '#333333',
90
- bg: '',
91
- });
92
-
93
- // ---------------------------------------------------------------------------
94
- // CostTrackerPanel
95
- // ---------------------------------------------------------------------------
96
-
97
- export class CostTrackerPanel extends BasePanel {
98
- // Session-level usage (from main orchestrator)
99
- private sessionUsage: UsageSnapshot = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
100
- private sessionModel = 'unknown';
101
-
102
- // Per-turn cost history for sparkline
103
- private costHistory: number[] = [];
104
- private lastSessionCost = 0;
105
-
106
- // Per-agent tracking (keyed by agent id)
107
- private agents = new Map<string, AgentEntry>();
108
-
109
- // Budget alert threshold in USD (0 = disabled). Mutable at runtime via the
110
- // in-panel 'b' numeric entry or the /cost budget <usd> command, both call
111
- // setBudgetThreshold() below. When configAccess is wired (production path),
112
- // this field is a cache only, currentBudgetThreshold() re-reads
113
- // behavior.budgetAlertUsd from config on every use so the panel and the
114
- // background budget-breach notifier (core/budget-breach-notifier.ts) never
115
- // disagree about the threshold. Tests that omit configAccess fall back to
116
- // this field as the sole source of truth, unchanged from prior behavior.
117
- private budgetThreshold: number;
118
-
119
- // Optional config-backed source of truth for the budget threshold. Absent
120
- // in most existing unit tests (which construct the panel directly) and in
121
- // any build that doesn't wire a config manager; present in production via
122
- // builtin/development.ts.
123
- private readonly configAccess: BudgetAlertConfigAccess | undefined;
124
-
125
- // Draft buffer for the in-panel budget-entry mode ('b' key). Non-null while
126
- // entry is active; unlike LocalAuthPanel's masked entry, the value is not
127
- // secret so it is echoed directly.
128
- private budgetEntry: string | null = null;
129
-
130
- // Draft buffer for the in-panel manual-price entry mode ('p' key): the one
131
- // action away from any price display (and from the "price unknown" marker).
132
- // Format: "input,output" in USD per 1M tokens; persists to the
133
- // pricing.modelPrices settings key live on Enter.
134
- private priceEntry: string | null = null;
135
-
136
- // Scroll offset for agent list
137
- private scrollOffset = 0;
138
-
139
- // Unsubscribe functions
140
- private unsubs: Array<() => void> = [];
141
-
142
- // Polls getAgentStatus() for agents still marked 'running' so their token/
143
- // cost columns fill in as usage streams, instead of staying pinned at
144
- // 'unknown'/$0 until AGENT_COMPLETED fires.
145
- private statusPollTimer: ReturnType<typeof setInterval> | null = null;
146
-
147
- // Getter for live orchestrator usage
148
- private readonly getOrchestratorUsage: () => UsageSnapshot & { model?: string };
149
-
150
- // Optional resolver for agent usage on completion, enables real cost attribution.
151
- // When omitted, completed agents show $0 (honest: data unavailable).
152
- private readonly getAgentStatus: ((agentId: string) => AgentRecord | null) | undefined;
153
-
154
- constructor(
155
- turnEvents: UiEventFeed<TurnEvent>,
156
- agentEvents: UiEventFeed<AgentEvent>,
157
- getOrchestratorUsage: () => UsageSnapshot & { model?: string },
158
- opts: {
159
- budgetThreshold?: number;
160
- getAgentStatus?: (agentId: string) => AgentRecord | null;
161
- configAccess?: BudgetAlertConfigAccess;
162
- } = {},
163
- ) {
164
- super('cost', 'Cost', '$', 'providers');
165
- this.getOrchestratorUsage = getOrchestratorUsage;
166
- this.getAgentStatus = opts.getAgentStatus;
167
- this.configAccess = opts.configAccess;
168
- this.budgetThreshold = this.configAccess ? readBudgetAlertUsd(this.configAccess.get) : (opts.budgetThreshold ?? 0);
169
- this.attachEvents(turnEvents, agentEvents);
170
- }
171
-
172
- /** The live budget threshold: re-read from config when configAccess is wired
173
- * (single source of truth shared with the background notifier), otherwise
174
- * the locally-cached field (test/no-config-manager fallback). */
175
- private currentBudgetThreshold(): number {
176
- return this.configAccess ? readBudgetAlertUsd(this.configAccess.get) : this.budgetThreshold;
177
- }
178
-
179
- // -------------------------------------------------------------------------
180
- // Bus wiring
181
- // -------------------------------------------------------------------------
182
-
183
- private attachEvents(turnEvents: UiEventFeed<TurnEvent>, agentEvents: UiEventFeed<AgentEvent>): void {
184
- // Refresh after every completed turn
185
- this.unsubs.push(
186
- turnEvents.on('TURN_COMPLETED', () => this.refreshSessionCost()),
187
- );
188
-
189
- // Refresh mid-turn too: a single turn can span many LLM calls (tool
190
- // loops), so waiting for TURN_COMPLETED leaves the meter frozen for the
191
- // whole turn. LLM_RESPONSE_RECEIVED fires per call, same refresh path.
192
- this.unsubs.push(
193
- turnEvents.on('LLM_RESPONSE_RECEIVED', () => this.refreshSessionCost()),
194
- );
195
-
196
- // Track agent spawns
197
- this.unsubs.push(
198
- agentEvents.on('AGENT_SPAWNING', (payload) => {
199
- this.agents.set(payload.agentId, {
200
- id: payload.agentId.slice(0, 8),
201
- task: truncateDisplay(payload.task, 40),
202
- model: 'unknown',
203
- inputTokens: 0,
204
- outputTokens: 0,
205
- cost: 0,
206
- status: 'running',
207
- });
208
- this.markDirty();
209
- }),
210
- );
211
-
212
- // Agent completed, capture real token usage via AgentRecord when available
213
- this.unsubs.push(
214
- agentEvents.on('AGENT_COMPLETED', (payload) => {
215
- const entry = this.agents.get(payload.agentId);
216
- if (entry) {
217
- entry.status = 'done';
218
- if (this.getAgentStatus) {
219
- const rec = this.getAgentStatus(payload.agentId);
220
- if (rec?.usage) {
221
- entry.inputTokens = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
222
- entry.outputTokens = rec.usage.outputTokens;
223
- entry.cost = calcSessionCost(rec.usage.inputTokens, rec.usage.outputTokens, rec.usage.cacheReadTokens ?? 0, rec.usage.cacheWriteTokens ?? 0, rec.model ?? 'unknown');
224
- if (rec.model && rec.model !== 'unknown') entry.model = rec.model;
225
- }
226
- }
227
- this.markDirty();
228
- }
229
- }),
230
- );
231
-
232
- // Agent error
233
- this.unsubs.push(
234
- agentEvents.on('AGENT_FAILED', (payload) => {
235
- const entry = this.agents.get(payload.agentId);
236
- if (entry) {
237
- entry.status = 'failed';
238
- this.markDirty();
239
- }
240
- }),
241
- );
242
- }
243
-
244
- private refreshSessionCost(): void {
245
- const usage = this.getOrchestratorUsage();
246
- this.sessionUsage = {
247
- input: usage.input,
248
- output: usage.output,
249
- cacheRead: usage.cacheRead,
250
- cacheWrite: usage.cacheWrite,
251
- };
252
- if (usage.model) this.sessionModel = usage.model;
253
-
254
- // Record cost delta for sparkline
255
- const totalCost = calcSessionCost(usage.input, usage.output, usage.cacheRead, usage.cacheWrite, this.sessionModel);
256
- const delta = Math.max(0, totalCost - this.lastSessionCost);
257
- this.lastSessionCost = totalCost;
258
- this.costHistory.push(delta);
259
- if (this.costHistory.length > SPARKLINE_LEN) {
260
- this.costHistory.shift();
261
- }
262
-
263
- this.markDirty();
264
- }
265
-
266
- // -------------------------------------------------------------------------
267
- // Lifecycle
268
- // -------------------------------------------------------------------------
269
-
270
- override onActivate(): void {
271
- // Sync latest usage on activation
272
- const usage = this.getOrchestratorUsage();
273
- this.sessionUsage = {
274
- input: usage.input,
275
- output: usage.output,
276
- cacheRead: usage.cacheRead,
277
- cacheWrite: usage.cacheWrite,
278
- };
279
- this.needsRender = true;
280
- this.startStatusPollTimer();
281
- }
282
-
283
- override onDeactivate(): void {
284
- // Stop polling while hidden, nothing to catch up on that a fresh
285
- // onActivate() poll won't cover.
286
- this.stopStatusPollTimer();
287
- }
288
-
289
- override onDestroy(): void {
290
- for (const unsub of this.unsubs) unsub();
291
- this.unsubs = [];
292
- }
293
-
294
- private startStatusPollTimer(): void {
295
- if (this.statusPollTimer !== null || !this.getAgentStatus) return;
296
- this.statusPollTimer = this.registerTimer(setInterval(() => this.pollRunningAgents(), 3_000));
297
- }
298
-
299
- private stopStatusPollTimer(): void {
300
- if (this.statusPollTimer !== null) {
301
- this.clearTimer(this.statusPollTimer);
302
- this.statusPollTimer = null;
303
- }
304
- }
305
-
306
- /**
307
- * Poll getAgentStatus() for every agent still marked 'running'. Completion
308
- * already captures real usage via AGENT_COMPLETED, but a long-running agent
309
- * would otherwise show 'unknown' tokens / $0 cost for its entire lifetime,
310
- * this fills the row in as usage becomes available mid-flight.
311
- */
312
- private pollRunningAgents(): void {
313
- if (!this.getAgentStatus) return;
314
- let changed = false;
315
- for (const [agentId, entry] of this.agents) {
316
- if (entry.status !== 'running') continue;
317
- const rec = this.getAgentStatus(agentId);
318
- if (!rec?.usage) continue;
319
- const inputTokens = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
320
- const outputTokens = rec.usage.outputTokens;
321
- const cost = calcSessionCost(rec.usage.inputTokens, rec.usage.outputTokens, rec.usage.cacheReadTokens ?? 0, rec.usage.cacheWriteTokens ?? 0, rec.model ?? entry.model);
322
- if (inputTokens !== entry.inputTokens || outputTokens !== entry.outputTokens || cost !== entry.cost) {
323
- entry.inputTokens = inputTokens;
324
- entry.outputTokens = outputTokens;
325
- entry.cost = cost;
326
- changed = true;
327
- }
328
- if (rec.model && rec.model !== 'unknown' && rec.model !== entry.model) {
329
- entry.model = rec.model;
330
- changed = true;
331
- }
332
- }
333
- if (changed) this.markDirty();
334
- }
335
-
336
- /** Set the budget alert threshold (USD; 0 disables it). Shared by the
337
- * in-panel 'b' entry and the /cost budget <usd> command. */
338
- public setBudgetThreshold(usd: number): void {
339
- if (!Number.isFinite(usd) || usd < 0) return;
340
- this.budgetThreshold = usd;
341
- this.configAccess?.set(BUDGET_ALERT_USD_CONFIG_KEY, usd);
342
- this.markDirty();
343
- }
344
-
345
- // -------------------------------------------------------------------------
346
- // Input
347
- // -------------------------------------------------------------------------
348
-
349
- /**
350
- * The budget-threshold entry field wants every character of a burst
351
- * (paste, or fast typing landing in one input.feed() call) delivered one
352
- * at a time, same as it always has, see the interface doc on
353
- * `Panel.isCapturingTextBurst`.
354
- */
355
- isCapturingTextBurst(): boolean {
356
- return this.budgetEntry !== null || this.priceEntry !== null;
357
- }
358
-
359
- handleInput(key: string): boolean {
360
- if (this.budgetEntry !== null) return this.handleBudgetEntryInput(key);
361
- if (this.priceEntry !== null) return this.handlePriceEntryInput(key);
362
-
363
- if (key === 'b') {
364
- const threshold = this.currentBudgetThreshold();
365
- this.budgetEntry = threshold > 0 ? String(threshold) : '';
366
- this.markDirty();
367
- return true;
368
- }
369
-
370
- if (key === 'p') {
371
- // One action from any price display: edit the manual price for the
372
- // session's provider:model. Needs a wired config (production path),
373
- // without one the key falls through rather than opening a dead editor.
374
- if (!this.configAccess) return false;
375
- this.priceEntry = '';
376
- this.markDirty();
377
- return true;
378
- }
379
-
380
- switch (key) {
381
- case 'up': return this.scroll(-1);
382
- case 'down': return this.scroll(1);
383
- case 'pageup': return this.scroll(-10);
384
- case 'pagedown': return this.scroll(10);
385
- default: return false;
386
- }
387
- }
388
-
389
- private handleBudgetEntryInput(key: string): boolean {
390
- if (key === 'escape') {
391
- this.budgetEntry = null;
392
- this.markDirty();
393
- return true;
394
- }
395
- if (key === 'enter' || key === 'return') {
396
- const raw = this.budgetEntry ?? '';
397
- this.budgetEntry = null;
398
- const parsed = Number(raw);
399
- if (raw.length > 0 && Number.isFinite(parsed) && parsed >= 0) {
400
- this.setBudgetThreshold(parsed);
401
- } else {
402
- this.markDirty();
403
- }
404
- return true;
405
- }
406
- if (isTextBackspace(key)) {
407
- if (this.budgetEntry && this.budgetEntry.length > 0) {
408
- this.budgetEntry = this.budgetEntry.slice(0, -1);
409
- this.markDirty();
410
- }
411
- return true;
412
- }
413
- // Digits and a single decimal point only, this is a USD amount, not free text.
414
- if (key.length === 1 && /[0-9.]/.test(key) && !(key === '.' && this.budgetEntry?.includes('.'))) {
415
- this.budgetEntry = (this.budgetEntry ?? '') + key;
416
- this.markDirty();
417
- return true;
418
- }
419
- return true; // absorb everything else while entry is active
420
- }
421
-
422
- /** True when the session model can carry a manual price (pricing.modelPrices keys are `provider:model`). */
423
- private canEditSessionModelPrice(): boolean {
424
- return this.sessionModel !== 'unknown' && this.sessionModel.includes(':');
425
- }
426
-
427
- private handlePriceEntryInput(key: string): boolean {
428
- if (key === 'escape') {
429
- this.priceEntry = null;
430
- this.markDirty();
431
- return true;
432
- }
433
- if (key === 'enter' || key === 'return') {
434
- const raw = (this.priceEntry ?? '').trim();
435
- if (!this.canEditSessionModelPrice()) {
436
- // The prompt already explains why; Enter just closes it.
437
- this.priceEntry = null;
438
- this.markDirty();
439
- return true;
440
- }
441
- const parts = raw.split(/[\s,]+/).filter((part) => part.length > 0);
442
- const input = Number(parts[0]);
443
- const output = Number(parts[1]);
444
- if (parts.length === 2 && Number.isFinite(input) && input >= 0 && Number.isFinite(output) && output >= 0) {
445
- writeManualModelPrice(this.configAccess!, this.sessionModel, { input, output });
446
- this.priceEntry = null;
447
- }
448
- // Malformed input keeps the editor open so the draft can be corrected.
449
- this.markDirty();
450
- return true;
451
- }
452
- if (isTextBackspace(key)) {
453
- if (this.priceEntry && this.priceEntry.length > 0) {
454
- this.priceEntry = this.priceEntry.slice(0, -1);
455
- this.markDirty();
456
- }
457
- return true;
458
- }
459
- // Two USD amounts separated by a comma (or space), digits, dots, comma, space.
460
- if (key.length === 1 && /[0-9., ]/.test(key)) {
461
- this.priceEntry = (this.priceEntry ?? '') + key;
462
- this.markDirty();
463
- return true;
464
- }
465
- return true; // absorb everything else while entry is active
466
- }
467
-
468
- /**
469
- * Scroll the agent list. Over-consumption fix: only absorbs the key (and
470
- * only advances) when the agent list is actually long enough to scroll,
471
- * mirrors the same `> 5` threshold render() uses to decide whether to show
472
- * scroll hints (below). Previously this always returned true and grew
473
- * scrollOffset unboundedly even with a handful of agents, swallowing
474
- * up/down/page keys that had nothing to do.
475
- */
476
- private scroll(delta: number): boolean {
477
- const agentCount = this.agents.size;
478
- if (agentCount <= 5) return false;
479
- const maxOffset = Math.max(0, agentCount - 1);
480
- const prev = this.scrollOffset;
481
- this.scrollOffset = Math.max(0, Math.min(maxOffset, this.scrollOffset + delta));
482
- if (this.scrollOffset !== prev) this.markDirty();
483
- return true;
484
- }
485
-
486
- // -------------------------------------------------------------------------
487
- // Render
488
- // -------------------------------------------------------------------------
489
-
490
- render(width: number, height: number): Line[] {
491
- if (height <= 0 || width <= 0) return [];
492
- if (this.budgetEntry !== null) return this.renderBudgetEntryPrompt(width, height);
493
- if (this.priceEntry !== null) return this.renderPriceEntryPrompt(width, height);
494
-
495
- const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
496
- const sessionCost = calcSessionCost(this.sessionUsage.input, this.sessionUsage.output, this.sessionUsage.cacheRead, this.sessionUsage.cacheWrite, this.sessionModel);
497
- const budgetThreshold = this.currentBudgetThreshold();
498
- const overBudget = computeBudgetBreach(sessionCost, budgetThreshold);
499
- const sparkline = buildSparkline(this.costHistory);
500
- const sessionPriced = isModelPriced(this.sessionModel);
501
- const costStr = formatCost(sessionCost, !sessionPriced);
502
- const costFg = overBudget ? C.bad : C.cost;
503
- // Dollars carry their source: "your price" for a manual entry,
504
- // "catalog price, as of <date>" for the dated catalog, etc. The
505
- // "price unknown" marker carries the one-key action to fix it instead.
506
- const sourceDescription = sessionPriced ? describePricingSource(this.sessionModel) : null;
507
- const sourceStr = sessionPriced
508
- ? (sourceDescription ? ` (${sourceDescription})` : '')
509
- : ': press p to set a price';
510
- const budgetStr = budgetThreshold > 0
511
- ? ` / ${formatCost(budgetThreshold)}`
512
- : '';
513
- const alertStr = overBudget ? ' ! OVER BUDGET' : '';
514
- const sessionLines: Line[] = [
515
- this.renderKeyValue(width, ' Total', `${costStr}${budgetStr}${alertStr}${sourceStr}`, costFg),
516
- ];
517
- // Budget meter, the single most important glance for this panel: how much
518
- // of the configured budget the session has consumed. Only shown when a
519
- // budget is set (otherwise the bar would be meaningless).
520
- if (budgetThreshold > 0) {
521
- const ratio = sessionCost / budgetThreshold;
522
- const BAR_W = 24;
523
- const filled = Math.max(0, Math.min(BAR_W, Math.round(ratio * BAR_W)));
524
- const meterFg = overBudget ? C.bad : ratio >= 0.8 ? C.warn : C.good;
525
- const pctStr = `${Math.round(ratio * 100)}%`;
526
- sessionLines.push(buildMeterLine(width, filled, BAR_W,
527
- { filled: meterFg, empty: C.separator, label: C.label },
528
- { prefix: ' Budget [', suffix: `] ${pctStr}` },
529
- ));
530
- // Unpriced-spend honesty wherever budget state shows: a meter over a
531
- // partially-unpriced session must say the % undercounts real spend.
532
- const anyUnpricedSpend = (!sessionPriced && (totalInputTokens > 0 || this.sessionUsage.output > 0))
533
- || Array.from(this.agents.values()).some((agent) => agent.inputTokens > 0 && !isModelPriced(agent.model));
534
- if (anyUnpricedSpend) {
535
- sessionLines.push(this.renderKeyValue(width, '', 'some spend has no known price and is not counted above', C.warn));
536
- }
537
- }
538
- if (sparkline.length > 0) sessionLines.push(this.renderLabeledLine(width, ' Trend', sparkline, C.good));
539
- sessionLines.push(this.renderKeyValue(width, ' Input', formatTokens(this.sessionUsage.input), C.label));
540
- sessionLines.push(this.renderKeyValue(width, ' Output', formatTokens(this.sessionUsage.output), C.label));
541
- if (this.sessionUsage.cacheRead > 0 || this.sessionUsage.cacheWrite > 0) {
542
- sessionLines.push(this.renderKeyValue(width, ' Cache W', formatTokens(this.sessionUsage.cacheWrite), C.dim));
543
- sessionLines.push(this.renderKeyValue(width, ' Cache R', formatTokens(this.sessionUsage.cacheRead), C.dim));
544
- }
545
- sessionLines.push(this.renderKeyValue(width, ' Total T', formatTokens(totalInputTokens + this.sessionUsage.output), C.label));
546
- sessionLines.push(this.renderKeyValue(width, ' Model', this.sessionModel, C.model));
547
-
548
- const sections: PanelWorkspaceSection[] = [
549
- {
550
- title: 'Session Cost',
551
- lines: sessionLines,
552
- },
553
- ];
554
-
555
- const agentList = Array.from(this.agents.values());
556
- const scrollableAgents = agentList.length > 5;
557
- // Footer hints adapt to whether the agent list is long enough to scroll.
558
- const hintRow = scrollableAgents
559
- ? buildKeyboardHints(width, [
560
- { keys: 'Up/Down', label: 'scroll agents' },
561
- { keys: 'PgUp/PgDn', label: 'page' },
562
- { keys: 'b', label: 'set budget' },
563
- { keys: 'p', label: 'set model price' },
564
- ], DEFAULT_PANEL_PALETTE)
565
- : buildKeyboardHints(width, [
566
- { keys: 'b', label: 'set budget alert' },
567
- { keys: 'p', label: 'set model price' },
568
- ], DEFAULT_PANEL_PALETTE);
569
- if (agentList.length > 0) {
570
- const planCost = agentList.reduce((sum, a) => sum + a.cost, 0);
571
- const running = agentList.filter((a) => a.status === 'running').length;
572
- const failed = agentList.filter((a) => a.status === 'failed').length;
573
- // Plan total mixes the session model with every agent's model, flag it
574
- // as unpriced (rather than showing a sum that silently omits unpriced
575
- // agents' contribution) if any one of them has no real pricing.
576
- const planHasUnpriced = !isModelPriced(this.sessionModel) || agentList.some((a) => !isModelPriced(a.model));
577
- const agentRows: Line[] = [
578
- buildStyledPanelLine(width, [
579
- { text: ' Plan total ', fg: C.label },
580
- { text: formatCost(planCost + sessionCost, planHasUnpriced), fg: C.cost, bold: true },
581
- { text: ` ${agentList.length} agent${agentList.length === 1 ? '' : 's'}`, fg: C.dim },
582
- ...(running > 0 ? [{ text: ` ${running} running`, fg: C.running }] : []),
583
- ...(failed > 0 ? [{ text: ` ${failed} failed`, fg: C.bad }] : []),
584
- ]),
585
- // Per-agent cost ledger as an aligned table, agent, model, tokens, cost
586
- // line up in columns instead of wrapping across two ragged rows.
587
- ...buildTable(
588
- width,
589
- [
590
- { label: 'Agent', width: 14 },
591
- { label: 'Model', width: 18 },
592
- { label: 'In', width: 7, align: 'right' },
593
- { label: 'Out', width: 7, align: 'right' },
594
- { label: 'Cost', align: 'right' },
595
- ],
596
- agentList.map((agent) => {
597
- const statusFg = agent.status === 'running' ? C.running
598
- : agent.status === 'failed' ? C.bad
599
- : C.good;
600
- const statusIcon = agent.status === 'running' ? '…'
601
- : agent.status === 'failed' ? '✕'
602
- : '✓';
603
- return {
604
- cells: [
605
- { text: `${statusIcon} ${agent.id}`, fg: statusFg },
606
- { text: agent.model, fg: C.model },
607
- { text: agent.inputTokens > 0 ? formatTokens(agent.inputTokens) : '-', fg: C.dim },
608
- { text: agent.outputTokens > 0 ? formatTokens(agent.outputTokens) : '-', fg: C.dim },
609
- {
610
- // '-' means no usage reported yet; 'unpriced' means usage
611
- // exists but the model has no real price; otherwise the cost.
612
- text: agent.inputTokens > 0 && !isModelPriced(agent.model)
613
- ? 'unpriced'
614
- : agent.cost > 0 ? formatCost(agent.cost) : '-',
615
- fg: agent.cost > 0 ? C.cost : C.dim,
616
- },
617
- ],
618
- };
619
- }),
620
- DEFAULT_PANEL_PALETTE,
621
- ),
622
- ];
623
- const sessionSection: PanelWorkspaceSection = sections[0]!;
624
- const agentsSection = resolveScrollablePanelSection(width, height, {
625
- intro: 'Track per-session and per-agent token spend using model pricing and live usage snapshots.',
626
- footerLines: [hintRow],
627
- palette: DEFAULT_PANEL_PALETTE,
628
- beforeSections: [sessionSection],
629
- section: {
630
- title: 'Agents',
631
- scrollableLines: agentRows,
632
- scrollOffset: this.scrollOffset,
633
- minRows: 4,
634
- appendWindowSummary: scrollableAgents ? { dimColor: DEFAULT_PANEL_PALETTE.dim } : undefined,
635
- },
636
- });
637
- this.scrollOffset = agentsSection.scrollOffset;
638
- sections.push(agentsSection.section);
639
- } else {
640
- sections.push({
641
- title: 'Agents',
642
- lines: buildEmptyState(
643
- width,
644
- ' No agents spawned this session',
645
- 'Agent-level cost estimates appear here once delegated or background agents start running.',
646
- // 'b' already opens the in-panel budget-entry field (see
647
- // handleInput/handleBudgetEntryInput above) and is advertised in
648
- // the footer's 'b: set budget' hint, so the printed
649
- // '/cost budget <usd>' command was a redundant action substitute.
650
- [
651
- { command: 'b', summary: 'set a session budget alert to track spend against a cap' },
652
- ],
653
- DEFAULT_PANEL_PALETTE,
654
- ),
655
- });
656
- }
657
-
658
- return buildPanelWorkspace(width, height, {
659
- title: ' Cost Tracker',
660
- intro: 'Track per-session and per-agent token spend using model pricing and live usage snapshots.',
661
- sections,
662
- footerLines: [hintRow],
663
- palette: DEFAULT_PANEL_PALETTE,
664
- });
665
- }
666
-
667
- // -------------------------------------------------------------------------
668
- // Render helpers
669
- // -------------------------------------------------------------------------
670
-
671
- private renderKeyValue(width: number, label: string, value: string, valueFg: string): Line {
672
- const LABEL_W = 10;
673
- return buildStyledPanelLine(width, [
674
- { text: fitDisplay(label, LABEL_W), fg: C.label, bg: C.bg },
675
- { text: ': ', fg: C.dim, bg: C.bg },
676
- { text: value, fg: valueFg, bg: C.bg, bold: true },
677
- ]);
678
- }
679
-
680
- private renderLabeledLine(width: number, label: string, value: string, valueFg: string): Line {
681
- return buildStyledPanelLine(width, [
682
- ...(label.length > 0 ? [{ text: `${fitDisplay(label, 10)} `, fg: C.label }] : []),
683
- { text: value, fg: valueFg },
684
- ]);
685
- }
686
-
687
- // Non-masked numeric entry for the 'b' budget-alert key, the value is a
688
- // USD amount, not a secret, so (unlike LocalAuthPanel's masked entry) it is
689
- // echoed directly rather than dotted out.
690
- private renderBudgetEntryPrompt(width: number, height: number): Line[] {
691
- const draft = this.budgetEntry ?? '';
692
- const display = `$${draft}█`;
693
- const promptLines: Line[] = [
694
- buildPanelLine(width, [[' Set the session budget alert (USD). 0 disables it.', C.label]]),
695
- buildPanelLine(width, [['', C.label]]),
696
- buildPanelLine(width, [[' Budget ', C.label], [display, C.cost]]),
697
- buildPanelLine(width, [['', C.label]]),
698
- buildPanelLine(width, [[' [Enter] Confirm [Esc] Cancel [Backspace] Delete char', C.dim]]),
699
- ];
700
- const workspace = buildPanelWorkspace(width, height, {
701
- title: ' Cost Tracker: Budget',
702
- intro: 'Type a USD amount and press Enter to set the budget alert threshold.',
703
- sections: [{ lines: promptLines }],
704
- palette: DEFAULT_PANEL_PALETTE,
705
- });
706
- while (workspace.length < height) workspace.push(createEmptyLine(width));
707
- return workspace.slice(0, height);
708
- }
709
-
710
- // Non-masked entry for the 'p' manual-price key. Two USD-per-1M-token
711
- // amounts (input,output); persists to pricing.modelPrices for the session's
712
- // provider:model live. A model without a provider prefix cannot carry a
713
- // manual price (the settings key is keyed provider:model), the prompt says
714
- // so instead of accepting a value it could not store.
715
- private renderPriceEntryPrompt(width: number, height: number): Line[] {
716
- const editable = this.canEditSessionModelPrice();
717
- const draft = this.priceEntry ?? '';
718
- const display = `${draft}█`;
719
- const promptLines: Line[] = editable
720
- ? [
721
- buildPanelLine(width, [[` Set your manual price for ${this.sessionModel} (USD per 1M tokens).`, C.label]]),
722
- buildPanelLine(width, [[' Format: input,output, for example 3.00,15.00', C.dim]]),
723
- buildPanelLine(width, [['', C.label]]),
724
- buildPanelLine(width, [[' Price ', C.label], [display, C.cost]]),
725
- buildPanelLine(width, [['', C.label]]),
726
- buildPanelLine(width, [[' [Enter] Save [Esc] Cancel [Backspace] Delete char', C.dim]]),
727
- ]
728
- : [
729
- buildPanelLine(width, [[` The current model (${this.sessionModel}) has no provider prefix, so a`, C.label]]),
730
- buildPanelLine(width, [[' manual price cannot be stored for it (prices are keyed provider:model).', C.label]]),
731
- buildPanelLine(width, [['', C.label]]),
732
- buildPanelLine(width, [[' [Esc] Close', C.dim]]),
733
- ];
734
- const workspace = buildPanelWorkspace(width, height, {
735
- title: ' Cost Tracker: Model Price',
736
- intro: editable
737
- ? 'Your price wins over provider and catalog prices, immediately and everywhere.'
738
- : 'Manual prices are stored per provider:model.',
739
- sections: [{ lines: promptLines }],
740
- palette: DEFAULT_PANEL_PALETTE,
741
- });
742
- while (workspace.length < height) workspace.push(createEmptyLine(width));
743
- return workspace.slice(0, height);
744
- }
745
-
746
- }