@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.
Files changed (118) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/input/commands/cost-runtime.ts +49 -0
  5. package/src/input/commands/operator-runtime.ts +5 -1
  6. package/src/input/commands.ts +2 -0
  7. package/src/input/handler-feed-routes.ts +12 -35
  8. package/src/input/handler-feed.ts +4 -8
  9. package/src/input/handler-shortcuts.ts +51 -0
  10. package/src/input/handler.ts +43 -6
  11. package/src/input/keybindings.ts +48 -8
  12. package/src/input/panel-integration-actions.ts +107 -1
  13. package/src/main.ts +5 -1
  14. package/src/panels/agent-inspector-panel.ts +125 -36
  15. package/src/panels/agent-inspector-shared.ts +144 -0
  16. package/src/panels/approval-panel.ts +67 -16
  17. package/src/panels/automation-control-panel.ts +368 -124
  18. package/src/panels/builtin/agent.ts +28 -37
  19. package/src/panels/builtin/development.ts +40 -32
  20. package/src/panels/builtin/knowledge.ts +15 -6
  21. package/src/panels/builtin/operations.ts +178 -117
  22. package/src/panels/builtin/session.ts +35 -9
  23. package/src/panels/builtin/shared.ts +98 -6
  24. package/src/panels/cockpit-panel.ts +232 -73
  25. package/src/panels/communication-panel.ts +58 -20
  26. package/src/panels/confirm-state.ts +8 -1
  27. package/src/panels/control-plane-panel.ts +351 -118
  28. package/src/panels/cost-tracker-panel.ts +165 -7
  29. package/src/panels/debug-panel.ts +312 -159
  30. package/src/panels/diff-panel.ts +203 -57
  31. package/src/panels/docs-panel.ts +152 -66
  32. package/src/panels/eval-panel.ts +220 -42
  33. package/src/panels/file-explorer-panel.ts +202 -122
  34. package/src/panels/file-preview-panel.ts +132 -57
  35. package/src/panels/git-panel.ts +309 -128
  36. package/src/panels/hooks-panel.ts +150 -22
  37. package/src/panels/incident-review-panel.ts +223 -35
  38. package/src/panels/index.ts +0 -4
  39. package/src/panels/intelligence-panel.ts +212 -86
  40. package/src/panels/knowledge-graph-panel.ts +461 -101
  41. package/src/panels/local-auth-panel.ts +240 -28
  42. package/src/panels/marketplace-panel.ts +193 -26
  43. package/src/panels/memory-panel.ts +78 -77
  44. package/src/panels/ops-control-panel.ts +146 -29
  45. package/src/panels/ops-strategy-panel.ts +72 -4
  46. package/src/panels/orchestration-panel.ts +231 -69
  47. package/src/panels/panel-list-panel.ts +154 -131
  48. package/src/panels/panel-manager.ts +97 -9
  49. package/src/panels/plan-dashboard-panel.ts +333 -17
  50. package/src/panels/plugins-panel.ts +184 -29
  51. package/src/panels/policy-panel.ts +210 -38
  52. package/src/panels/polish-core.ts +7 -2
  53. package/src/panels/polish.ts +23 -4
  54. package/src/panels/project-planning-answer-actions.ts +134 -0
  55. package/src/panels/project-planning-panel.ts +62 -113
  56. package/src/panels/provider-health-panel.ts +434 -518
  57. package/src/panels/provider-health-routes.ts +203 -0
  58. package/src/panels/provider-health-tracker.ts +194 -6
  59. package/src/panels/provider-health-views.ts +560 -0
  60. package/src/panels/qr-panel.ts +116 -30
  61. package/src/panels/remote-panel.ts +114 -36
  62. package/src/panels/routes-panel.ts +63 -22
  63. package/src/panels/sandbox-panel.ts +174 -42
  64. package/src/panels/scrollable-list-panel.ts +19 -135
  65. package/src/panels/security-panel.ts +133 -33
  66. package/src/panels/services-panel.ts +116 -64
  67. package/src/panels/session-browser-panel.ts +73 -2
  68. package/src/panels/session-maintenance.ts +4 -122
  69. package/src/panels/settings-sync-panel.ts +335 -72
  70. package/src/panels/skills-panel.ts +157 -89
  71. package/src/panels/subscription-panel.ts +86 -33
  72. package/src/panels/symbol-outline-panel.ts +248 -108
  73. package/src/panels/system-messages-panel.ts +114 -13
  74. package/src/panels/tasks-panel.ts +326 -139
  75. package/src/panels/thinking-panel.ts +43 -10
  76. package/src/panels/token-budget-panel.ts +194 -18
  77. package/src/panels/tool-inspector-panel.ts +144 -34
  78. package/src/panels/types.ts +40 -4
  79. package/src/panels/work-plan-panel.ts +280 -17
  80. package/src/panels/worktree-panel.ts +175 -42
  81. package/src/panels/wrfc-panel.ts +116 -24
  82. package/src/renderer/conversation-overlays.ts +25 -11
  83. package/src/renderer/footer-tips.ts +41 -0
  84. package/src/renderer/fullscreen-primitives.ts +22 -16
  85. package/src/renderer/help-overlay.ts +91 -14
  86. package/src/renderer/hint-grammar.ts +52 -0
  87. package/src/renderer/layout.ts +7 -7
  88. package/src/renderer/modal-factory.ts +23 -15
  89. package/src/renderer/model-picker-overlay.ts +21 -7
  90. package/src/renderer/overlay-box.ts +16 -10
  91. package/src/renderer/process-indicator.ts +3 -1
  92. package/src/renderer/progress.ts +5 -4
  93. package/src/renderer/search-overlay.ts +27 -6
  94. package/src/renderer/session-picker-modal.ts +6 -4
  95. package/src/renderer/settings-modal.ts +70 -10
  96. package/src/renderer/shell-surface.ts +41 -15
  97. package/src/renderer/status-glyphs.ts +11 -9
  98. package/src/renderer/theme.ts +60 -3
  99. package/src/renderer/ui-factory.ts +41 -30
  100. package/src/renderer/ui-primitives.ts +23 -2
  101. package/src/runtime/bootstrap-shell.ts +35 -18
  102. package/src/runtime/diagnostics/panels/index.ts +0 -3
  103. package/src/shell/ui-openers.ts +14 -22
  104. package/src/utils/format-duration.ts +2 -2
  105. package/src/utils/splash-lines.ts +44 -3
  106. package/src/version.ts +1 -1
  107. package/src/work-plans/work-plan-store.ts +13 -0
  108. package/src/panels/agent-logs-panel.ts +0 -635
  109. package/src/panels/agent-logs-shared.ts +0 -129
  110. package/src/panels/context-visualizer-panel.ts +0 -238
  111. package/src/panels/forensics-panel.ts +0 -378
  112. package/src/panels/provider-account-snapshot.ts +0 -259
  113. package/src/panels/provider-accounts-panel.ts +0 -255
  114. package/src/panels/provider-stats-panel.ts +0 -391
  115. package/src/panels/schedule-panel.ts +0 -365
  116. package/src/panels/watchers-panel.ts +0 -213
  117. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  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', '$', 'monitoring');
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.onTurnComplete()),
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 onTurnComplete(): void {
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: '/cost budget <usd>', label: 'set budget alert' },
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: '/cost budget <usd>', summary: 'set a session budget alert to track spend against a cap' },
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
  }