@pellux/goodvibes-agent 1.4.4 → 1.5.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 (230) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +52 -7
  3. package/dist/package/main.js +7491 -12253
  4. package/docs/README.md +2 -2
  5. package/docs/channels-remote-and-api.md +1 -1
  6. package/docs/getting-started.md +2 -2
  7. package/docs/release-and-publishing.md +1 -1
  8. package/docs/tools-and-commands.md +5 -5
  9. package/package.json +4 -2
  10. package/release/live-verification/live-verification.json +13 -13
  11. package/release/live-verification/live-verification.md +15 -15
  12. package/release/performance-snapshot.json +2 -2
  13. package/release/release-notes.md +5 -7
  14. package/release/release-readiness.json +6 -6
  15. package/src/agent/behavior-discovery-summary.ts +4 -18
  16. package/src/agent/calendar-registry.ts +322 -0
  17. package/src/agent/competitive-feature-inventory.ts +268 -130
  18. package/src/agent/document-registry.ts +5 -1
  19. package/src/agent/email/email-service.ts +350 -0
  20. package/src/agent/email/imap-client.ts +596 -0
  21. package/src/agent/email/smtp-client.ts +453 -0
  22. package/src/agent/ics-calendar.ts +662 -0
  23. package/src/agent/ics-timezone.ts +172 -0
  24. package/src/agent/markdown-frontmatter.ts +31 -0
  25. package/src/agent/memory-safety.ts +1 -1
  26. package/src/agent/note-registry.ts +3 -9
  27. package/src/agent/persona-discovery.ts +3 -15
  28. package/src/agent/persona-registry.ts +1 -10
  29. package/src/agent/research-source-registry.ts +5 -1
  30. package/src/agent/routine-discovery.ts +3 -15
  31. package/src/agent/runtime-profile.ts +3 -0
  32. package/src/agent/skill-discovery.ts +3 -15
  33. package/src/agent/skill-draft-proposer.ts +203 -0
  34. package/src/agent/skill-draft-runner.ts +201 -0
  35. package/src/agent/skill-registry.ts +36 -1
  36. package/src/agent/skill-standard.ts +99 -0
  37. package/src/agent/vibe-file.ts +7 -22
  38. package/src/cli/completion.ts +1 -1
  39. package/src/cli/config-overrides.ts +10 -1
  40. package/src/cli/redaction.ts +17 -5
  41. package/src/config/provider-model.ts +2 -1
  42. package/src/config/secret-config.ts +13 -6
  43. package/src/core/activity-feed.ts +97 -0
  44. package/src/core/away-digest.ts +161 -0
  45. package/src/core/conversation-rendering.ts +7 -3
  46. package/src/core/conversation.ts +22 -15
  47. package/src/core/hardware-profile.ts +362 -0
  48. package/src/core/last-seen-store.ts +78 -0
  49. package/src/core/plain-language.ts +52 -0
  50. package/src/core/setup-incomplete-hint.ts +90 -0
  51. package/src/core/system-message-router.ts +38 -87
  52. package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
  53. package/src/input/agent-workspace-basic-command-editors.ts +39 -141
  54. package/src/input/agent-workspace-categories.ts +40 -125
  55. package/src/input/agent-workspace-command-editor.ts +4 -0
  56. package/src/input/agent-workspace-host-category.ts +0 -5
  57. package/src/input/agent-workspace-local-editor-submission.ts +3 -1
  58. package/src/input/agent-workspace-navigation.ts +10 -3
  59. package/src/input/agent-workspace-onboarding-actions.ts +132 -0
  60. package/src/input/agent-workspace-onboarding-categories.ts +29 -26
  61. package/src/input/agent-workspace-onboarding-finish.ts +11 -4
  62. package/src/input/agent-workspace-onboarding-state.ts +111 -0
  63. package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
  64. package/src/input/agent-workspace-profile-editors.ts +155 -0
  65. package/src/input/agent-workspace-subscription-editor.ts +7 -0
  66. package/src/input/agent-workspace-types.ts +5 -1
  67. package/src/input/agent-workspace.ts +65 -39
  68. package/src/input/command-registry.ts +18 -5
  69. package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
  70. package/src/input/commands/agent-skills-runtime.ts +60 -3
  71. package/src/input/commands/calendar-runtime.ts +209 -0
  72. package/src/input/commands/channels-runtime.ts +1 -0
  73. package/src/input/commands/command-error.ts +9 -0
  74. package/src/input/commands/compat-runtime.ts +1 -0
  75. package/src/input/commands/config.ts +1 -0
  76. package/src/input/commands/conversation-runtime.ts +1 -1
  77. package/src/input/commands/delegation-runtime.ts +5 -6
  78. package/src/input/commands/email-runtime.ts +387 -0
  79. package/src/input/commands/experience-runtime.ts +17 -4
  80. package/src/input/commands/guidance-runtime.ts +1 -1
  81. package/src/input/commands/health-runtime.ts +6 -1
  82. package/src/input/commands/knowledge-format.ts +73 -0
  83. package/src/input/commands/knowledge.ts +9 -74
  84. package/src/input/commands/local-provider-runtime.ts +2 -1
  85. package/src/input/commands/local-runtime.ts +10 -4
  86. package/src/input/commands/mcp-runtime.ts +7 -0
  87. package/src/input/commands/notify-runtime.ts +17 -1
  88. package/src/input/commands/onboarding-runtime.ts +1 -0
  89. package/src/input/commands/operator-actions-runtime.ts +1 -0
  90. package/src/input/commands/operator-runtime.ts +3 -0
  91. package/src/input/commands/personas-runtime.ts +1 -0
  92. package/src/input/commands/platform-access-runtime.ts +40 -17
  93. package/src/input/commands/product-runtime.ts +6 -1
  94. package/src/input/commands/provider-accounts-runtime.ts +3 -2
  95. package/src/input/commands/qrcode-runtime.ts +1 -0
  96. package/src/input/commands/routines-runtime.ts +1 -0
  97. package/src/input/commands/runtime-services.ts +0 -13
  98. package/src/input/commands/security-runtime.ts +1 -0
  99. package/src/input/commands/session-content.ts +1 -0
  100. package/src/input/commands/shell-core.ts +5 -2
  101. package/src/input/commands/subscription-runtime.ts +33 -9
  102. package/src/input/commands/support-bundle-runtime.ts +8 -1
  103. package/src/input/commands/tasks-runtime.ts +1 -0
  104. package/src/input/commands/tts-runtime.ts +1 -0
  105. package/src/input/commands/vibe-runtime.ts +1 -0
  106. package/src/input/commands.ts +4 -0
  107. package/src/input/feed-context-factory.ts +3 -12
  108. package/src/input/handler-command-route.ts +7 -60
  109. package/src/input/handler-feed-routes.ts +0 -194
  110. package/src/input/handler-feed.ts +2 -54
  111. package/src/input/handler-interactions.ts +0 -2
  112. package/src/input/handler-modal-stack.ts +0 -9
  113. package/src/input/handler-picker-routes.ts +24 -1
  114. package/src/input/handler-shortcuts.ts +11 -40
  115. package/src/input/handler-ui-state.ts +13 -0
  116. package/src/input/handler.ts +8 -35
  117. package/src/input/keybindings.ts +40 -17
  118. package/src/input/model-picker-local-fit.ts +130 -0
  119. package/src/input/shell-passthrough.ts +58 -0
  120. package/src/input/submission-router.ts +0 -5
  121. package/src/main.ts +129 -90
  122. package/src/renderer/activity-sidebar.ts +186 -0
  123. package/src/renderer/agent-workspace-context-lines.ts +5 -48
  124. package/src/renderer/agent-workspace-style.ts +1 -1
  125. package/src/renderer/agent-workspace.ts +14 -2
  126. package/src/renderer/compositor.ts +37 -125
  127. package/src/renderer/conversation-overlays.ts +3 -3
  128. package/src/renderer/help-overlay.ts +7 -5
  129. package/src/renderer/model-workspace.ts +101 -86
  130. package/src/{panels → renderer}/polish.ts +3 -3
  131. package/src/renderer/shell-surface.ts +4 -5
  132. package/src/renderer/status-token.ts +31 -11
  133. package/src/renderer/tab-strip.ts +1 -1
  134. package/src/renderer/tool-call.ts +7 -8
  135. package/src/renderer/tool-labels.ts +92 -0
  136. package/src/renderer/ui-factory.ts +48 -96
  137. package/src/runtime/bootstrap-command-context.ts +0 -5
  138. package/src/runtime/bootstrap-command-parts.ts +6 -15
  139. package/src/runtime/bootstrap-core.ts +34 -13
  140. package/src/runtime/bootstrap-hook-bridge.ts +3 -1
  141. package/src/runtime/bootstrap-shell.ts +3 -50
  142. package/src/runtime/bootstrap.ts +3 -4
  143. package/src/runtime/context.ts +1 -1
  144. package/src/runtime/diagnostics/panels/index.ts +0 -1
  145. package/src/runtime/diagnostics/panels/policy.ts +1 -1
  146. package/src/runtime/execution-ledger.ts +16 -1
  147. package/src/runtime/index.ts +6 -1
  148. package/src/runtime/onboarding/index.ts +1 -0
  149. package/src/runtime/onboarding/onboarding-state.ts +200 -0
  150. package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
  151. package/src/runtime/services.ts +16 -4
  152. package/src/runtime/terminal-output-guard.ts +7 -0
  153. package/src/runtime/tool-permission-safety.ts +1 -1
  154. package/src/runtime/ui/model-picker/data-provider.ts +1 -1
  155. package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
  156. package/src/runtime/ui/model-picker/types.ts +2 -2
  157. package/src/runtime/ui/provider-health/data-provider.ts +3 -3
  158. package/src/runtime/ui/provider-health/types.ts +2 -2
  159. package/src/runtime/ui-services.ts +0 -2
  160. package/src/shell/agent-workspace-fullscreen.ts +0 -1
  161. package/src/shell/autonomy-surfacing.ts +272 -0
  162. package/src/shell/session-continuity-hints.ts +0 -6
  163. package/src/shell/startup-wiring.ts +221 -0
  164. package/src/shell/ui-openers.ts +18 -29
  165. package/src/tools/agent-context-policy.ts +1 -1
  166. package/src/tools/agent-harness-background-processes.ts +4 -4
  167. package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
  168. package/src/tools/agent-harness-command-runner.ts +6 -1
  169. package/src/tools/agent-harness-document-ops.ts +26 -53
  170. package/src/tools/agent-harness-execution-history.ts +1 -13
  171. package/src/tools/agent-harness-execution-posture.ts +0 -15
  172. package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
  173. package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
  174. package/src/tools/agent-harness-metadata.ts +16 -0
  175. package/src/tools/agent-harness-mode-catalog.ts +2 -9
  176. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  177. package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
  178. package/src/tools/agent-harness-tool-schema.ts +13 -14
  179. package/src/tools/agent-harness-tool.ts +27 -23
  180. package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
  181. package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
  182. package/src/tools/agent-workspace-tool.ts +2 -33
  183. package/src/utils/terminal-width.ts +9 -3
  184. package/src/version.ts +1 -1
  185. package/src/input/agent-workspace-panel-route.ts +0 -44
  186. package/src/input/panel-integration-actions.ts +0 -26
  187. package/src/panels/approval-panel.ts +0 -149
  188. package/src/panels/automation-control-panel.ts +0 -212
  189. package/src/panels/base-panel.ts +0 -254
  190. package/src/panels/builtin/agent.ts +0 -58
  191. package/src/panels/builtin/knowledge.ts +0 -26
  192. package/src/panels/builtin/operations.ts +0 -121
  193. package/src/panels/builtin/session.ts +0 -138
  194. package/src/panels/builtin/shared.ts +0 -275
  195. package/src/panels/builtin/usage.ts +0 -21
  196. package/src/panels/builtin-panels.ts +0 -23
  197. package/src/panels/confirm-state.ts +0 -61
  198. package/src/panels/context-visualizer-panel.ts +0 -204
  199. package/src/panels/cost-tracker-panel.ts +0 -444
  200. package/src/panels/docs-panel.ts +0 -285
  201. package/src/panels/index.ts +0 -25
  202. package/src/panels/knowledge-panel.ts +0 -417
  203. package/src/panels/memory-panel.ts +0 -226
  204. package/src/panels/panel-list-panel.ts +0 -464
  205. package/src/panels/panel-manager.ts +0 -570
  206. package/src/panels/provider-accounts-panel.ts +0 -233
  207. package/src/panels/provider-health-domains.ts +0 -208
  208. package/src/panels/provider-health-panel.ts +0 -720
  209. package/src/panels/provider-health-tracker.ts +0 -115
  210. package/src/panels/provider-stats-panel.ts +0 -366
  211. package/src/panels/qr-panel.ts +0 -207
  212. package/src/panels/schedule-panel.ts +0 -321
  213. package/src/panels/scrollable-list-panel.ts +0 -491
  214. package/src/panels/search-focus.ts +0 -32
  215. package/src/panels/security-panel.ts +0 -295
  216. package/src/panels/session-browser-panel.ts +0 -395
  217. package/src/panels/session-maintenance.ts +0 -125
  218. package/src/panels/subscription-panel.ts +0 -263
  219. package/src/panels/system-messages-panel.ts +0 -230
  220. package/src/panels/tasks-panel.ts +0 -344
  221. package/src/panels/thinking-panel.ts +0 -304
  222. package/src/panels/token-budget-panel.ts +0 -475
  223. package/src/panels/tool-inspector-panel.ts +0 -436
  224. package/src/panels/types.ts +0 -54
  225. package/src/renderer/panel-composite.ts +0 -158
  226. package/src/renderer/panel-tab-bar.ts +0 -69
  227. package/src/renderer/panel-workspace-bar.ts +0 -42
  228. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  229. package/src/tools/agent-harness-panel-metadata.ts +0 -211
  230. /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
