@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.
- package/CHANGELOG.md +58 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +16 -6
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/verification-ledger.ts +18 -16
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /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
|
-
}
|