@pellux/goodvibes-tui 2.0.21 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (391) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -29
  3. package/docs/README.md +8 -5
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +28 -19
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-reference.ts +2 -2
  52. package/src/input/command-registry.ts +77 -35
  53. package/src/input/commands/acp-runtime.ts +19 -9
  54. package/src/input/commands/channel-runtime.ts +2 -2
  55. package/src/input/commands/ci-runtime.ts +1 -1
  56. package/src/input/commands/codebase-runtime.ts +1 -1
  57. package/src/input/commands/config.ts +7 -1
  58. package/src/input/commands/context-window.ts +25 -19
  59. package/src/input/commands/control-room-runtime.ts +18 -18
  60. package/src/input/commands/conversation-runtime.ts +0 -1
  61. package/src/input/commands/cost-runtime.ts +29 -20
  62. package/src/input/commands/diff-runtime.ts +35 -181
  63. package/src/input/commands/eval.ts +1 -1
  64. package/src/input/commands/experience-runtime.ts +3 -7
  65. package/src/input/commands/fresh-conversation.ts +86 -0
  66. package/src/input/commands/git-runtime.ts +5 -22
  67. package/src/input/commands/graph-runtime.ts +1 -1
  68. package/src/input/commands/guidance-runtime.ts +1 -1
  69. package/src/input/commands/health-runtime.ts +8 -9
  70. package/src/input/commands/hooks-runtime.ts +2 -2
  71. package/src/input/commands/hosted-runtime.ts +3 -3
  72. package/src/input/commands/incident-runtime.ts +3 -3
  73. package/src/input/commands/intelligence-runtime.ts +6 -9
  74. package/src/input/commands/knowledge.ts +2 -2
  75. package/src/input/commands/legacy-panel-command.ts +57 -0
  76. package/src/input/commands/local-auth-runtime.ts +8 -9
  77. package/src/input/commands/local-provider-runtime.ts +7 -4
  78. package/src/input/commands/local-runtime.ts +12 -15
  79. package/src/input/commands/local-setup-review.ts +1 -1
  80. package/src/input/commands/local-setup.ts +13 -19
  81. package/src/input/commands/memory.ts +1 -1
  82. package/src/input/commands/notify-runtime.ts +9 -0
  83. package/src/input/commands/operator-runtime.ts +10 -12
  84. package/src/input/commands/permissions-provenance.ts +3 -3
  85. package/src/input/commands/planning-runtime.ts +7 -7
  86. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  87. package/src/input/commands/platform-services-runtime.ts +1 -1
  88. package/src/input/commands/policy-dispatch.ts +2 -2
  89. package/src/input/commands/policy.ts +3 -3
  90. package/src/input/commands/provider.ts +1 -1
  91. package/src/input/commands/remote-runtime.ts +1 -1
  92. package/src/input/commands/review-runtime.ts +39 -94
  93. package/src/input/commands/rewind-runtime.ts +81 -108
  94. package/src/input/commands/runtime-services.ts +15 -17
  95. package/src/input/commands/services-runtime.ts +2 -2
  96. package/src/input/commands/session-content.ts +21 -5
  97. package/src/input/commands/session-workflow.ts +13 -38
  98. package/src/input/commands/settings-sync-runtime.ts +2 -2
  99. package/src/input/commands/shell-core.ts +19 -4
  100. package/src/input/commands/skills-runtime.ts +2 -2
  101. package/src/input/commands/status-runtime.ts +13 -0
  102. package/src/input/commands/subscription-runtime.ts +1 -1
  103. package/src/input/commands/tasks-runtime.ts +2 -8
  104. package/src/input/commands/work-plan-runtime.ts +4 -4
  105. package/src/input/commands/workstream-attempts.ts +24 -47
  106. package/src/input/commands/workstream-runtime.ts +2 -2
  107. package/src/input/commands/worktree-runtime.ts +3 -3
  108. package/src/input/commands.ts +3 -1
  109. package/src/input/composer-editor.ts +44 -0
  110. package/src/input/concealed-input.ts +1 -1
  111. package/src/input/config-modal-types.ts +20 -8
  112. package/src/input/config-modal.ts +106 -105
  113. package/src/input/confirm-dialog-types.ts +28 -0
  114. package/src/input/confirm-dialog.ts +70 -0
  115. package/src/input/delete-key-policy.ts +3 -3
  116. package/src/input/feed-context-factory.ts +15 -16
  117. package/src/input/handler-command-route.ts +7 -42
  118. package/src/input/handler-content-actions.ts +16 -15
  119. package/src/input/handler-feed-routes.ts +26 -211
  120. package/src/input/handler-feed.ts +68 -95
  121. package/src/input/handler-interactions.ts +5 -3
  122. package/src/input/handler-modal-routes.ts +215 -216
  123. package/src/input/handler-modal-stack.ts +81 -11
  124. package/src/input/handler-modal-token-routes.ts +4 -0
  125. package/src/input/handler-onboarding.ts +0 -1
  126. package/src/input/handler-picker-routes.ts +65 -107
  127. package/src/input/handler-session-view-route.ts +149 -0
  128. package/src/input/handler-shortcuts.ts +16 -154
  129. package/src/input/handler-types.ts +4 -2
  130. package/src/input/handler-ui-state.ts +45 -15
  131. package/src/input/handler-work-tree-route.ts +123 -0
  132. package/src/input/handler.ts +25 -70
  133. package/src/input/keybindings.ts +16 -71
  134. package/src/input/masked-entry-modal.ts +117 -0
  135. package/src/input/mcp-workspace.ts +48 -17
  136. package/src/input/model-picker-filter.ts +8 -0
  137. package/src/input/model-picker-open.ts +99 -0
  138. package/src/input/model-picker-types.ts +1 -1
  139. package/src/input/model-picker.ts +92 -9
  140. package/src/input/notifications-modal.ts +123 -0
  141. package/src/input/overlay-filter.ts +26 -0
  142. package/src/input/profile-picker-modal.ts +37 -8
  143. package/src/input/search.ts +3 -3
  144. package/src/input/selection-modal-queue.ts +2 -0
  145. package/src/input/selection-modal.ts +28 -16
  146. package/src/input/session-picker-modal.ts +33 -8
  147. package/src/input/settings-modal-activation.ts +7 -2
  148. package/src/input/settings-modal-data.ts +11 -6
  149. package/src/input/settings-modal-types.ts +1 -1
  150. package/src/input/settings-modal.ts +20 -20
  151. package/src/input/submission-intent.ts +1 -1
  152. package/src/input/submission-router.ts +4 -9
  153. package/src/input/surface-modal-host.ts +110 -0
  154. package/src/input/theme-settings-actions.ts +82 -0
  155. package/src/input/usage-modal.ts +183 -0
  156. package/src/input/views.ts +97 -0
  157. package/src/main.ts +175 -165
  158. package/src/permissions/hunk-selection.ts +1 -1
  159. package/src/permissions/prompt-card.ts +511 -0
  160. package/src/permissions/prompt.ts +59 -622
  161. package/src/renderer/agents-modal.ts +310 -0
  162. package/src/renderer/autocomplete-overlay.ts +42 -220
  163. package/src/renderer/back-to-bottom.ts +80 -0
  164. package/src/renderer/block-actions-overlay.ts +30 -48
  165. package/src/renderer/bookmark-modal.ts +48 -79
  166. package/src/renderer/changes-modal.ts +325 -0
  167. package/src/renderer/code-block.ts +184 -106
  168. package/src/renderer/command-palette.ts +93 -0
  169. package/src/renderer/composer-fragments.ts +4 -4
  170. package/src/renderer/composer.ts +146 -0
  171. package/src/renderer/compositor.ts +26 -160
  172. package/src/renderer/config-modal.ts +185 -81
  173. package/src/renderer/confirm-dialog.ts +50 -0
  174. package/src/renderer/context-inspector.ts +116 -154
  175. package/src/renderer/context-status-hint.ts +4 -4
  176. package/src/renderer/conversation-layout.ts +20 -0
  177. package/src/renderer/conversation-overlays.ts +94 -84
  178. package/src/renderer/conversation-surface.ts +19 -38
  179. package/src/renderer/diff-tint.ts +98 -0
  180. package/src/renderer/file-picker-overlay.ts +63 -90
  181. package/src/renderer/footer-targets.ts +38 -0
  182. package/src/renderer/git-status.ts +3 -4
  183. package/src/renderer/header-line.ts +140 -117
  184. package/src/renderer/help-overlay.ts +133 -219
  185. package/src/renderer/history-search-overlay.ts +15 -12
  186. package/src/renderer/lane-graph/bead.ts +800 -0
  187. package/src/renderer/lane-graph/glyphs.ts +134 -0
  188. package/src/renderer/lane-graph/layout.ts +248 -0
  189. package/src/renderer/lane-graph/paint.ts +537 -0
  190. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  191. package/src/renderer/layout-engine.ts +2 -57
  192. package/src/renderer/layout.ts +7 -13
  193. package/src/renderer/markdown-inline.ts +2 -1
  194. package/src/renderer/markdown-table.ts +17 -12
  195. package/src/renderer/markdown.ts +92 -34
  196. package/src/renderer/masked-entry-modal.ts +78 -0
  197. package/src/renderer/mcp-workspace.ts +166 -203
  198. package/src/renderer/model-workspace.ts +381 -436
  199. package/src/renderer/notifications-modal.ts +116 -0
  200. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  201. package/src/renderer/process-view.ts +192 -0
  202. package/src/renderer/profile-picker-modal.ts +58 -110
  203. package/src/renderer/search-overlay.ts +6 -3
  204. package/src/renderer/selection-modal-overlay.ts +87 -327
  205. package/src/renderer/semantic-diff.ts +2 -2
  206. package/src/renderer/session-chips.ts +102 -0
  207. package/src/renderer/session-picker-modal.ts +177 -297
  208. package/src/renderer/settings-modal-connections.ts +14 -23
  209. package/src/renderer/settings-modal-context.ts +367 -0
  210. package/src/renderer/settings-modal-helpers.ts +6 -11
  211. package/src/renderer/settings-modal.ts +322 -726
  212. package/src/renderer/shell-surface.ts +277 -175
  213. package/src/renderer/shortcuts-overlay.ts +217 -0
  214. package/src/renderer/status-line.ts +475 -0
  215. package/src/renderer/surface-compose.ts +174 -0
  216. package/src/renderer/surface-kit-extra.ts +303 -0
  217. package/src/renderer/surface-kit-list.ts +366 -0
  218. package/src/renderer/surface-kit-parts.ts +360 -0
  219. package/src/renderer/surface-kit.ts +568 -0
  220. package/src/renderer/syntax-highlighter.ts +280 -197
  221. package/src/renderer/syntax-theme.ts +51 -0
  222. package/src/renderer/system-message.ts +0 -1
  223. package/src/renderer/term-caps.ts +42 -1
  224. package/src/renderer/terminal-bg-probe.ts +116 -19
  225. package/src/renderer/terminal-palette-probe.ts +322 -0
  226. package/src/renderer/terminal-palette-reader.ts +183 -0
  227. package/src/renderer/terminal-palette.ts +58 -0
  228. package/src/renderer/theme-mode-config.ts +16 -1
  229. package/src/renderer/theme.ts +234 -207
  230. package/src/renderer/thinking.ts +0 -1
  231. package/src/renderer/throbber.ts +188 -0
  232. package/src/renderer/toast-center.ts +119 -0
  233. package/src/renderer/ui-factory.ts +39 -602
  234. package/src/renderer/ui-primitives.ts +8 -25
  235. package/src/renderer/usage-modal.ts +353 -0
  236. package/src/renderer/user-message.ts +58 -0
  237. package/src/renderer/voice-capture-chip.ts +53 -0
  238. package/src/runtime/bootstrap-command-context.ts +0 -5
  239. package/src/runtime/bootstrap-command-parts.ts +38 -89
  240. package/src/runtime/bootstrap-core.ts +13 -15
  241. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  242. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  243. package/src/runtime/bootstrap-shell.ts +16 -50
  244. package/src/runtime/bootstrap.ts +14 -14
  245. package/src/runtime/client/fleet-union.ts +5 -5
  246. package/src/runtime/client/hosted-exit.ts +2 -2
  247. package/src/runtime/client/hosted-sessions.ts +1 -1
  248. package/src/runtime/client/spine-adoption.ts +1 -1
  249. package/src/runtime/context-accounting-source.ts +5 -4
  250. package/src/runtime/index.ts +1 -0
  251. package/src/runtime/notification-dispatch.ts +102 -29
  252. package/src/runtime/recovery-offer-wiring.ts +4 -1
  253. package/src/runtime/recovery-prompt.ts +31 -15
  254. package/src/runtime/runtime-services-types.ts +10 -6
  255. package/src/runtime/services.ts +17 -12
  256. package/src/runtime/session-continuity-hints.ts +2 -5
  257. package/src/runtime/session-inbound-inputs.ts +1 -1
  258. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  259. package/src/runtime/startup-typeahead-gate.ts +103 -0
  260. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  261. package/src/runtime/store/domains/index.ts +0 -8
  262. package/src/runtime/store/selectors/index.ts +0 -28
  263. package/src/runtime/store/state.ts +0 -8
  264. package/src/runtime/ui-services.ts +4 -6
  265. package/src/runtime/usage-tracker.ts +348 -0
  266. package/src/runtime/wrfc-persistence.ts +4 -4
  267. package/src/shell/blocking-input.ts +92 -2
  268. package/src/shell/recovery-input-helpers.ts +1 -1
  269. package/src/shell/session-views.ts +539 -0
  270. package/src/shell/status-report.ts +86 -0
  271. package/src/shell/transcript-scroll.ts +68 -0
  272. package/src/shell/ui-openers.ts +84 -172
  273. package/src/shell/view-openers.ts +205 -0
  274. package/src/tools/wrfc-agent-guard.ts +22 -2
  275. package/src/utils/format-duration.ts +4 -8
  276. package/src/utils/splash-lines.ts +9 -1
  277. package/src/utils/terminal-width.ts +56 -0
  278. package/src/verification/live-verifier.ts +0 -10
  279. package/src/version.ts +1 -1
  280. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  281. package/src/{panels → views}/builtin-modals.ts +22 -21
  282. package/src/views/builtin-views.ts +135 -0
  283. package/src/{panels → views}/confirm-state.ts +9 -9
  284. package/src/{panels → views}/diff-review-model.ts +10 -3
  285. package/src/{panels → views}/eval-registry.ts +1 -1
  286. package/src/{panels → views}/fleet-acts.ts +49 -85
  287. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  288. package/src/{panels → views}/fleet-gateway.ts +5 -5
  289. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  290. package/src/{panels → views}/fleet-read-model.ts +9 -9
  291. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  292. package/src/{panels → views}/fleet-spawn.ts +33 -49
  293. package/src/{panels → views}/fleet-steer.ts +12 -12
  294. package/src/{panels → views}/fleet-stop.ts +4 -4
  295. package/src/{panels → views}/fleet-transcript.ts +26 -27
  296. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  297. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  298. package/src/views/modal-surface-registry.ts +41 -0
  299. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  300. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  301. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  302. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  303. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  304. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  305. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  306. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  307. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  308. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  309. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  310. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  311. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  312. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  313. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  314. package/src/{panels → views}/modals/security-modal.ts +22 -16
  315. package/src/{panels → views}/modals/services-modal.ts +3 -3
  316. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  317. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  318. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  319. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  320. package/src/views/notifications-feed.ts +314 -0
  321. package/src/{panels → views}/polish-core.ts +38 -36
  322. package/src/{panels → views}/polish-tables.ts +35 -26
  323. package/src/{panels → views}/polish.ts +101 -95
  324. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  325. package/src/{panels → views}/provider-health-routes.ts +14 -14
  326. package/src/{panels → views}/search-focus.ts +8 -8
  327. package/src/{panels → views}/session-maintenance.ts +11 -11
  328. package/src/views/skills-discovery.ts +159 -0
  329. package/src/views/view-deps.ts +80 -0
  330. package/src/views/view-panel-adapter.ts +53 -0
  331. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  332. package/src/core/conversation-fold.ts +0 -101
  333. package/src/core/conversation-turn-structure.ts +0 -464
  334. package/src/core/conversation-user-receipts.ts +0 -42
  335. package/src/input/commands/operator-panel-runtime.ts +0 -186
  336. package/src/input/panel-integration-actions.ts +0 -33
  337. package/src/input/panel-mouse-geometry.ts +0 -97
  338. package/src/input/panel-paste-flood-guard.ts +0 -86
  339. package/src/panels/base-panel.ts +0 -301
  340. package/src/panels/builtin/agent.ts +0 -27
  341. package/src/panels/builtin/development.ts +0 -92
  342. package/src/panels/builtin/knowledge.ts +0 -12
  343. package/src/panels/builtin/operations.ts +0 -177
  344. package/src/panels/builtin/session.ts +0 -81
  345. package/src/panels/builtin/shared.ts +0 -357
  346. package/src/panels/builtin-panels.ts +0 -28
  347. package/src/panels/cost-tracker-panel.ts +0 -746
  348. package/src/panels/diff-panel.ts +0 -797
  349. package/src/panels/diff-review-panel.ts +0 -395
  350. package/src/panels/expandable-list-panel.ts +0 -189
  351. package/src/panels/fleet-deep-link.ts +0 -31
  352. package/src/panels/fleet-panel.ts +0 -799
  353. package/src/panels/git-panel.ts +0 -800
  354. package/src/panels/hosted-session-panel.ts +0 -221
  355. package/src/panels/index.ts +0 -21
  356. package/src/panels/local-auth-panel.ts +0 -478
  357. package/src/panels/notifications-feed.ts +0 -127
  358. package/src/panels/notifications-panel.ts +0 -160
  359. package/src/panels/panel-confirm-overlay.ts +0 -72
  360. package/src/panels/panel-manager.ts +0 -791
  361. package/src/panels/plugins-panel.ts +0 -371
  362. package/src/panels/scrollable-list-panel.ts +0 -498
  363. package/src/panels/skills-panel.ts +0 -454
  364. package/src/panels/token-budget-panel.ts +0 -688
  365. package/src/panels/types.ts +0 -191
  366. package/src/panels/wrfc-panel-format.ts +0 -133
  367. package/src/renderer/diff-view.ts +0 -134
  368. package/src/renderer/fleet-tab-strip.ts +0 -56
  369. package/src/renderer/footer-tips.ts +0 -53
  370. package/src/renderer/fullscreen-primitives.ts +0 -146
  371. package/src/renderer/fullscreen-workspace.ts +0 -194
  372. package/src/renderer/hint-grammar.ts +0 -52
  373. package/src/renderer/modal-factory.ts +0 -480
  374. package/src/renderer/overlay-box.ts +0 -154
  375. package/src/renderer/overlay-viewport.ts +0 -104
  376. package/src/renderer/panel-composite.ts +0 -145
  377. package/src/renderer/panel-workspace-bar.ts +0 -54
  378. package/src/renderer/process-indicator.ts +0 -163
  379. package/src/renderer/tab-strip.ts +0 -222
  380. package/src/renderer/text-layout.ts +0 -31
  381. package/src/renderer/tool-call.ts +0 -276
  382. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  383. package/src/renderer/tool-result-summary.ts +0 -219
  384. package/src/runtime/diagnostics/panels/index.ts +0 -21
  385. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  386. package/src/runtime/perf/panel-contracts.ts +0 -32
  387. package/src/runtime/store/domains/panels.ts +0 -117
  388. package/src/verification/verification-ledger.ts +0 -707
  389. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  390. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  391. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -1,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
- }