@@ -1,204 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // ContextVisualizerPanel — stacked bar showing context window composition.
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
6
- import type { Line } from '../types/grid.ts';
7
- import { BasePanel } from './base-panel.ts';
8
- import { evaluateSessionMaintenance } from '@/runtime/index.ts';
9
- import type { TurnEvent } from '@/runtime/index.ts';
10
- import type { UiEventFeed } from '../runtime/ui-events.ts';
11
- import type { UiReadModel, UiSessionSnapshot } from '../runtime/ui-read-models.ts';
12
- import type { SessionMemoryQuery } from '../runtime/ui-service-queries.ts';
13
- import {
14
- buildEmptyState,
15
- buildGuidanceLine,
16
- buildMeterLine,
17
- buildPanelLine,
18
- buildStyledPanelLine,
19
- buildPanelWorkspace,
20
- DEFAULT_PANEL_PALETTE,
21
- } from './polish.ts';
22
-
23
- const C = {
24
- convFg: '#cc99ff',
25
- overFg: '#ff6666',
26
- barEmpty: '#333344',
27
- labelFg: '#8888bb',
28
- } as const;
29
-
30
-
31
-
32
- interface ContextSnapshot {
33
- input: number;
34
- limit: number;
35
- }
36
-
37
- function formatK(n: number): string {
38
- if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
39
- return String(n);
40
- }
41
-
42
- export class ContextVisualizerPanel extends BasePanel {
43
- private snapshot: ContextSnapshot = { input: 0, limit: 0 };
44
- private unsubs: Array<() => void> = [];
45
- private readonly sessionMemoryStore: SessionMemoryQuery;
46
-
47
- constructor(
48
- private readonly turnEvents: UiEventFeed<TurnEvent>,
49
- sessionMemoryStore: SessionMemoryQuery,
50
- private readonly configManager: Pick<ConfigManager, 'get'>,
51
- private getUsage?: () => { input: number; output: number; cacheRead: number; cacheWrite: number; model?: string },
52
- private contextLimit?: number,
53
- private sessionReadModel?: UiReadModel<UiSessionSnapshot>,
54
- ) {
55
- super('context', 'Context', 'C', 'ai');
56
- this.sessionMemoryStore = sessionMemoryStore;
57
- this._attachBus();
58
- this._refresh();
59
- }
60
-
61
- override onActivate(): void {
62
- this.needsRender = true;
63
- this._refresh();
64
- }
65
-
66
- override onDeactivate(): void {
67
- super.onDeactivate();
68
- }
69
-
70
- override onDestroy(): void {
71
- this._detachBus();
72
- }
73
-
74
- render(width: number, height: number): Line[] {
75
- return this.trackedRender(() => {
76
- if (height <= 0 || width <= 0) return [];
77
-
78
- const input = this.snapshot.input;
79
- const limit = this.snapshot.limit;
80
- const pct = limit > 0 ? Math.min(100, Math.round((input / limit) * 100)) : 0;
81
- const barWidth = Math.max(1, width - 2);
82
- const overLimit = limit > 0 && input > limit;
83
- const fg = overLimit ? C.overFg : C.convFg;
84
- if (limit <= 0) {
85
- return buildPanelWorkspace(width, height, {
86
- title: ' Context Usage',
87
- intro: 'Visualize current input-token pressure against the active model context window.',
88
- sections: [
89
- {
90
- lines: buildEmptyState(
91
- width,
92
- ' Context limit unavailable',
93
- 'Select a model with a known context window and submit or complete a turn to populate live context usage.',
94
- [],
95
- DEFAULT_PANEL_PALETTE,
96
- ),
97
- },
98
- ],
99
- palette: DEFAULT_PANEL_PALETTE,
100
- });
101
- }
102
-
103
- return buildPanelWorkspace(width, height, {
104
- title: ' Context Usage',
105
- intro: 'Visualize current input-token pressure against the active model context window.',
106
- sections: [
107
- {
108
- title: 'Summary',
109
- lines: [
110
- buildPanelLine(width, [
111
- [' Input ', DEFAULT_PANEL_PALETTE.label],
112
- [formatK(input), DEFAULT_PANEL_PALETTE.value],
113
- [' Limit ', DEFAULT_PANEL_PALETTE.label],
114
- [formatK(limit), DEFAULT_PANEL_PALETTE.info],
115
- [' Fill ', DEFAULT_PANEL_PALETTE.label],
116
- [`${pct}%`, overLimit ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.good],
117
- ]),
118
- ],
119
- },
120
- {
121
- title: 'Usage',
122
- lines: [
123
- this._renderBar(width, barWidth, input, limit),
124
- this._renderSegLine(width, 'Input tokens', input, pct, fg),
125
- buildPanelLine(width, [[` ${formatK(input)} / ${formatK(limit)} tokens (${pct}%) Refreshes each LLM call`, DEFAULT_PANEL_PALETTE.dim]]),
126
- ],
127
- },
128
- {
129
- title: 'Maintenance',
130
- lines: this._renderMaintenance(width),
131
- },
132
- ],
133
- palette: DEFAULT_PANEL_PALETTE,
134
- });
135
- });
136
- }
137
-
138
- private _renderBar(width: number, barWidth: number, input: number, limit: number): Line {
139
- const filled = limit > 0 ? Math.min(barWidth, Math.round((input / limit) * barWidth)) : 0;
140
- const overLimit = limit > 0 && input > limit;
141
- const barFg = overLimit ? C.overFg : C.convFg;
142
- return buildMeterLine(width, filled, barWidth, {
143
- filled: barFg,
144
- empty: C.barEmpty,
145
- label: DEFAULT_PANEL_PALETTE.dim,
146
- });
147
- }
148
-
149
- private _renderSegLine(width: number, label: string, val: number, pct: number, fg: string): Line {
150
- const labelPadded = ` ${label}`.padEnd(22);
151
- const valStr = formatK(val).padStart(7);
152
- const pctStr = `${pct}%`.padStart(5);
153
- return buildStyledPanelLine(width, [
154
- { text: labelPadded, fg: C.labelFg },
155
- { text: valStr, fg },
156
- { text: ' ', fg: DEFAULT_PANEL_PALETTE.dim },
157
- { text: pctStr, fg },
158
- ]);
159
- }
160
-
161
- private _refresh(): void {
162
- const usage = this.getUsage?.();
163
- if (usage) {
164
- this.snapshot.input = usage.input;
165
- this.snapshot.limit = this.contextLimit ?? 0;
166
- }
167
- this.markDirty();
168
- }
169
-
170
- private _renderMaintenance(width: number): Line[] {
171
- const status = evaluateSessionMaintenance({
172
- configManager: this.configManager,
173
- currentTokens: this.snapshot.input,
174
- contextWindow: this.snapshot.limit,
175
- sessionMemoryCount: this.sessionMemoryStore.list().length,
176
- session: this.sessionReadModel?.getSnapshot().session,
177
- });
178
- const lines: Line[] = [
179
- buildPanelLine(width, [[` ${status.summary}`, DEFAULT_PANEL_PALETTE.label]]),
180
- ];
181
- if (status.reasons[0]) {
182
- lines.push(buildPanelLine(width, [[` ${status.reasons[0]}`, DEFAULT_PANEL_PALETTE.dim]]));
183
- }
184
- if (status.guidanceMode !== 'off' && status.nextSteps[0]) {
185
- lines.push(buildGuidanceLine(width, status.nextSteps[0], 'open the suggested maintenance action', DEFAULT_PANEL_PALETTE));
186
- }
187
- return lines;
188
- }
189
-
190
- private _attachBus(): void {
191
- if (this.unsubs.length > 0) return;
192
- this.unsubs.push(this.turnEvents.on('TURN_COMPLETED', () => {
193
- this._refresh();
194
- }));
195
- this.unsubs.push(this.turnEvents.on('TURN_SUBMITTED', () => {
196
- this._refresh();
197
- }));
198
- }
199
-
200
- private _detachBus(): void {
201
- for (const unsub of this.unsubs) unsub();
202
- this.unsubs = [];
203
- }
204
- }
@@ -1,444 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // CostTrackerPanel — per-session / per-agent / per-plan cost estimates
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { Line } from '../types/grid.ts';
6
- import { createStyledCell, createEmptyLine } from '../types/grid.ts';
7
- import { BasePanel } from './base-panel.ts';
8
- import type { AgentEvent, TurnEvent } from '@/runtime/index.ts';
9
- import type { UiEventFeed } from '../runtime/ui-events.ts';
10
- import {
11
- buildEmptyState,
12
- buildPanelLine,
13
- buildStyledPanelLine,
14
- buildPanelWorkspace,
15
- resolveScrollablePanelSection,
16
- DEFAULT_PANEL_PALETTE,
17
- type PanelWorkspaceSection,
18
- } from './polish.ts';
19
-
20
- // ---------------------------------------------------------------------------
21
- // Pricing table (USD per 1M tokens)
22
- // ---------------------------------------------------------------------------
23
-
24
- interface ModelPricing {
25
- input: number;
26
- output: number;
27
- }
28
-
29
- const MODEL_PRICING: Record<string, ModelPricing> = {
30
- // Free tier
31
- 'openrouter/free': { input: 0, output: 0 },
32
-
33
- // InceptionLabs
34
- 'mercury-2': { input: 0.50, output: 1.50 },
35
- 'mercury-edit': { input: 0.50, output: 1.50 },
36
-
37
- // OpenAI
38
- 'gpt-5.4': { input: 5, output: 15 },
39
- 'gpt-5.3-chat-latest': { input: 3, output: 10 },
40
- 'gpt-5-mini': { input: 0.15, output: 0.60 },
41
- 'gpt-5-nano': { input: 0.05, output: 0.20 },
42
- 'gpt-oss-120b': { input: 0, output: 0 },
43
-
44
- // Anthropic (correct registry IDs)
45
- 'claude-opus-4-6': { input: 15, output: 75 },
46
- 'claude-sonnet-4-6': { input: 3, output: 15 },
47
- 'claude-haiku-4-5': { input: 0.80, output: 4 },
48
-
49
- // Google
50
- 'gemini-3.1-pro': { input: 1.25, output: 5 },
51
- 'gemini-3-flash': { input: 0.075, output: 0.30 },
52
- 'gemini-3.1-flash-lite': { input: 0.02, output: 0.10 },
53
- 'gemini-2.5-pro': { input: 1.25, output: 5 },
54
- };
55
-
56
- /**
57
- * Look up pricing from the model catalog.
58
- * Returns { input: 0, output: 0 } for free models and unknown models.
59
- */
60
- function getCostFromCatalogForPanel(modelId: string): ModelPricing {
61
- if (modelId.endsWith(':free')) return { input: 0, output: 0 };
62
- return { input: 0, output: 0 };
63
- }
64
-
65
- function getPricing(modelId: string): ModelPricing {
66
- // 2. Hardcoded table — exact match
67
- if (MODEL_PRICING[modelId]) return MODEL_PRICING[modelId]!;
68
- // 1. OpenRouter :free suffix — treat as free
69
- if (modelId.endsWith(':free')) return { input: 0, output: 0 };
70
- // 2. Prefix match (e.g. "openrouter/free:..." or "claude-sonnet-4-6-20..")
71
- for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
72
- if (modelId.startsWith(key) || modelId.includes(key)) return pricing;
73
- }
74
- // 3. Unknown model — default to free-ish safe fallback
75
- return getCostFromCatalogForPanel(modelId);
76
- }
77
-
78
- function calcCost(inputTokens: number, outputTokens: number, pricing: ModelPricing): number {
79
- return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
80
- }
81
-
82
- function formatCost(usd: number): string {
83
- if (usd === 0) return '$0.00';
84
- if (usd < 0.0001) return '<$0.0001';
85
- if (usd < 0.01) return `$${usd.toFixed(4)}`;
86
- return `$${usd.toFixed(3)}`;
87
- }
88
-
89
- function formatTokens(n: number): string {
90
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
91
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
92
- return String(n);
93
- }
94
-
95
- // ---------------------------------------------------------------------------
96
- // Internal types
97
- // ---------------------------------------------------------------------------
98
-
99
- interface UsageSnapshot {
100
- input: number;
101
- output: number;
102
- cacheRead: number;
103
- cacheWrite: number;
104
- }
105
-
106
- interface AgentEntry {
107
- id: string; // short form, e.g. first 8 chars
108
- task: string;
109
- model: string;
110
- inputTokens: number;
111
- outputTokens: number;
112
- cost: number;
113
- status: 'running' | 'done' | 'failed';
114
- }
115
-
116
- // Sparkline history (rolling last N cost-per-turn values)
117
- const SPARKLINE_BARS = '._-:=+*#';
118
- const SPARKLINE_LEN = 16;
119
-
120
- function buildSparkline(history: number[]): string {
121
- if (history.length === 0) return '';
122
- const max = Math.max(...history);
123
- if (max === 0) return '.'.repeat(history.length);
124
- return history
125
- .map(v => {
126
- const idx = Math.round((v / max) * (SPARKLINE_BARS.length - 1));
127
- return SPARKLINE_BARS[idx] ?? '.';
128
- })
129
- .join('');
130
- }
131
-
132
- // ---------------------------------------------------------------------------
133
- // Colour palette
134
- // ---------------------------------------------------------------------------
135
-
136
- const C = {
137
- header: '#ffffff',
138
- label: '#aaaaaa',
139
- value: '#00ff88',
140
- cost: '#ffdd44',
141
- alert: '#ff4444',
142
- dim: '#555555',
143
- model: '#88aaff',
144
- running: '#88aaff',
145
- done: '#00ff88',
146
- failed: '#ff4444',
147
- separator: '#333333',
148
- bg: '',
149
- } as const;
150
-
151
- // ---------------------------------------------------------------------------
152
- // CostTrackerPanel
153
- // ---------------------------------------------------------------------------
154
-
155
- export class CostTrackerPanel extends BasePanel {
156
- // Session-level usage (from main orchestrator)
157
- private sessionUsage: UsageSnapshot = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
158
- private sessionModel = 'unknown';
159
-
160
- // Per-turn cost history for sparkline
161
- private costHistory: number[] = [];
162
- private lastSessionCost = 0;
163
-
164
- // Per-agent tracking (keyed by agent id)
165
- private agents = new Map<string, AgentEntry>();
166
-
167
- // Budget alert threshold in USD (0 = disabled)
168
- private budgetThreshold: number;
169
-
170
- // Scroll offset for agent list
171
- private scrollOffset = 0;
172
-
173
- // Unsubscribe functions
174
- private unsubs: Array<() => void> = [];
175
-
176
- // Getter for live orchestrator usage
177
- private readonly getOrchestratorUsage: () => UsageSnapshot & { model?: string };
178
-
179
- constructor(
180
- turnEvents: UiEventFeed<TurnEvent>,
181
- agentEvents: UiEventFeed<AgentEvent>,
182
- getOrchestratorUsage: () => UsageSnapshot & { model?: string },
183
- opts: { budgetThreshold?: number } = {},
184
- ) {
185
- super('cost', 'Cost', '$', 'monitoring');
186
- this.getOrchestratorUsage = getOrchestratorUsage;
187
- this.budgetThreshold = opts.budgetThreshold ?? 0;
188
- this.attachEvents(turnEvents, agentEvents);
189
- }
190
-
191
- // -------------------------------------------------------------------------
192
- // Bus wiring
193
- // -------------------------------------------------------------------------
194
-
195
- private attachEvents(turnEvents: UiEventFeed<TurnEvent>, agentEvents: UiEventFeed<AgentEvent>): void {
196
- // Refresh after every completed turn
197
- this.unsubs.push(
198
- turnEvents.on('TURN_COMPLETED', () => this.onTurnComplete()),
199
- );
200
-
201
- // Track runtime worker events for cost attribution only.
202
- this.unsubs.push(
203
- agentEvents.on('AGENT_SPAWNING', (payload) => {
204
- this.agents.set(payload.agentId, {
205
- id: payload.agentId.slice(0, 8),
206
- task: payload.task.length > 40 ? payload.task.slice(0, 37) + '...' : payload.task,
207
- model: 'unknown',
208
- inputTokens: 0,
209
- outputTokens: 0,
210
- cost: 0,
211
- status: 'running',
212
- });
213
- this.markDirty();
214
- }),
215
- );
216
-
217
- // Agent completed — capture token data from result if available
218
- this.unsubs.push(
219
- agentEvents.on('AGENT_COMPLETED', (payload) => {
220
- const entry = this.agents.get(payload.agentId);
221
- if (entry) {
222
- entry.status = 'done';
223
- this.markDirty();
224
- }
225
- }),
226
- );
227
-
228
- // Agent error
229
- this.unsubs.push(
230
- agentEvents.on('AGENT_FAILED', (payload) => {
231
- const entry = this.agents.get(payload.agentId);
232
- if (entry) {
233
- entry.status = 'failed';
234
- this.markDirty();
235
- }
236
- }),
237
- );
238
- }
239
-
240
- private onTurnComplete(): void {
241
- const usage = this.getOrchestratorUsage();
242
- this.sessionUsage = {
243
- input: usage.input,
244
- output: usage.output,
245
- cacheRead: usage.cacheRead,
246
- cacheWrite: usage.cacheWrite,
247
- };
248
- if (usage.model) this.sessionModel = usage.model;
249
-
250
- // Record cost delta for sparkline
251
- const sessionProvider = this.sessionModel.includes('/') ? this.sessionModel.split('/')[0]! : '';
252
- const pricing = getPricing(this.sessionModel);
253
- const totalCost = calcCost(usage.input + usage.cacheRead + usage.cacheWrite, usage.output, pricing);
254
- const delta = Math.max(0, totalCost - this.lastSessionCost);
255
- this.lastSessionCost = totalCost;
256
- this.costHistory.push(delta);
257
- if (this.costHistory.length > SPARKLINE_LEN) {
258
- this.costHistory.shift();
259
- }
260
-
261
- this.markDirty();
262
- }
263
-
264
- // -------------------------------------------------------------------------
265
- // Lifecycle
266
- // -------------------------------------------------------------------------
267
-
268
- override onActivate(): void {
269
- // Sync latest usage on activation
270
- const usage = this.getOrchestratorUsage();
271
- this.sessionUsage = {
272
- input: usage.input,
273
- output: usage.output,
274
- cacheRead: usage.cacheRead,
275
- cacheWrite: usage.cacheWrite,
276
- };
277
- this.needsRender = true;
278
- }
279
-
280
- override onDestroy(): void {
281
- for (const unsub of this.unsubs) unsub();
282
- this.unsubs = [];
283
- }
284
-
285
- // -------------------------------------------------------------------------
286
- // Input
287
- // -------------------------------------------------------------------------
288
-
289
- handleInput(key: string): boolean {
290
- switch (key) {
291
- case 'up': return this.scroll(-1);
292
- case 'down': return this.scroll(1);
293
- case 'pageup': return this.scroll(-10);
294
- case 'pagedown': return this.scroll(10);
295
- default: return false;
296
- }
297
- }
298
-
299
- private scroll(delta: number): boolean {
300
- const prev = this.scrollOffset;
301
- this.scrollOffset = Math.max(0, this.scrollOffset + delta);
302
- if (this.scrollOffset !== prev) this.markDirty();
303
- return true;
304
- }
305
-
306
- // -------------------------------------------------------------------------
307
- // Render
308
- // -------------------------------------------------------------------------
309
-
310
- render(width: number, height: number): Line[] {
311
- if (height <= 0 || width <= 0) return [];
312
-
313
- const sessionProvider = this.sessionModel.includes('/') ? this.sessionModel.split('/')[0]! : '';
314
- const pricing = getPricing(this.sessionModel);
315
- const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
316
- const sessionCost = calcCost(this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite, this.sessionUsage.output, pricing);
317
- const overBudget = this.budgetThreshold > 0 && sessionCost > this.budgetThreshold;
318
- const sparkline = buildSparkline(this.costHistory);
319
- const costStr = formatCost(sessionCost);
320
- const costFg = overBudget ? C.alert : C.cost;
321
- const budgetStr = this.budgetThreshold > 0
322
- ? ` / ${formatCost(this.budgetThreshold)}`
323
- : '';
324
- const alertStr = overBudget ? ' ! OVER BUDGET' : '';
325
- const sessionLines: Line[] = [
326
- this.renderKeyValue(width, ' Total', `${costStr}${budgetStr}${alertStr}`, costFg),
327
- ];
328
- if (sparkline.length > 0) sessionLines.push(this.renderLabeledLine(width, ' Trend', sparkline, C.value));
329
- sessionLines.push(this.renderKeyValue(width, ' Input', formatTokens(this.sessionUsage.input), C.label));
330
- sessionLines.push(this.renderKeyValue(width, ' Output', formatTokens(this.sessionUsage.output), C.label));
331
- if (this.sessionUsage.cacheRead > 0 || this.sessionUsage.cacheWrite > 0) {
332
- sessionLines.push(this.renderKeyValue(width, ' Cache W', formatTokens(this.sessionUsage.cacheWrite), C.dim));
333
- sessionLines.push(this.renderKeyValue(width, ' Cache R', formatTokens(this.sessionUsage.cacheRead), C.dim));
334
- }
335
- sessionLines.push(this.renderKeyValue(width, ' Total T', formatTokens(totalInputTokens + this.sessionUsage.output), C.label));
336
- sessionLines.push(this.renderKeyValue(width, ' Model', this.sessionModel, C.model));
337
-
338
- const sections: PanelWorkspaceSection[] = [
339
- {
340
- title: 'Session Cost',
341
- lines: sessionLines,
342
- },
343
- ];
344
-
345
- const agentList = Array.from(this.agents.values());
346
- if (agentList.length > 0) {
347
- const planCost = agentList.reduce((sum, a) => sum + a.cost, 0);
348
- const agentRows: Line[] = [
349
- this.renderKeyValue(width, ' Plan total', formatCost(planCost + sessionCost), C.cost),
350
- this.renderDivider(width),
351
- ];
352
- for (const agent of agentList) {
353
- const statusFg = agent.status === 'running' ? C.running
354
- : agent.status === 'failed' ? C.failed
355
- : C.done;
356
- const statusIcon = agent.status === 'running' ? '…'
357
- : agent.status === 'failed' ? '✕'
358
- : '✓';
359
-
360
- const agentLabel = `${statusIcon} ${agent.id}`;
361
- const taskText = agent.task;
362
- agentRows.push(this.renderAgent(width, agentLabel, taskText, statusFg));
363
-
364
- if (agent.inputTokens > 0 || agent.outputTokens > 0) {
365
- const tokenInfo = ` in:${formatTokens(agent.inputTokens)} out:${formatTokens(agent.outputTokens)} ${formatCost(agent.cost)}`;
366
- agentRows.push(this.renderLabeledLine(width, '', tokenInfo, C.dim));
367
- }
368
- }
369
- const sessionSection: PanelWorkspaceSection = sections[0]!;
370
- const agentsSection = resolveScrollablePanelSection(width, height, {
371
- intro: 'Track per-session and per-agent token spend using model pricing and live usage snapshots.',
372
- footerLines: [
373
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll agents', DEFAULT_PANEL_PALETTE.dim]]),
374
- ],
375
- palette: DEFAULT_PANEL_PALETTE,
376
- beforeSections: [sessionSection],
377
- section: {
378
- title: 'Agents',
379
- scrollableLines: agentRows,
380
- scrollOffset: this.scrollOffset,
381
- minRows: 4,
382
- },
383
- });
384
- this.scrollOffset = agentsSection.scrollOffset;
385
- sections.push(agentsSection.section);
386
- } else {
387
- sections.push({
388
- title: 'Agents',
389
- lines: buildEmptyState(
390
- width,
391
- ' No delegated agent cost records',
392
- 'Agent-level cost estimates appear only for explicit delegated build/review session records.',
393
- [],
394
- DEFAULT_PANEL_PALETTE,
395
- ),
396
- });
397
- }
398
-
399
- return buildPanelWorkspace(width, height, {
400
- title: ' Cost Tracker',
401
- intro: 'Track per-session and per-agent token spend using model pricing and live usage snapshots.',
402
- sections,
403
- footerLines: [
404
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll agents', DEFAULT_PANEL_PALETTE.dim]]),
405
- ],
406
- palette: DEFAULT_PANEL_PALETTE,
407
- });
408
- }
409
-
410
- // -------------------------------------------------------------------------
411
- // Render helpers
412
- // -------------------------------------------------------------------------
413
-
414
- private renderKeyValue(width: number, label: string, value: string, valueFg: string): Line {
415
- const LABEL_W = 10;
416
- return buildStyledPanelLine(width, [
417
- { text: label.padEnd(LABEL_W).slice(0, LABEL_W), fg: C.label, bg: C.bg },
418
- { text: ': ', fg: C.dim, bg: C.bg },
419
- { text: value, fg: valueFg, bg: C.bg, bold: true },
420
- ]);
421
- }
422
-
423
- private renderLabeledLine(width: number, label: string, value: string, valueFg: string): Line {
424
- return buildStyledPanelLine(width, [
425
- ...(label.length > 0 ? [{ text: `${label.slice(0, 10).padEnd(10)} `, fg: C.label }] : []),
426
- { text: value, fg: valueFg },
427
- ]);
428
- }
429
-
430
- private renderAgent(width: number, label: string, task: string, fg: string): Line {
431
- const LABEL_W = 12;
432
- const remaining = width - LABEL_W - 1;
433
- const trimmed = task.length > remaining ? task.slice(0, remaining - 3) + '...' : task;
434
- return buildStyledPanelLine(width, [
435
- { text: `${label.padEnd(LABEL_W).slice(0, LABEL_W)} `, fg, bold: true },
436
- { text: trimmed, fg: C.label },
437
- ]);
438
- }
439
-
440
- private renderDivider(width: number): Line {
441
- return buildStyledPanelLine(width, [{ text: '─'.repeat(width), fg: C.separator }]);
442
- }
443
-
444
- }