@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.
- package/CHANGELOG.md +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- 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-content-actions.ts +8 -3
- 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/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- 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/base-panel.ts +1 -1
- 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 +209 -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 +310 -129
- 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/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- 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/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createEmptyLine,
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
107
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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', '
|
|
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
|
-
|
|
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: '
|
|
216
|
-
model: '
|
|
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)
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
267
|
-
title: 'API
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
|
318
|
-
? [
|
|
319
|
-
{
|
|
320
|
-
{
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
329
|
-
sections,
|
|
553
|
+
intro,
|
|
554
|
+
sections: [summarySection, filterSection, callSection.section, detailSection],
|
|
330
555
|
footerLines,
|
|
331
|
-
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
|
}
|