@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,19 +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
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
3
4
  import type { TurnEvent } from '@/runtime/index.ts';
4
5
  import type { UiEventFeed } from '../runtime/ui-events.ts';
5
6
  import type { Orchestrator } from '../core/orchestrator';
6
7
  import {
8
+ buildAlignedRow,
9
+ buildBodyText,
7
10
  buildEmptyState,
11
+ buildKeyboardHints,
12
+ buildKeyValueLine,
8
13
  buildPanelLine,
9
- buildStyledPanelLine,
10
14
  buildPanelWorkspace,
11
- resolveScrollablePanelSection,
12
- resolveStackedScrollableSections,
15
+ buildStyledPanelLine,
16
+ resolvePrimaryScrollableSection,
13
17
  DEFAULT_PANEL_PALETTE,
14
18
  extendPalette,
19
+ type ColumnSpec,
15
20
  type PanelWorkspaceSection,
16
21
  } from './polish.ts';
22
+ import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
23
+ import { abbreviateCount } from '../utils/format-number.ts';
24
+ import { formatLatencyMs } from '../utils/format-duration.ts';
25
+ import { calcSessionCost } from '../export/cost-utils.ts';
17
26
 
18
27
  // ---------------------------------------------------------------------------
19
28
  // Types
@@ -34,7 +43,12 @@ export interface ApiCallEntry {
34
43
  outputTokens: number;
35
44
  /** End-to-end latency in ms (stream-start → llm-response, or turn-start → llm-response). */
36
45
  latencyMs: number;
37
- /** 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
+ */
38
52
  statusCode: number;
39
53
  /** 'ok' | 'error' */
40
54
  status: ApiCallStatus;
@@ -48,6 +62,7 @@ export interface ApiCallEntry {
48
62
 
49
63
  const MAX_CALL_LOG = 50;
50
64
  const MAX_ERROR_LOG = 20;
65
+ const AGES_REFRESH_MS = 1_000;
51
66
 
52
67
  // ---------------------------------------------------------------------------
53
68
  // Colors
@@ -68,25 +83,29 @@ const LATENCY_BAD_MS = 5_000;
68
83
  // ---------------------------------------------------------------------------
69
84
 
70
85
  function fmtTok(n: number): string {
71
- if (n < 10_000) return String(n);
72
- if (n < 1_000_000) return (n / 1000).toFixed(1) + 'k';
73
- return (n / 1_000_000).toFixed(2) + 'M';
86
+ return abbreviateCount(n, { guard: 10_000, decimals: 1, mDecimals: 2 });
74
87
  }
75
88
 
76
89
  function fmtMs(ms: number): string {
77
- if (ms <= 0) return 'n/a';
78
- if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
79
- if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
80
- return `${Math.round(ms)}ms`;
90
+ return formatLatencyMs(ms);
81
91
  }
82
92
 
83
93
  function fmtAgo(ts: number): string {
84
- const sec = Math.floor((Date.now() - ts) / 1000);
94
+ const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
85
95
  if (sec < 60) return `${sec}s ago`;
86
96
  if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
87
97
  return `${Math.floor(sec / 3600)}h ago`;
88
98
  }
89
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
+
90
109
  function latColor(ms: number): string {
91
110
  if (ms >= LATENCY_BAD_MS) return C.bad;
92
111
  if (ms >= LATENCY_WARN_MS) return C.warn;
@@ -98,6 +117,17 @@ function statusCodeFromError(msg: string): number {
98
117
  return m ? parseInt(m[1]!, 10) : 0;
99
118
  }
100
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
+
101
131
  // ---------------------------------------------------------------------------
102
132
  // DebugPanel
103
133
  // ---------------------------------------------------------------------------
@@ -105,12 +135,16 @@ function statusCodeFromError(msg: string): number {
105
135
  /**
106
136
  * Real-time API debug panel.
107
137
  *
108
- * Shows per-call log (model, provider, input/output tokens, latency, status code),
109
- * 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).
110
142
  *
111
- * 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.
112
146
  */
113
- export class DebugPanel extends BasePanel {
147
+ export class DebugPanel extends ScrollableListPanel<ApiCallEntry> {
114
148
  private _unsubs: Array<() => void> = [];
115
149
 
116
150
  // Timing state
@@ -122,17 +156,32 @@ export class DebugPanel extends BasePanel {
122
156
  private _prevInput = 0;
123
157
  private _prevOutput = 0;
124
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
+
125
166
  // Session data
126
167
  private _calls: ApiCallEntry[] = [];
127
168
  private _errors: ApiCallEntry[] = [];
128
169
  private _totalCalls = 0;
129
170
  private _totalErrors = 0;
130
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
+
131
178
  constructor(
132
179
  private readonly turnEvents: UiEventFeed<TurnEvent>,
133
180
  private readonly requestRender: () => void = () => {},
134
181
  ) {
135
- super('debug', 'Debug', 'B', 'monitoring');
182
+ super('debug', 'Debug', '', 'incidents-diagnostics');
183
+ this.filterEnabled = true;
184
+ this.filterLabel = 'Filter calls';
136
185
  this._subscribe();
137
186
  }
138
187
 
@@ -166,6 +215,13 @@ export class DebugPanel extends BasePanel {
166
215
  }),
167
216
  );
168
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
+
169
225
  this._unsubs.push(
170
226
  this.turnEvents.on('LLM_RESPONSE_RECEIVED', (env) => {
171
227
  const now = Date.now();
@@ -176,6 +232,8 @@ export class DebugPanel extends BasePanel {
176
232
  ? now - this._turnStartMs
177
233
  : 0;
178
234
  this._streamStartMs = null;
235
+ this._inflightProvider = null;
236
+ this._inflightModel = null;
179
237
 
180
238
  // Compute per-call token delta if orchestrator is wired
181
239
  let inputTokens = env.inputTokens + (env.cacheReadTokens ?? 0) + (env.cacheWriteTokens ?? 0);
@@ -195,7 +253,9 @@ export class DebugPanel extends BasePanel {
195
253
  inputTokens,
196
254
  outputTokens,
197
255
  latencyMs,
198
- 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,
199
259
  status: 'ok',
200
260
  };
201
261
  this._pushCall(entry);
@@ -214,8 +274,8 @@ export class DebugPanel extends BasePanel {
214
274
 
215
275
  const entry: ApiCallEntry = {
216
276
  ts: Date.now(),
217
- provider: 'unknown',
218
- model: 'unknown',
277
+ provider: this._inflightProvider ?? 'n/a',
278
+ model: this._inflightModel ?? 'n/a',
219
279
  inputTokens: 0,
220
280
  outputTokens: 0,
221
281
  latencyMs: 0,
@@ -223,18 +283,42 @@ export class DebugPanel extends BasePanel {
223
283
  status: 'error',
224
284
  errorMessage: msg.slice(0, 120),
225
285
  };
286
+ this._inflightProvider = null;
287
+ this._inflightModel = null;
226
288
  this._pushCall(entry);
227
289
  this._pushError(entry);
228
290
  this.markDirty();
229
291
  this.requestRender();
230
292
  }),
231
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
+ );
232
308
  }
233
309
 
234
310
  private _pushCall(entry: ApiCallEntry): void {
235
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;
236
316
  this._calls.push(entry);
237
- 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;
238
322
  }
239
323
 
240
324
  private _pushError(entry: ApiCallEntry): void {
@@ -247,80 +331,232 @@ export class DebugPanel extends BasePanel {
247
331
  // Lifecycle
248
332
  // -------------------------------------------------------------------------
249
333
 
334
+ override onActivate(): void {
335
+ super.onActivate();
336
+ this._startAgesTimer();
337
+ }
338
+
339
+ override onDeactivate(): void {
340
+ this._stopAgesTimer();
341
+ }
342
+
250
343
  override onDestroy(): void {
344
+ this._stopAgesTimer();
251
345
  for (const unsub of this._unsubs) unsub();
252
346
  this._unsubs = [];
253
347
  }
254
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
+
255
437
  // -------------------------------------------------------------------------
256
438
  // Rendering
257
439
  // -------------------------------------------------------------------------
258
440
 
259
441
  override render(width: number, height: number): Line[] {
260
- const sections: PanelWorkspaceSection[] = [
261
- {
262
- title: 'Session',
263
- lines: this._renderSummary(width),
264
- },
265
- ];
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.';
266
455
 
267
456
  if (this._calls.length === 0) {
268
- sections.push({
269
- title: 'API Call Log',
270
- lines: buildEmptyState(
271
- width,
272
- ' No calls yet',
273
- 'Completed provider calls and API failures will appear here with timing, token counts, and status codes.',
274
- [],
275
- DEFAULT_PANEL_PALETTE,
276
- ),
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,
277
471
  });
278
- } else {
279
- const rows = this._renderCallLog(width);
280
- if (this._errors.length > 0) {
281
- const errors = this._renderErrorHistory(width);
282
- const [callSection, errorSection] = resolveStackedScrollableSections(width, height, {
283
- palette: DEFAULT_PANEL_PALETTE,
284
- beforeSections: sections,
285
- sections: [
286
- {
287
- title: 'API Call Log',
288
- scrollableLines: rows,
289
- scrollOffset: Math.max(0, rows.length - 1),
290
- minRows: 8,
291
- weight: 2,
292
- },
293
- {
294
- title: 'Error History',
295
- scrollableLines: errors,
296
- scrollOffset: Math.max(0, errors.length - 1),
297
- minRows: 4,
298
- weight: 1,
299
- },
300
- ],
301
- });
302
- sections.push(callSection!.section, errorSection!.section);
303
- } else {
304
- const callSection = resolveScrollablePanelSection(width, height, {
305
- palette: DEFAULT_PANEL_PALETTE,
306
- beforeSections: sections,
307
- section: {
308
- title: 'API Call Log',
309
- scrollableLines: rows,
310
- scrollOffset: Math.max(0, rows.length - 1),
311
- minRows: 8,
312
- },
313
- });
314
- sections.push(callSection.section);
315
- }
472
+ while (lines.length < height) lines.push(createEmptyLine(width));
473
+ return lines.slice(0, height);
316
474
  }
317
475
 
318
- return buildPanelWorkspace(width, height, {
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
+
510
+ const latestError = this._errors[this._errors.length - 1];
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, {
319
552
  title: ' API Debug',
320
- intro: 'Recent provider calls, token deltas, latency, status codes, and error history.',
321
- sections,
322
- palette: DEFAULT_PANEL_PALETTE,
553
+ intro,
554
+ sections: [summarySection, filterSection, callSection.section, detailSection],
555
+ footerLines,
556
+ palette: C,
323
557
  });
558
+ while (lines.length < height) lines.push(createEmptyLine(width));
559
+ return lines.slice(0, height);
324
560
  }
325
561
 
326
562
  // -------------------------------------------------------------------------
@@ -330,92 +566,44 @@ export class DebugPanel extends BasePanel {
330
566
  private _renderSummary(width: number): Line[] {
331
567
  const errCount = this._totalErrors;
332
568
  const okCount = this._totalCalls - this._totalErrors;
333
- return [
569
+ const last = this._calls[this._calls.length - 1];
570
+ const recent = this._calls.slice(-10);
571
+ const avgLat = recent.length > 0
572
+ ? Math.round(recent.reduce((s, c) => s + c.latencyMs, 0) / recent.length)
573
+ : 0;
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);
576
+
577
+ const lines: Line[] = [
334
578
  buildStyledPanelLine(width, [
335
- { text: ' Calls: ', fg: C.label },
579
+ { text: ' Calls ', fg: C.label },
336
580
  { text: String(this._totalCalls), fg: C.value },
337
- { text: ' OK: ', fg: C.label },
581
+ { text: ' OK ', fg: C.label },
338
582
  { text: String(okCount), fg: C.good },
339
- { text: ' Errors: ', fg: C.label },
583
+ { text: ' Errors ', fg: C.label },
340
584
  { text: String(errCount), fg: errCount > 0 ? C.bad : C.dim },
585
+ { text: ' Avg latency ', fg: C.label },
586
+ { text: fmtMs(avgLat), fg: avgLat > 0 ? latColor(avgLat) : C.dim },
587
+ { text: ' Tokens ', fg: C.label },
588
+ { text: fmtTok(sessionTokens), fg: C.value },
589
+ { text: ' Cost ', fg: C.label },
590
+ { text: fmtUsd(sessionCost), fg: C.value },
341
591
  ]),
342
592
  ];
343
- }
344
-
345
- private _renderCallLog(width: number): Line[] {
346
- const lines: Line[] = [];
347
- lines.push(this._callLogHeader(width));
348
- for (const entry of this._calls) {
349
- lines.push(this._callLogRow(entry, width));
593
+ // Live status: most recent call (latency / age) or wiring hint.
594
+ if (last) {
595
+ lines.push(buildStyledPanelLine(width, [
596
+ { text: ' Last ', fg: C.label },
597
+ { text: last.status === 'ok' ? '✓ ' : '✕ ', fg: last.status === 'ok' ? C.good : C.bad },
598
+ { text: fitDisplay(last.model, 22), fg: C.value },
599
+ { text: ' ' + fmtMs(last.latencyMs), fg: latColor(last.latencyMs) },
600
+ { text: ' ' + fmtAgo(last.ts), fg: C.dim },
601
+ ]));
602
+ } else if (!this._orchestrator) {
603
+ lines.push(buildStyledPanelLine(width, [
604
+ { text: ' Per-call token deltas need the orchestrator wired (wireOrchestrator).', fg: C.dim },
605
+ ]));
350
606
  }
351
-
352
607
  return lines;
353
608
  }
354
-
355
- private _callLogHeader(width: number): Line {
356
- // Layout: time(8) status(2) provider(12) model(20) in(8) out(8) lat(8)
357
- const header = ' Time S Provider Model In Out Lat';
358
- return this._textLine(header.slice(0, width), C.colHdr, width, { dim: true });
359
- }
360
-
361
- private _callLogRow(e: ApiCallEntry, width: number): Line {
362
- const timeStr = fmtAgo(e.ts).padEnd(8);
363
- const statusChar = e.status === 'ok' ? '✓' : '✕';
364
- const statusFg = e.status === 'ok' ? C.good : C.bad;
365
- const provStr = e.provider.slice(0, 11).padEnd(12);
366
- const modelStr = e.model.slice(0, 19).padEnd(20);
367
- const inStr = fmtTok(e.inputTokens).padStart(8);
368
- const outStr = fmtTok(e.outputTokens).padStart(8);
369
- const latStr = fmtMs(e.latencyMs).padStart(8);
370
-
371
- const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
372
- { text: ' ' + timeStr, fg: C.dim },
373
- { text: statusChar + ' ', fg: statusFg },
374
- { text: provStr, fg: C.provName },
375
- { text: modelStr, fg: C.value },
376
- { text: inStr, fg: C.input },
377
- { text: outStr, fg: C.output },
378
- { text: latStr, fg: latColor(e.latencyMs) },
379
- ];
380
-
381
- // Append status code for errors
382
- if (e.status === 'error' && e.statusCode > 0) {
383
- segments.push({ text: ` [${e.statusCode}]`, fg: C.bad });
384
- }
385
-
386
- return buildStyledPanelLine(
387
- width,
388
- segments.map((seg) => ({ text: seg.text, fg: seg.fg, bold: seg.bold })),
389
- );
390
- }
391
-
392
- private _renderErrorHistory(width: number): Line[] {
393
- const lines: Line[] = [];
394
- for (const e of this._errors) {
395
- lines.push(this._errorRow(e, width));
396
- }
397
-
398
- return lines;
399
- }
400
-
401
- private _errorRow(e: ApiCallEntry, width: number): Line {
402
- const timeStr = fmtAgo(e.ts).padEnd(8);
403
- const codeStr = e.statusCode > 0 ? `[${e.statusCode}] ` : '';
404
- const msgStr = (e.errorMessage ?? 'unknown error').slice(0, width - 12 - codeStr.length);
405
- const full = ` ${timeStr} ${codeStr}${msgStr}`;
406
- return this._textLine(full.slice(0, width), C.bad, width);
407
- }
408
-
409
- // -------------------------------------------------------------------------
410
- // Line-builder helpers
411
- // -------------------------------------------------------------------------
412
-
413
- private _textLine(
414
- text: string,
415
- fg: string,
416
- width: number,
417
- opts: { dim?: boolean } = {},
418
- ): Line {
419
- return buildStyledPanelLine(width, [{ text, fg, dim: opts.dim }]);
420
- }
421
609
  }