@pellux/goodvibes-tui 0.28.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 +101 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +5 -1
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- 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 +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +203 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +309 -128
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- 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 +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- 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 +21 -7
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +5 -4
- 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/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +41 -30
- 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/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- 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 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from './polish.ts';
|
|
25
25
|
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
26
26
|
import { abbreviateCount } from '../utils/format-number.ts';
|
|
27
|
+
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
27
28
|
|
|
28
29
|
// Pricing lookups are provided by ../export/cost-utils.ts (single source of truth).
|
|
29
30
|
|
|
@@ -103,15 +104,27 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
103
104
|
// Per-agent tracking (keyed by agent id)
|
|
104
105
|
private agents = new Map<string, AgentEntry>();
|
|
105
106
|
|
|
106
|
-
// 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.
|
|
107
110
|
private budgetThreshold: number;
|
|
108
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
|
+
|
|
109
117
|
// Scroll offset for agent list
|
|
110
118
|
private scrollOffset = 0;
|
|
111
119
|
|
|
112
120
|
// Unsubscribe functions
|
|
113
121
|
private unsubs: Array<() => void> = [];
|
|
114
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
|
+
|
|
115
128
|
// Getter for live orchestrator usage
|
|
116
129
|
private readonly getOrchestratorUsage: () => UsageSnapshot & { model?: string };
|
|
117
130
|
|
|
@@ -125,7 +138,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
125
138
|
getOrchestratorUsage: () => UsageSnapshot & { model?: string },
|
|
126
139
|
opts: { budgetThreshold?: number; getAgentStatus?: (agentId: string) => AgentRecord | null } = {},
|
|
127
140
|
) {
|
|
128
|
-
super('cost', 'Cost', '$', '
|
|
141
|
+
super('cost', 'Cost', '$', 'providers');
|
|
129
142
|
this.getOrchestratorUsage = getOrchestratorUsage;
|
|
130
143
|
this.getAgentStatus = opts.getAgentStatus;
|
|
131
144
|
this.budgetThreshold = opts.budgetThreshold ?? 0;
|
|
@@ -139,7 +152,14 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
139
152
|
private attachEvents(turnEvents: UiEventFeed<TurnEvent>, agentEvents: UiEventFeed<AgentEvent>): void {
|
|
140
153
|
// Refresh after every completed turn
|
|
141
154
|
this.unsubs.push(
|
|
142
|
-
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()),
|
|
143
163
|
);
|
|
144
164
|
|
|
145
165
|
// Track agent spawns
|
|
@@ -190,7 +210,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
190
210
|
);
|
|
191
211
|
}
|
|
192
212
|
|
|
193
|
-
private
|
|
213
|
+
private refreshSessionCost(): void {
|
|
194
214
|
const usage = this.getOrchestratorUsage();
|
|
195
215
|
this.sessionUsage = {
|
|
196
216
|
input: usage.input,
|
|
@@ -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,6 +389,7 @@ 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
394
|
const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
|
|
265
395
|
const sessionCost = calcSessionCost(this.sessionUsage.input, this.sessionUsage.output, this.sessionUsage.cacheRead, this.sessionUsage.cacheWrite, this.sessionModel);
|
|
@@ -312,9 +442,10 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
312
442
|
? buildKeyboardHints(width, [
|
|
313
443
|
{ keys: 'Up/Down', label: 'scroll agents' },
|
|
314
444
|
{ keys: 'PgUp/PgDn', label: 'page' },
|
|
445
|
+
{ keys: 'b', label: 'set budget' },
|
|
315
446
|
], DEFAULT_PANEL_PALETTE)
|
|
316
447
|
: buildKeyboardHints(width, [
|
|
317
|
-
{ keys: '
|
|
448
|
+
{ keys: 'b', label: 'set budget alert' },
|
|
318
449
|
], DEFAULT_PANEL_PALETTE);
|
|
319
450
|
if (agentList.length > 0) {
|
|
320
451
|
const planCost = agentList.reduce((sum, a) => sum + a.cost, 0);
|
|
@@ -382,8 +513,12 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
382
513
|
width,
|
|
383
514
|
' No agents spawned this session',
|
|
384
515
|
'Agent-level cost estimates appear here once delegated or background agents start running.',
|
|
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.
|
|
385
520
|
[
|
|
386
|
-
{ command: '
|
|
521
|
+
{ command: 'b', summary: 'set a session budget alert to track spend against a cap' },
|
|
387
522
|
],
|
|
388
523
|
DEFAULT_PANEL_PALETTE,
|
|
389
524
|
),
|
|
@@ -419,4 +554,27 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
419
554
|
]);
|
|
420
555
|
}
|
|
421
556
|
|
|
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);
|
|
578
|
+
}
|
|
579
|
+
|
|
422
580
|
}
|