@pellux/goodvibes-tui 0.28.0 → 1.0.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 (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,22 +1,28 @@
1
- import { BasePanel } from './base-panel.ts';
2
- import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
1
+ import { ScrollableListPanel } from './scrollable-list-panel.ts';
2
+ import { createEmptyLine, type Line } from '../types/grid.ts';
3
3
  import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
4
4
  import type { TurnEvent } from '@/runtime/index.ts';
5
5
  import type { UiEventFeed } from '../runtime/ui-events.ts';
6
6
  import type { Orchestrator } from '../core/orchestrator';
7
7
  import {
8
+ buildAlignedRow,
9
+ buildBodyText,
8
10
  buildEmptyState,
11
+ buildKeyboardHints,
12
+ buildKeyValueLine,
9
13
  buildPanelLine,
10
- buildStyledPanelLine,
11
14
  buildPanelWorkspace,
12
- resolveScrollablePanelSection,
13
- resolveStackedScrollableSections,
15
+ buildStyledPanelLine,
16
+ resolvePrimaryScrollableSection,
14
17
  DEFAULT_PANEL_PALETTE,
15
18
  extendPalette,
19
+ type ColumnSpec,
16
20
  type PanelWorkspaceSection,
17
21
  } from './polish.ts';
22
+ import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
18
23
  import { abbreviateCount } from '../utils/format-number.ts';
19
24
  import { formatLatencyMs } from '../utils/format-duration.ts';
25
+ import { calcSessionCost } from '../export/cost-utils.ts';
20
26
 
21
27
  // ---------------------------------------------------------------------------
22
28
  // Types
@@ -37,7 +43,12 @@ export interface ApiCallEntry {
37
43
  outputTokens: number;
38
44
  /** End-to-end latency in ms (stream-start → llm-response, or turn-start → llm-response). */
39
45
  latencyMs: number;
40
- /** HTTP-level status code hint; 0 when unknown. */
46
+ /**
47
+ * HTTP-level status code hint. Only ever set from a real signal (scraped
48
+ * from an error message); 0 means no real code is known. Never fabricated
49
+ * (WO-137 dropped the old always-200-on-success convention — this layer
50
+ * doesn't actually observe HTTP status codes on success).
51
+ */
41
52
  statusCode: number;
42
53
  /** 'ok' | 'error' */
43
54
  status: ApiCallStatus;
@@ -51,6 +62,7 @@ export interface ApiCallEntry {
51
62
 
52
63
  const MAX_CALL_LOG = 50;
53
64
  const MAX_ERROR_LOG = 20;
65
+ const AGES_REFRESH_MS = 1_000;
54
66
 
55
67
  // ---------------------------------------------------------------------------
56
68
  // Colors
@@ -79,12 +91,21 @@ function fmtMs(ms: number): string {
79
91
  }
80
92
 
81
93
  function fmtAgo(ts: number): string {
82
- const sec = Math.floor((Date.now() - ts) / 1000);
94
+ const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
83
95
  if (sec < 60) return `${sec}s ago`;
84
96
  if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
85
97
  return `${Math.floor(sec / 3600)}h ago`;
86
98
  }
87
99
 
100
+ function fmtUsd(value: number): string {
101
+ if (value <= 0) return '$0';
102
+ return value >= 1 ? `$${value.toFixed(2)}` : `$${value.toFixed(4)}`;
103
+ }
104
+
105
+ function callCost(entry: ApiCallEntry): number {
106
+ return calcSessionCost(entry.inputTokens, entry.outputTokens, 0, 0, entry.model);
107
+ }
108
+
88
109
  function latColor(ms: number): string {
89
110
  if (ms >= LATENCY_BAD_MS) return C.bad;
90
111
  if (ms >= LATENCY_WARN_MS) return C.warn;
@@ -96,6 +117,17 @@ function statusCodeFromError(msg: string): number {
96
117
  return m ? parseInt(m[1]!, 10) : 0;
97
118
  }
98
119
 
120
+ const CALL_COLUMNS: ColumnSpec[] = [
121
+ { width: 8 }, // time
122
+ { width: 1 }, // status glyph
123
+ { width: 12 }, // provider
124
+ { width: 26 }, // model (+ scraped status code suffix)
125
+ { width: 8, align: 'right' }, // in
126
+ { width: 8, align: 'right' }, // out
127
+ { width: 8, align: 'right' }, // latency
128
+ { width: 9, align: 'right' }, // cost
129
+ ];
130
+
99
131
  // ---------------------------------------------------------------------------
100
132
  // DebugPanel
101
133
  // ---------------------------------------------------------------------------
@@ -103,12 +135,16 @@ function statusCodeFromError(msg: string): number {
103
135
  /**
104
136
  * Real-time API debug panel.
105
137
  *
106
- * Shows per-call log (model, provider, input/output tokens, latency, status code),
107
- * running session call total, and error history.
138
+ * Migrated onto ScrollableListPanel<ApiCallEntry> (WO-137) so nav
139
+ * (up/down/j/k/pageup/pagedown/g/G), the '/' inline filter, and a
140
+ * selected-row detail section come from the shared base class instead of a
141
+ * bespoke render (SystemMessagesPanel precedent).
108
142
  *
109
- * Subscribes to typed turn runtime events.
143
+ * Shows per-call log (model, provider, input/output tokens, latency, status,
144
+ * cost), running session call total, and error history. Subscribes to typed
145
+ * turn runtime events.
110
146
  */
111
- export class DebugPanel extends BasePanel {
147
+ export class DebugPanel extends ScrollableListPanel<ApiCallEntry> {
112
148
  private _unsubs: Array<() => void> = [];
113
149
 
114
150
  // Timing state
@@ -120,17 +156,32 @@ export class DebugPanel extends BasePanel {
120
156
  private _prevInput = 0;
121
157
  private _prevOutput = 0;
122
158
 
159
+ // Real provider/model attribution for TURN_ERROR rows: the provider/model
160
+ // named on the in-flight LLM request is the authoritative attribution
161
+ // target when the turn subsequently errors (provider-health-panel.ts
162
+ // precedent) — never a fabricated 'unknown'.
163
+ private _inflightProvider: string | null = null;
164
+ private _inflightModel: string | null = null;
165
+
123
166
  // Session data
124
167
  private _calls: ApiCallEntry[] = [];
125
168
  private _errors: ApiCallEntry[] = [];
126
169
  private _totalCalls = 0;
127
170
  private _totalErrors = 0;
128
171
 
172
+ // c=clear confirmation
173
+ private confirm: ConfirmState<'clear'> | null = null;
174
+
175
+ // Re-render timer so "Xs ago" ages stay live even without new events.
176
+ private agesTimerId: ReturnType<typeof setInterval> | null = null;
177
+
129
178
  constructor(
130
179
  private readonly turnEvents: UiEventFeed<TurnEvent>,
131
180
  private readonly requestRender: () => void = () => {},
132
181
  ) {
133
- super('debug', 'Debug', 'B', 'monitoring');
182
+ super('debug', 'Debug', '', 'incidents-diagnostics');
183
+ this.filterEnabled = true;
184
+ this.filterLabel = 'Filter calls';
134
185
  this._subscribe();
135
186
  }
136
187
 
@@ -164,6 +215,13 @@ export class DebugPanel extends BasePanel {
164
215
  }),
165
216
  );
166
217
 
218
+ this._unsubs.push(
219
+ this.turnEvents.on('LLM_REQUEST_STARTED', (env) => {
220
+ this._inflightProvider = env.provider;
221
+ this._inflightModel = env.model;
222
+ }),
223
+ );
224
+
167
225
  this._unsubs.push(
168
226
  this.turnEvents.on('LLM_RESPONSE_RECEIVED', (env) => {
169
227
  const now = Date.now();
@@ -174,6 +232,8 @@ export class DebugPanel extends BasePanel {
174
232
  ? now - this._turnStartMs
175
233
  : 0;
176
234
  this._streamStartMs = null;
235
+ this._inflightProvider = null;
236
+ this._inflightModel = null;
177
237
 
178
238
  // Compute per-call token delta if orchestrator is wired
179
239
  let inputTokens = env.inputTokens + (env.cacheReadTokens ?? 0) + (env.cacheWriteTokens ?? 0);
@@ -193,7 +253,9 @@ export class DebugPanel extends BasePanel {
193
253
  inputTokens,
194
254
  outputTokens,
195
255
  latencyMs,
196
- statusCode: 200,
256
+ // WO-137: no fabricated 200 — this layer never observes a real
257
+ // HTTP status code on success, so 0 means "no code, just OK".
258
+ statusCode: 0,
197
259
  status: 'ok',
198
260
  };
199
261
  this._pushCall(entry);
@@ -212,8 +274,8 @@ export class DebugPanel extends BasePanel {
212
274
 
213
275
  const entry: ApiCallEntry = {
214
276
  ts: Date.now(),
215
- provider: 'unknown',
216
- model: 'unknown',
277
+ provider: this._inflightProvider ?? 'n/a',
278
+ model: this._inflightModel ?? 'n/a',
217
279
  inputTokens: 0,
218
280
  outputTokens: 0,
219
281
  latencyMs: 0,
@@ -221,18 +283,42 @@ export class DebugPanel extends BasePanel {
221
283
  status: 'error',
222
284
  errorMessage: msg.slice(0, 120),
223
285
  };
286
+ this._inflightProvider = null;
287
+ this._inflightModel = null;
224
288
  this._pushCall(entry);
225
289
  this._pushError(entry);
226
290
  this.markDirty();
227
291
  this.requestRender();
228
292
  }),
229
293
  );
294
+
295
+ this._unsubs.push(
296
+ this.turnEvents.on('TURN_COMPLETED', () => {
297
+ this._inflightProvider = null;
298
+ this._inflightModel = null;
299
+ }),
300
+ );
301
+
302
+ this._unsubs.push(
303
+ this.turnEvents.on('TURN_CANCEL', () => {
304
+ this._inflightProvider = null;
305
+ this._inflightModel = null;
306
+ }),
307
+ );
230
308
  }
231
309
 
232
310
  private _pushCall(entry: ApiCallEntry): void {
233
311
  this._totalCalls++;
312
+ // Follow-mode: only auto-jump to the new row when the selection was
313
+ // already at (or past) the previous tail — mirrors the SystemMessagesPanel
314
+ // "don't yank the cursor while the user is reviewing history" contract.
315
+ const wasAtTail = this._calls.length === 0 || this.selectedIndex >= this._calls.length - 1;
234
316
  this._calls.push(entry);
235
- if (this._calls.length > MAX_CALL_LOG) this._calls.shift();
317
+ if (this._calls.length > MAX_CALL_LOG) {
318
+ this._calls.shift();
319
+ if (this.selectedIndex > 0) this.selectedIndex--;
320
+ }
321
+ if (wasAtTail) this.selectedIndex = this._calls.length - 1;
236
322
  }
237
323
 
238
324
  private _pushError(entry: ApiCallEntry): void {
@@ -245,91 +331,232 @@ export class DebugPanel extends BasePanel {
245
331
  // Lifecycle
246
332
  // -------------------------------------------------------------------------
247
333
 
334
+ override onActivate(): void {
335
+ super.onActivate();
336
+ this._startAgesTimer();
337
+ }
338
+
339
+ override onDeactivate(): void {
340
+ this._stopAgesTimer();
341
+ }
342
+
248
343
  override onDestroy(): void {
344
+ this._stopAgesTimer();
249
345
  for (const unsub of this._unsubs) unsub();
250
346
  this._unsubs = [];
251
347
  }
252
348
 
349
+ private _startAgesTimer(): void {
350
+ if (this.agesTimerId) return;
351
+ this.agesTimerId = this.registerTimer(setInterval(() => {
352
+ if (this._calls.length === 0) return;
353
+ this.markDirty();
354
+ this.requestRender();
355
+ }, AGES_REFRESH_MS));
356
+ }
357
+
358
+ private _stopAgesTimer(): void {
359
+ if (this.agesTimerId) {
360
+ this.clearTimer(this.agesTimerId);
361
+ this.agesTimerId = null;
362
+ }
363
+ }
364
+
365
+ // -------------------------------------------------------------------------
366
+ // ScrollableListPanel contract
367
+ // -------------------------------------------------------------------------
368
+
369
+ protected getItems(): readonly ApiCallEntry[] {
370
+ return this._calls;
371
+ }
372
+
373
+ protected override getPalette() { return C; }
374
+ protected override getEmptyStateMessage() { return ' No calls yet'; }
375
+
376
+ protected override filterMatches(entry: ApiCallEntry, q: string): boolean {
377
+ return entry.provider.toLowerCase().includes(q)
378
+ || entry.model.toLowerCase().includes(q)
379
+ || entry.status.includes(q)
380
+ || (entry.errorMessage?.toLowerCase().includes(q) ?? false);
381
+ }
382
+
383
+ protected renderItem(entry: ApiCallEntry, _index: number, selected: boolean, width: number): Line {
384
+ const statusChar = entry.status === 'ok' ? '✓' : '✕';
385
+ const statusFg = entry.status === 'ok' ? C.good : C.bad;
386
+ const codeSuffix = entry.status === 'error' && entry.statusCode > 0 ? ` [${entry.statusCode}]` : '';
387
+ return buildAlignedRow(
388
+ width,
389
+ [
390
+ { text: fmtAgo(entry.ts), fg: C.dim },
391
+ { text: statusChar, fg: statusFg },
392
+ { text: entry.provider, fg: C.provName },
393
+ { text: entry.model + codeSuffix, fg: C.value },
394
+ { text: fmtTok(entry.inputTokens), fg: C.input },
395
+ { text: fmtTok(entry.outputTokens), fg: C.output },
396
+ { text: fmtMs(entry.latencyMs), fg: latColor(entry.latencyMs) },
397
+ { text: fmtUsd(callCost(entry)), fg: C.value },
398
+ ],
399
+ CALL_COLUMNS,
400
+ { selected, selectedBg: C.selectBg },
401
+ );
402
+ }
403
+
404
+ // -------------------------------------------------------------------------
405
+ // Input
406
+ // -------------------------------------------------------------------------
407
+
408
+ handleInput(key: string): boolean {
409
+ const confirmResult = handleConfirmInput(this.confirm, key);
410
+ if (confirmResult === 'confirmed') {
411
+ this.confirm = null;
412
+ this._calls = [];
413
+ this._errors = [];
414
+ this._totalCalls = 0;
415
+ this._totalErrors = 0;
416
+ this.selectedIndex = 0;
417
+ this.markDirty();
418
+ return true;
419
+ }
420
+ if (confirmResult === 'cancelled') {
421
+ this.confirm = null;
422
+ this.markDirty();
423
+ return true;
424
+ }
425
+ if (confirmResult === 'absorbed') return true;
426
+
427
+ if (!this.filterActive && key === 'c') {
428
+ if (this._calls.length === 0) return false;
429
+ this.confirm = { subject: 'clear', label: `${this._calls.length} API call(s)`, verb: 'Clear' };
430
+ this.markDirty();
431
+ return true;
432
+ }
433
+
434
+ return super.handleInput(key);
435
+ }
436
+
253
437
  // -------------------------------------------------------------------------
254
438
  // Rendering
255
439
  // -------------------------------------------------------------------------
256
440
 
257
441
  override render(width: number, height: number): Line[] {
258
- const sections: PanelWorkspaceSection[] = [
259
- {
260
- title: 'Session',
261
- lines: this._renderSummary(width),
262
- },
263
- ];
442
+ this.clampSelection();
443
+
444
+ if (this.confirm) {
445
+ const lines = buildPanelWorkspace(width, height, {
446
+ title: ' API Debug',
447
+ sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
448
+ palette: C,
449
+ });
450
+ while (lines.length < height) lines.push(createEmptyLine(width));
451
+ return lines.slice(0, height);
452
+ }
453
+
454
+ const intro = 'Recent provider calls, token deltas, latency, status codes, and cost, with error history.';
264
455
 
265
456
  if (this._calls.length === 0) {
266
- sections.push({
267
- title: 'API Call Log',
268
- lines: buildEmptyState(
269
- width,
270
- ' No calls yet',
271
- 'Completed provider calls and API failures will appear here with timing, token counts, and status codes.',
272
- [],
273
- DEFAULT_PANEL_PALETTE,
274
- ),
457
+ const lines = buildPanelWorkspace(width, height, {
458
+ title: ' API Debug',
459
+ intro,
460
+ sections: [{
461
+ title: 'Session',
462
+ lines: buildEmptyState(
463
+ width,
464
+ this.getEmptyStateMessage(),
465
+ 'Completed provider calls and API failures will appear here with timing, token counts, status, and cost.',
466
+ [],
467
+ C,
468
+ ),
469
+ }],
470
+ palette: C,
275
471
  });
276
- } else {
277
- const rows = this._renderCallLog(width);
278
- if (this._errors.length > 0) {
279
- const errors = this._renderErrorHistory(width);
280
- const [callSection, errorSection] = resolveStackedScrollableSections(width, height, {
281
- palette: DEFAULT_PANEL_PALETTE,
282
- beforeSections: sections,
283
- sections: [
284
- {
285
- title: 'API Call Log',
286
- scrollableLines: rows,
287
- scrollOffset: Math.max(0, rows.length - 1),
288
- minRows: 8,
289
- weight: 2,
290
- },
291
- {
292
- title: 'Error History',
293
- scrollableLines: errors,
294
- scrollOffset: Math.max(0, errors.length - 1),
295
- minRows: 4,
296
- weight: 1,
297
- },
298
- ],
299
- });
300
- sections.push(callSection!.section, errorSection!.section);
301
- } else {
302
- const callSection = resolveScrollablePanelSection(width, height, {
303
- palette: DEFAULT_PANEL_PALETTE,
304
- beforeSections: sections,
305
- section: {
306
- title: 'API Call Log',
307
- scrollableLines: rows,
308
- scrollOffset: Math.max(0, rows.length - 1),
309
- minRows: 8,
310
- },
311
- });
312
- sections.push(callSection.section);
313
- }
472
+ while (lines.length < height) lines.push(createEmptyLine(width));
473
+ return lines.slice(0, height);
314
474
  }
315
475
 
476
+ const summarySection: PanelWorkspaceSection = { title: 'Session', lines: this._renderSummary(width) };
477
+ const filterSection: PanelWorkspaceSection = { lines: [this.buildFilterLine(width)] };
478
+
479
+ const visible = this.getVisibleItems();
480
+ this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, Math.max(0, visible.length - 1)));
481
+ const selected = this.getSelectedItem();
482
+
483
+ const rows: Line[] = visible.length > 0
484
+ ? visible.map((entry, index) => this.renderItem(entry, index, index === this.selectedIndex, width))
485
+ : [buildPanelLine(width, [[` No calls match "${this.filterQuery.trim()}" (Esc to clear)`, C.dim]])];
486
+
487
+ const detailSection: PanelWorkspaceSection = selected
488
+ ? {
489
+ title: 'Selected Call',
490
+ lines: [
491
+ buildKeyValueLine(width, [
492
+ { label: 'provider', value: selected.provider, valueColor: C.provName },
493
+ { label: 'model', value: selected.model, valueColor: C.value },
494
+ { label: 'status', value: selected.status === 'ok' ? 'OK' : 'ERROR', valueColor: selected.status === 'ok' ? C.good : C.bad },
495
+ { label: 'when', value: fmtAgo(selected.ts), valueColor: C.dim },
496
+ ], C),
497
+ buildKeyValueLine(width, [
498
+ { label: 'in', value: String(selected.inputTokens), valueColor: C.input },
499
+ { label: 'out', value: String(selected.outputTokens), valueColor: C.output },
500
+ { label: 'latency', value: fmtMs(selected.latencyMs), valueColor: latColor(selected.latencyMs) },
501
+ { label: 'cost', value: fmtUsd(callCost(selected)), valueColor: C.value },
502
+ ], C),
503
+ ...(selected.errorMessage
504
+ ? buildBodyText(width, selected.errorMessage, C, C.bad)
505
+ : []),
506
+ ],
507
+ }
508
+ : { title: 'Selected Call', lines: [] };
509
+
316
510
  const latestError = this._errors[this._errors.length - 1];
317
- const footerLines = latestError
318
- ? [buildStyledPanelLine(width, [
319
- { text: ' ✕ latest error ', fg: C.bad },
320
- { text: truncateDisplay(latestError.errorMessage ?? 'unknown error', Math.max(0, width - 18)), fg: C.dim },
321
- ])]
322
- : [buildStyledPanelLine(width, [
323
- { text: ' Live feed — newest calls at the bottom; auto-follows.', fg: C.dim },
324
- ])];
325
-
326
- return buildPanelWorkspace(width, height, {
511
+ const hints = this.filterActive
512
+ ? [
513
+ { keys: 'type', label: 'filter calls' },
514
+ { keys: 'Enter', label: 'apply' },
515
+ { keys: 'Esc', label: 'clear' },
516
+ ]
517
+ : [
518
+ { keys: '↑/↓', label: 'select call' },
519
+ { keys: '/', label: 'filter' },
520
+ { keys: 'c', label: 'clear log' },
521
+ ];
522
+
523
+ const footerLines: Line[] = [
524
+ ...(latestError
525
+ ? [buildStyledPanelLine(width, [
526
+ { text: ' ✕ latest error ', fg: C.bad },
527
+ { text: truncateDisplay(latestError.errorMessage ?? 'unknown error', Math.max(0, width - 18)), fg: C.dim },
528
+ ])]
529
+ : []),
530
+ buildKeyboardHints(width, hints, C),
531
+ ];
532
+
533
+ const callSection = resolvePrimaryScrollableSection(width, height, {
534
+ intro,
535
+ footerLines,
536
+ palette: C,
537
+ beforeSections: [summarySection, filterSection],
538
+ section: {
539
+ title: `API Call Log (${visible.length} of ${this._calls.length})`,
540
+ scrollableLines: rows,
541
+ selectedIndex: this.selectedIndex,
542
+ scrollOffset: this.scrollStart,
543
+ minRows: 6,
544
+ appendWindowSummary: { dimColor: C.dim },
545
+ },
546
+ afterSections: [detailSection],
547
+ });
548
+ this.scrollStart = callSection.scrollOffset;
549
+
550
+ this.needsRender = false;
551
+ const lines = buildPanelWorkspace(width, height, {
327
552
  title: ' API Debug',
328
- intro: 'Recent provider calls, token deltas, latency, status codes, and error history.',
329
- sections,
553
+ intro,
554
+ sections: [summarySection, filterSection, callSection.section, detailSection],
330
555
  footerLines,
331
- palette: DEFAULT_PANEL_PALETTE,
556
+ palette: C,
332
557
  });
558
+ while (lines.length < height) lines.push(createEmptyLine(width));
559
+ return lines.slice(0, height);
333
560
  }
334
561
 
335
562
  // -------------------------------------------------------------------------
@@ -345,6 +572,7 @@ export class DebugPanel extends BasePanel {
345
572
  ? Math.round(recent.reduce((s, c) => s + c.latencyMs, 0) / recent.length)
346
573
  : 0;
347
574
  const sessionTokens = this._calls.reduce((s, c) => s + c.inputTokens + c.outputTokens, 0);
575
+ const sessionCost = this._calls.reduce((s, c) => s + callCost(c), 0);
348
576
 
349
577
  const lines: Line[] = [
350
578
  buildStyledPanelLine(width, [
@@ -358,6 +586,8 @@ export class DebugPanel extends BasePanel {
358
586
  { text: fmtMs(avgLat), fg: avgLat > 0 ? latColor(avgLat) : C.dim },
359
587
  { text: ' Tokens ', fg: C.label },
360
588
  { text: fmtTok(sessionTokens), fg: C.value },
589
+ { text: ' Cost ', fg: C.label },
590
+ { text: fmtUsd(sessionCost), fg: C.value },
361
591
  ]),
362
592
  ];
363
593
  // Live status: most recent call (latency / age) or wiring hint.
@@ -376,81 +606,4 @@ export class DebugPanel extends BasePanel {
376
606
  }
377
607
  return lines;
378
608
  }
379
-
380
- private _renderCallLog(width: number): Line[] {
381
- const lines: Line[] = [];
382
- lines.push(this._callLogHeader(width));
383
- for (const entry of this._calls) {
384
- lines.push(this._callLogRow(entry, width));
385
- }
386
-
387
- return lines;
388
- }
389
-
390
- private _callLogHeader(width: number): Line {
391
- // Layout: time(8) status(2) provider(12) model(20) in(8) out(8) lat(8)
392
- const header = ' Time S Provider Model In Out Lat';
393
- return this._textLine(truncateDisplay(header, width), C.colHdr, width, { dim: true });
394
- }
395
-
396
- private _callLogRow(e: ApiCallEntry, width: number): Line {
397
- const timeStr = fmtAgo(e.ts).padEnd(8);
398
- const statusChar = e.status === 'ok' ? '✓' : '✕';
399
- const statusFg = e.status === 'ok' ? C.good : C.bad;
400
- const provStr = fitDisplay(e.provider, 12);
401
- const modelStr = fitDisplay(e.model, 20);
402
- const inStr = fmtTok(e.inputTokens).padStart(8);
403
- const outStr = fmtTok(e.outputTokens).padStart(8);
404
- const latStr = fmtMs(e.latencyMs).padStart(8);
405
-
406
- const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
407
- { text: ' ' + timeStr, fg: C.dim },
408
- { text: statusChar + ' ', fg: statusFg },
409
- { text: provStr, fg: C.provName },
410
- { text: modelStr, fg: C.value },
411
- { text: inStr, fg: C.input },
412
- { text: outStr, fg: C.output },
413
- { text: latStr, fg: latColor(e.latencyMs) },
414
- ];
415
-
416
- // Append status code for errors
417
- if (e.status === 'error' && e.statusCode > 0) {
418
- segments.push({ text: ` [${e.statusCode}]`, fg: C.bad });
419
- }
420
-
421
- return buildStyledPanelLine(
422
- width,
423
- segments.map((seg) => ({ text: seg.text, fg: seg.fg, bold: seg.bold })),
424
- );
425
- }
426
-
427
- private _renderErrorHistory(width: number): Line[] {
428
- const lines: Line[] = [];
429
- for (const e of this._errors) {
430
- lines.push(this._errorRow(e, width));
431
- }
432
-
433
- return lines;
434
- }
435
-
436
- private _errorRow(e: ApiCallEntry, width: number): Line {
437
- const timeStr = fmtAgo(e.ts).padEnd(8);
438
- const codeStr = e.statusCode > 0 ? `[${e.statusCode}] ` : '';
439
- const msgStr = truncateDisplay(e.errorMessage ?? 'unknown error', Math.max(0, width - 12 - codeStr.length));
440
- const full = ` ${timeStr} ${codeStr}${msgStr}`;
441
- return this._textLine(truncateDisplay(full, width), C.bad, width);
442
- }
443
-
444
- // -------------------------------------------------------------------------
445
- // Line-builder helpers
446
- // -------------------------------------------------------------------------
447
-
448
- private _textLine(
449
- text: string,
450
- fg: string,
451
- width: number,
452
- opts: { dim?: boolean } = {},
453
- ): Line {
454
- return buildStyledPanelLine(width, [{ text, fg, dim: opts.dim }]);
455
- }
456
609
  }