@pellux/goodvibes-tui 0.27.0 → 0.29.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -4,21 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Line } from '../types/grid.ts';
|
|
6
6
|
import { createStyledCell, createEmptyLine } from '../types/grid.ts';
|
|
7
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
7
8
|
import { BasePanel } from './base-panel.ts';
|
|
8
9
|
import type { AgentEvent, TurnEvent } from '@/runtime/index.ts';
|
|
9
10
|
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
10
11
|
import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
11
12
|
import {
|
|
12
13
|
buildEmptyState,
|
|
14
|
+
buildKeyboardHints,
|
|
15
|
+
buildMeterLine,
|
|
13
16
|
buildPanelLine,
|
|
14
17
|
buildStyledPanelLine,
|
|
18
|
+
buildTable,
|
|
15
19
|
buildPanelWorkspace,
|
|
16
20
|
resolveScrollablePanelSection,
|
|
17
21
|
extendPalette,
|
|
18
22
|
DEFAULT_PANEL_PALETTE,
|
|
19
23
|
type PanelWorkspaceSection,
|
|
20
24
|
} from './polish.ts';
|
|
21
|
-
import {
|
|
25
|
+
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
26
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
27
|
+
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
22
28
|
|
|
23
29
|
// Pricing lookups are provided by ../export/cost-utils.ts (single source of truth).
|
|
24
30
|
|
|
@@ -30,9 +36,7 @@ function formatCost(usd: number): string {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
function formatTokens(n: number): string {
|
|
33
|
-
|
|
34
|
-
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
35
|
-
return String(n);
|
|
39
|
+
return abbreviateCount(n);
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
// ---------------------------------------------------------------------------
|
|
@@ -100,15 +104,27 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
100
104
|
// Per-agent tracking (keyed by agent id)
|
|
101
105
|
private agents = new Map<string, AgentEntry>();
|
|
102
106
|
|
|
103
|
-
// Budget alert threshold in USD (0 = disabled)
|
|
107
|
+
// Budget alert threshold in USD (0 = disabled). Mutable at runtime via the
|
|
108
|
+
// in-panel 'b' numeric entry or the /cost budget <usd> command — both call
|
|
109
|
+
// setBudgetThreshold() below.
|
|
104
110
|
private budgetThreshold: number;
|
|
105
111
|
|
|
112
|
+
// Draft buffer for the in-panel budget-entry mode ('b' key). Non-null while
|
|
113
|
+
// entry is active; unlike LocalAuthPanel's masked entry, the value is not
|
|
114
|
+
// secret so it is echoed directly.
|
|
115
|
+
private budgetEntry: string | null = null;
|
|
116
|
+
|
|
106
117
|
// Scroll offset for agent list
|
|
107
118
|
private scrollOffset = 0;
|
|
108
119
|
|
|
109
120
|
// Unsubscribe functions
|
|
110
121
|
private unsubs: Array<() => void> = [];
|
|
111
122
|
|
|
123
|
+
// Polls getAgentStatus() for agents still marked 'running' so their token/
|
|
124
|
+
// cost columns fill in as usage streams, instead of staying pinned at
|
|
125
|
+
// 'unknown'/$0 until AGENT_COMPLETED fires.
|
|
126
|
+
private statusPollTimer: ReturnType<typeof setInterval> | null = null;
|
|
127
|
+
|
|
112
128
|
// Getter for live orchestrator usage
|
|
113
129
|
private readonly getOrchestratorUsage: () => UsageSnapshot & { model?: string };
|
|
114
130
|
|
|
@@ -122,7 +138,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
122
138
|
getOrchestratorUsage: () => UsageSnapshot & { model?: string },
|
|
123
139
|
opts: { budgetThreshold?: number; getAgentStatus?: (agentId: string) => AgentRecord | null } = {},
|
|
124
140
|
) {
|
|
125
|
-
super('cost', 'Cost', '$', '
|
|
141
|
+
super('cost', 'Cost', '$', 'providers');
|
|
126
142
|
this.getOrchestratorUsage = getOrchestratorUsage;
|
|
127
143
|
this.getAgentStatus = opts.getAgentStatus;
|
|
128
144
|
this.budgetThreshold = opts.budgetThreshold ?? 0;
|
|
@@ -136,7 +152,14 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
136
152
|
private attachEvents(turnEvents: UiEventFeed<TurnEvent>, agentEvents: UiEventFeed<AgentEvent>): void {
|
|
137
153
|
// Refresh after every completed turn
|
|
138
154
|
this.unsubs.push(
|
|
139
|
-
turnEvents.on('TURN_COMPLETED', () => this.
|
|
155
|
+
turnEvents.on('TURN_COMPLETED', () => this.refreshSessionCost()),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Refresh mid-turn too: a single turn can span many LLM calls (tool
|
|
159
|
+
// loops), so waiting for TURN_COMPLETED leaves the meter frozen for the
|
|
160
|
+
// whole turn. LLM_RESPONSE_RECEIVED fires per call — same refresh path.
|
|
161
|
+
this.unsubs.push(
|
|
162
|
+
turnEvents.on('LLM_RESPONSE_RECEIVED', () => this.refreshSessionCost()),
|
|
140
163
|
);
|
|
141
164
|
|
|
142
165
|
// Track agent spawns
|
|
@@ -144,7 +167,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
144
167
|
agentEvents.on('AGENT_SPAWNING', (payload) => {
|
|
145
168
|
this.agents.set(payload.agentId, {
|
|
146
169
|
id: payload.agentId.slice(0, 8),
|
|
147
|
-
task: payload.task
|
|
170
|
+
task: truncateDisplay(payload.task, 40),
|
|
148
171
|
model: 'unknown',
|
|
149
172
|
inputTokens: 0,
|
|
150
173
|
outputTokens: 0,
|
|
@@ -166,8 +189,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
166
189
|
if (rec?.usage) {
|
|
167
190
|
entry.inputTokens = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
|
|
168
191
|
entry.outputTokens = rec.usage.outputTokens;
|
|
169
|
-
|
|
170
|
-
entry.cost = (entry.inputTokens * pricing.input + entry.outputTokens * pricing.output) / 1_000_000;
|
|
192
|
+
entry.cost = calcSessionCost(rec.usage.inputTokens, rec.usage.outputTokens, rec.usage.cacheReadTokens ?? 0, rec.usage.cacheWriteTokens ?? 0, rec.model ?? 'unknown');
|
|
171
193
|
if (rec.model && rec.model !== 'unknown') entry.model = rec.model;
|
|
172
194
|
}
|
|
173
195
|
}
|
|
@@ -188,7 +210,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
188
210
|
);
|
|
189
211
|
}
|
|
190
212
|
|
|
191
|
-
private
|
|
213
|
+
private refreshSessionCost(): void {
|
|
192
214
|
const usage = this.getOrchestratorUsage();
|
|
193
215
|
this.sessionUsage = {
|
|
194
216
|
input: usage.input,
|
|
@@ -199,9 +221,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
199
221
|
if (usage.model) this.sessionModel = usage.model;
|
|
200
222
|
|
|
201
223
|
// Record cost delta for sparkline
|
|
202
|
-
const
|
|
203
|
-
const billableInput = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
204
|
-
const totalCost = (billableInput * pricing.input + usage.output * pricing.output) / 1_000_000;
|
|
224
|
+
const totalCost = calcSessionCost(usage.input, usage.output, usage.cacheRead, usage.cacheWrite, this.sessionModel);
|
|
205
225
|
const delta = Math.max(0, totalCost - this.lastSessionCost);
|
|
206
226
|
this.lastSessionCost = totalCost;
|
|
207
227
|
this.costHistory.push(delta);
|
|
@@ -226,6 +246,13 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
226
246
|
cacheWrite: usage.cacheWrite,
|
|
227
247
|
};
|
|
228
248
|
this.needsRender = true;
|
|
249
|
+
this.startStatusPollTimer();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
override onDeactivate(): void {
|
|
253
|
+
// Stop polling while hidden — nothing to catch up on that a fresh
|
|
254
|
+
// onActivate() poll won't cover.
|
|
255
|
+
this.stopStatusPollTimer();
|
|
229
256
|
}
|
|
230
257
|
|
|
231
258
|
override onDestroy(): void {
|
|
@@ -233,11 +260,69 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
233
260
|
this.unsubs = [];
|
|
234
261
|
}
|
|
235
262
|
|
|
263
|
+
private startStatusPollTimer(): void {
|
|
264
|
+
if (this.statusPollTimer !== null || !this.getAgentStatus) return;
|
|
265
|
+
this.statusPollTimer = this.registerTimer(setInterval(() => this.pollRunningAgents(), 3_000));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private stopStatusPollTimer(): void {
|
|
269
|
+
if (this.statusPollTimer !== null) {
|
|
270
|
+
this.clearTimer(this.statusPollTimer);
|
|
271
|
+
this.statusPollTimer = null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Poll getAgentStatus() for every agent still marked 'running'. Completion
|
|
277
|
+
* already captures real usage via AGENT_COMPLETED, but a long-running agent
|
|
278
|
+
* would otherwise show 'unknown' tokens / $0 cost for its entire lifetime —
|
|
279
|
+
* this fills the row in as usage becomes available mid-flight.
|
|
280
|
+
*/
|
|
281
|
+
private pollRunningAgents(): void {
|
|
282
|
+
if (!this.getAgentStatus) return;
|
|
283
|
+
let changed = false;
|
|
284
|
+
for (const [agentId, entry] of this.agents) {
|
|
285
|
+
if (entry.status !== 'running') continue;
|
|
286
|
+
const rec = this.getAgentStatus(agentId);
|
|
287
|
+
if (!rec?.usage) continue;
|
|
288
|
+
const inputTokens = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
|
|
289
|
+
const outputTokens = rec.usage.outputTokens;
|
|
290
|
+
const cost = calcSessionCost(rec.usage.inputTokens, rec.usage.outputTokens, rec.usage.cacheReadTokens ?? 0, rec.usage.cacheWriteTokens ?? 0, rec.model ?? entry.model);
|
|
291
|
+
if (inputTokens !== entry.inputTokens || outputTokens !== entry.outputTokens || cost !== entry.cost) {
|
|
292
|
+
entry.inputTokens = inputTokens;
|
|
293
|
+
entry.outputTokens = outputTokens;
|
|
294
|
+
entry.cost = cost;
|
|
295
|
+
changed = true;
|
|
296
|
+
}
|
|
297
|
+
if (rec.model && rec.model !== 'unknown' && rec.model !== entry.model) {
|
|
298
|
+
entry.model = rec.model;
|
|
299
|
+
changed = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (changed) this.markDirty();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Set the budget alert threshold (USD; 0 disables it). Shared by the
|
|
306
|
+
* in-panel 'b' entry and the /cost budget <usd> command. */
|
|
307
|
+
public setBudgetThreshold(usd: number): void {
|
|
308
|
+
if (!Number.isFinite(usd) || usd < 0) return;
|
|
309
|
+
this.budgetThreshold = usd;
|
|
310
|
+
this.markDirty();
|
|
311
|
+
}
|
|
312
|
+
|
|
236
313
|
// -------------------------------------------------------------------------
|
|
237
314
|
// Input
|
|
238
315
|
// -------------------------------------------------------------------------
|
|
239
316
|
|
|
240
317
|
handleInput(key: string): boolean {
|
|
318
|
+
if (this.budgetEntry !== null) return this.handleBudgetEntryInput(key);
|
|
319
|
+
|
|
320
|
+
if (key === 'b') {
|
|
321
|
+
this.budgetEntry = this.budgetThreshold > 0 ? String(this.budgetThreshold) : '';
|
|
322
|
+
this.markDirty();
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
241
326
|
switch (key) {
|
|
242
327
|
case 'up': return this.scroll(-1);
|
|
243
328
|
case 'down': return this.scroll(1);
|
|
@@ -247,9 +332,53 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
247
332
|
}
|
|
248
333
|
}
|
|
249
334
|
|
|
335
|
+
private handleBudgetEntryInput(key: string): boolean {
|
|
336
|
+
if (key === 'escape') {
|
|
337
|
+
this.budgetEntry = null;
|
|
338
|
+
this.markDirty();
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
if (key === 'enter' || key === 'return') {
|
|
342
|
+
const raw = this.budgetEntry ?? '';
|
|
343
|
+
this.budgetEntry = null;
|
|
344
|
+
const parsed = Number(raw);
|
|
345
|
+
if (raw.length > 0 && Number.isFinite(parsed) && parsed >= 0) {
|
|
346
|
+
this.setBudgetThreshold(parsed);
|
|
347
|
+
} else {
|
|
348
|
+
this.markDirty();
|
|
349
|
+
}
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
if (isTextBackspace(key)) {
|
|
353
|
+
if (this.budgetEntry && this.budgetEntry.length > 0) {
|
|
354
|
+
this.budgetEntry = this.budgetEntry.slice(0, -1);
|
|
355
|
+
this.markDirty();
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
// Digits and a single decimal point only — this is a USD amount, not free text.
|
|
360
|
+
if (key.length === 1 && /[0-9.]/.test(key) && !(key === '.' && this.budgetEntry?.includes('.'))) {
|
|
361
|
+
this.budgetEntry = (this.budgetEntry ?? '') + key;
|
|
362
|
+
this.markDirty();
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
return true; // absorb everything else while entry is active
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Scroll the agent list. Over-consumption fix: only absorbs the key (and
|
|
370
|
+
* only advances) when the agent list is actually long enough to scroll —
|
|
371
|
+
* mirrors the same `> 5` threshold render() uses to decide whether to show
|
|
372
|
+
* scroll hints (below). Previously this always returned true and grew
|
|
373
|
+
* scrollOffset unboundedly even with a handful of agents, swallowing
|
|
374
|
+
* up/down/page keys that had nothing to do.
|
|
375
|
+
*/
|
|
250
376
|
private scroll(delta: number): boolean {
|
|
377
|
+
const agentCount = this.agents.size;
|
|
378
|
+
if (agentCount <= 5) return false;
|
|
379
|
+
const maxOffset = Math.max(0, agentCount - 1);
|
|
251
380
|
const prev = this.scrollOffset;
|
|
252
|
-
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
381
|
+
this.scrollOffset = Math.max(0, Math.min(maxOffset, this.scrollOffset + delta));
|
|
253
382
|
if (this.scrollOffset !== prev) this.markDirty();
|
|
254
383
|
return true;
|
|
255
384
|
}
|
|
@@ -260,10 +389,10 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
260
389
|
|
|
261
390
|
render(width: number, height: number): Line[] {
|
|
262
391
|
if (height <= 0 || width <= 0) return [];
|
|
392
|
+
if (this.budgetEntry !== null) return this.renderBudgetEntryPrompt(width, height);
|
|
263
393
|
|
|
264
|
-
const pricing = getPricing(this.sessionModel);
|
|
265
394
|
const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
|
|
266
|
-
const sessionCost = (
|
|
395
|
+
const sessionCost = calcSessionCost(this.sessionUsage.input, this.sessionUsage.output, this.sessionUsage.cacheRead, this.sessionUsage.cacheWrite, this.sessionModel);
|
|
267
396
|
const overBudget = this.budgetThreshold > 0 && sessionCost > this.budgetThreshold;
|
|
268
397
|
const sparkline = buildSparkline(this.costHistory);
|
|
269
398
|
const costStr = formatCost(sessionCost);
|
|
@@ -275,6 +404,20 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
275
404
|
const sessionLines: Line[] = [
|
|
276
405
|
this.renderKeyValue(width, ' Total', `${costStr}${budgetStr}${alertStr}`, costFg),
|
|
277
406
|
];
|
|
407
|
+
// Budget meter — the single most important glance for this panel: how much
|
|
408
|
+
// of the configured budget the session has consumed. Only shown when a
|
|
409
|
+
// budget is set (otherwise the bar would be meaningless).
|
|
410
|
+
if (this.budgetThreshold > 0) {
|
|
411
|
+
const ratio = sessionCost / this.budgetThreshold;
|
|
412
|
+
const BAR_W = 24;
|
|
413
|
+
const filled = Math.max(0, Math.min(BAR_W, Math.round(ratio * BAR_W)));
|
|
414
|
+
const meterFg = overBudget ? C.bad : ratio >= 0.8 ? C.warn : C.good;
|
|
415
|
+
const pctStr = `${Math.round(ratio * 100)}%`;
|
|
416
|
+
sessionLines.push(buildMeterLine(width, filled, BAR_W,
|
|
417
|
+
{ filled: meterFg, empty: C.separator, label: C.label },
|
|
418
|
+
{ prefix: ' Budget [', suffix: `] ${pctStr}` },
|
|
419
|
+
));
|
|
420
|
+
}
|
|
278
421
|
if (sparkline.length > 0) sessionLines.push(this.renderLabeledLine(width, ' Trend', sparkline, C.good));
|
|
279
422
|
sessionLines.push(this.renderKeyValue(width, ' Input', formatTokens(this.sessionUsage.input), C.label));
|
|
280
423
|
sessionLines.push(this.renderKeyValue(width, ' Output', formatTokens(this.sessionUsage.output), C.label));
|
|
@@ -293,35 +436,64 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
293
436
|
];
|
|
294
437
|
|
|
295
438
|
const agentList = Array.from(this.agents.values());
|
|
439
|
+
const scrollableAgents = agentList.length > 5;
|
|
440
|
+
// Footer hints adapt to whether the agent list is long enough to scroll.
|
|
441
|
+
const hintRow = scrollableAgents
|
|
442
|
+
? buildKeyboardHints(width, [
|
|
443
|
+
{ keys: 'Up/Down', label: 'scroll agents' },
|
|
444
|
+
{ keys: 'PgUp/PgDn', label: 'page' },
|
|
445
|
+
{ keys: 'b', label: 'set budget' },
|
|
446
|
+
], DEFAULT_PANEL_PALETTE)
|
|
447
|
+
: buildKeyboardHints(width, [
|
|
448
|
+
{ keys: 'b', label: 'set budget alert' },
|
|
449
|
+
], DEFAULT_PANEL_PALETTE);
|
|
296
450
|
if (agentList.length > 0) {
|
|
297
451
|
const planCost = agentList.reduce((sum, a) => sum + a.cost, 0);
|
|
452
|
+
const running = agentList.filter((a) => a.status === 'running').length;
|
|
453
|
+
const failed = agentList.filter((a) => a.status === 'failed').length;
|
|
298
454
|
const agentRows: Line[] = [
|
|
299
|
-
|
|
300
|
-
|
|
455
|
+
buildStyledPanelLine(width, [
|
|
456
|
+
{ text: ' Plan total ', fg: C.label },
|
|
457
|
+
{ text: formatCost(planCost + sessionCost), fg: C.cost, bold: true },
|
|
458
|
+
{ text: ` ${agentList.length} agent${agentList.length === 1 ? '' : 's'}`, fg: C.dim },
|
|
459
|
+
...(running > 0 ? [{ text: ` ${running} running`, fg: C.running }] : []),
|
|
460
|
+
...(failed > 0 ? [{ text: ` ${failed} failed`, fg: C.bad }] : []),
|
|
461
|
+
]),
|
|
462
|
+
// Per-agent cost ledger as an aligned table — agent, model, tokens, cost
|
|
463
|
+
// line up in columns instead of wrapping across two ragged rows.
|
|
464
|
+
...buildTable(
|
|
465
|
+
width,
|
|
466
|
+
[
|
|
467
|
+
{ label: 'Agent', width: 14 },
|
|
468
|
+
{ label: 'Model', width: 18 },
|
|
469
|
+
{ label: 'In', width: 7, align: 'right' },
|
|
470
|
+
{ label: 'Out', width: 7, align: 'right' },
|
|
471
|
+
{ label: 'Cost', align: 'right' },
|
|
472
|
+
],
|
|
473
|
+
agentList.map((agent) => {
|
|
474
|
+
const statusFg = agent.status === 'running' ? C.running
|
|
475
|
+
: agent.status === 'failed' ? C.bad
|
|
476
|
+
: C.good;
|
|
477
|
+
const statusIcon = agent.status === 'running' ? '…'
|
|
478
|
+
: agent.status === 'failed' ? '✕'
|
|
479
|
+
: '✓';
|
|
480
|
+
return {
|
|
481
|
+
cells: [
|
|
482
|
+
{ text: `${statusIcon} ${agent.id}`, fg: statusFg },
|
|
483
|
+
{ text: agent.model, fg: C.model },
|
|
484
|
+
{ text: agent.inputTokens > 0 ? formatTokens(agent.inputTokens) : '-', fg: C.dim },
|
|
485
|
+
{ text: agent.outputTokens > 0 ? formatTokens(agent.outputTokens) : '-', fg: C.dim },
|
|
486
|
+
{ text: agent.cost > 0 ? formatCost(agent.cost) : '-', fg: agent.cost > 0 ? C.cost : C.dim },
|
|
487
|
+
],
|
|
488
|
+
};
|
|
489
|
+
}),
|
|
490
|
+
DEFAULT_PANEL_PALETTE,
|
|
491
|
+
),
|
|
301
492
|
];
|
|
302
|
-
for (const agent of agentList) {
|
|
303
|
-
const statusFg = agent.status === 'running' ? C.running
|
|
304
|
-
: agent.status === 'failed' ? C.bad
|
|
305
|
-
: C.good;
|
|
306
|
-
const statusIcon = agent.status === 'running' ? '…'
|
|
307
|
-
: agent.status === 'failed' ? '✕'
|
|
308
|
-
: '✓';
|
|
309
|
-
|
|
310
|
-
const agentLabel = `${statusIcon} ${agent.id}`;
|
|
311
|
-
const taskText = agent.task;
|
|
312
|
-
agentRows.push(this.renderAgent(width, agentLabel, taskText, statusFg));
|
|
313
|
-
|
|
314
|
-
if (agent.inputTokens > 0 || agent.outputTokens > 0) {
|
|
315
|
-
const tokenInfo = ` in:${formatTokens(agent.inputTokens)} out:${formatTokens(agent.outputTokens)} ${formatCost(agent.cost)}`;
|
|
316
|
-
agentRows.push(this.renderLabeledLine(width, '', tokenInfo, C.dim));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
493
|
const sessionSection: PanelWorkspaceSection = sections[0]!;
|
|
320
494
|
const agentsSection = resolveScrollablePanelSection(width, height, {
|
|
321
495
|
intro: 'Track per-session and per-agent token spend using model pricing and live usage snapshots.',
|
|
322
|
-
footerLines: [
|
|
323
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll agents', DEFAULT_PANEL_PALETTE.dim]]),
|
|
324
|
-
],
|
|
496
|
+
footerLines: [hintRow],
|
|
325
497
|
palette: DEFAULT_PANEL_PALETTE,
|
|
326
498
|
beforeSections: [sessionSection],
|
|
327
499
|
section: {
|
|
@@ -329,6 +501,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
329
501
|
scrollableLines: agentRows,
|
|
330
502
|
scrollOffset: this.scrollOffset,
|
|
331
503
|
minRows: 4,
|
|
504
|
+
appendWindowSummary: scrollableAgents ? { dimColor: DEFAULT_PANEL_PALETTE.dim } : undefined,
|
|
332
505
|
},
|
|
333
506
|
});
|
|
334
507
|
this.scrollOffset = agentsSection.scrollOffset;
|
|
@@ -340,7 +513,13 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
340
513
|
width,
|
|
341
514
|
' No agents spawned this session',
|
|
342
515
|
'Agent-level cost estimates appear here once delegated or background agents start running.',
|
|
343
|
-
|
|
516
|
+
// WO-160: 'b' already opens the in-panel budget-entry field (see
|
|
517
|
+
// handleInput/handleBudgetEntryInput above) and is advertised in
|
|
518
|
+
// the footer's 'b: set budget' hint, so the printed
|
|
519
|
+
// '/cost budget <usd>' command was a redundant action substitute.
|
|
520
|
+
[
|
|
521
|
+
{ command: 'b', summary: 'set a session budget alert to track spend against a cap' },
|
|
522
|
+
],
|
|
344
523
|
DEFAULT_PANEL_PALETTE,
|
|
345
524
|
),
|
|
346
525
|
});
|
|
@@ -350,9 +529,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
350
529
|
title: ' Cost Tracker',
|
|
351
530
|
intro: 'Track per-session and per-agent token spend using model pricing and live usage snapshots.',
|
|
352
531
|
sections,
|
|
353
|
-
footerLines: [
|
|
354
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll agents', DEFAULT_PANEL_PALETTE.dim]]),
|
|
355
|
-
],
|
|
532
|
+
footerLines: [hintRow],
|
|
356
533
|
palette: DEFAULT_PANEL_PALETTE,
|
|
357
534
|
});
|
|
358
535
|
}
|
|
@@ -364,7 +541,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
364
541
|
private renderKeyValue(width: number, label: string, value: string, valueFg: string): Line {
|
|
365
542
|
const LABEL_W = 10;
|
|
366
543
|
return buildStyledPanelLine(width, [
|
|
367
|
-
{ text: label
|
|
544
|
+
{ text: fitDisplay(label, LABEL_W), fg: C.label, bg: C.bg },
|
|
368
545
|
{ text: ': ', fg: C.dim, bg: C.bg },
|
|
369
546
|
{ text: value, fg: valueFg, bg: C.bg, bold: true },
|
|
370
547
|
]);
|
|
@@ -372,23 +549,32 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
372
549
|
|
|
373
550
|
private renderLabeledLine(width: number, label: string, value: string, valueFg: string): Line {
|
|
374
551
|
return buildStyledPanelLine(width, [
|
|
375
|
-
...(label.length > 0 ? [{ text: `${label
|
|
552
|
+
...(label.length > 0 ? [{ text: `${fitDisplay(label, 10)} `, fg: C.label }] : []),
|
|
376
553
|
{ text: value, fg: valueFg },
|
|
377
554
|
]);
|
|
378
555
|
}
|
|
379
556
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
557
|
+
// Non-masked numeric entry for the 'b' budget-alert key — the value is a
|
|
558
|
+
// USD amount, not a secret, so (unlike LocalAuthPanel's masked entry) it is
|
|
559
|
+
// echoed directly rather than dotted out.
|
|
560
|
+
private renderBudgetEntryPrompt(width: number, height: number): Line[] {
|
|
561
|
+
const draft = this.budgetEntry ?? '';
|
|
562
|
+
const display = `$${draft}█`;
|
|
563
|
+
const promptLines: Line[] = [
|
|
564
|
+
buildPanelLine(width, [[' Set the session budget alert (USD). 0 disables it.', C.label]]),
|
|
565
|
+
buildPanelLine(width, [['', C.label]]),
|
|
566
|
+
buildPanelLine(width, [[' Budget ', C.label], [display, C.cost]]),
|
|
567
|
+
buildPanelLine(width, [['', C.label]]),
|
|
568
|
+
buildPanelLine(width, [[' [Enter] Confirm [Esc] Cancel [Backspace] Delete char', C.dim]]),
|
|
569
|
+
];
|
|
570
|
+
const workspace = buildPanelWorkspace(width, height, {
|
|
571
|
+
title: ' Cost Tracker — Budget',
|
|
572
|
+
intro: 'Type a USD amount and press Enter to set the budget alert threshold.',
|
|
573
|
+
sections: [{ lines: promptLines }],
|
|
574
|
+
palette: DEFAULT_PANEL_PALETTE,
|
|
575
|
+
});
|
|
576
|
+
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
577
|
+
return workspace.slice(0, height);
|
|
392
578
|
}
|
|
393
579
|
|
|
394
580
|
}
|